Kami sedang mempersiapkan untuk merilis edisi kedua dari buku legendaris oleh Mark Siman "Injeksi Ketergantungan pada .NET Platform ".
Bahkan dalam buku yang begitu banyak, hampir tidak mungkin untuk membahas topik seperti itu sepenuhnya. Namun kami menawarkan terjemahan singkat dari artikel yang sangat mudah diakses yang menguraikan esensi injeksi ketergantungan dalam bahasa sederhana - dengan contoh dalam C #.
Tujuan artikel ini adalah untuk menjelaskan konsep injeksi ketergantungan dan menunjukkan bagaimana program itu diprogram dalam proyek tertentu. Dari Wikipedia:
Injeksi ketergantungan adalah pola desain yang memisahkan perilaku dari resolusi ketergantungan. Dengan demikian, dimungkinkan untuk melepaskan komponen yang sangat bergantung satu sama lain.
Dependency Injection (atau DI) memungkinkan Anda menyediakan implementasi dan layanan ke kelas lain untuk dikonsumsi; kode tetap digabungkan dengan sangat longgar. Poin utama dalam kasus ini adalah sebagai berikut: sebagai ganti implementasi, Anda dapat dengan mudah mengganti implementasi lain, dan pada saat yang sama Anda harus mengubah kode minimum, karena implementasi dan konsumen terhubung, kemungkinan besar, hanya dengan kontrak .
Dalam C #, ini berarti bahwa implementasi layanan Anda harus sesuai dengan persyaratan antarmuka, dan saat membuat konsumen untuk layanan Anda, Anda harus menargetkan antarmuka , bukan implementasi, dan mengharuskan implementasi disediakan atau diterapkan untuk Anda.sehingga Anda tidak perlu membuat sendiri instance tersebut. Dengan pendekatan ini, Anda tidak perlu khawatir di tingkat kelas tentang bagaimana dependensi dibuat dan dari mana asalnya; dalam hal ini, hanya kontrak yang penting.
Injeksi ketergantungan dengan contoh
Mari kita lihat contoh di mana DI bisa berguna. Pertama, mari buat antarmuka (kontrak) yang memungkinkan kita melakukan beberapa tugas, misalnya, membuat log pesan:
public interface ILogger {
void LogMessage(string message);
}
Harap diperhatikan: antarmuka ini tidak menjelaskan di mana pun cara sebuah pesan dicatat dan di mana ia dicatat; di sini maksud hanya diteruskan untuk menulis string ke beberapa repositori. Selanjutnya, mari buat entitas yang menggunakan antarmuka ini. Katakanlah kita membuat kelas yang melacak direktori tertentu pada disk dan, segera setelah ada perubahan pada direktori, mencatat pesan yang sesuai:
public class DirectoryWatcher {
private ILogger _logger;
private FileSystemWatcher _watcher;
public DirectoryWatcher(ILogger logger) {
_logger = logger;
_watcher = new FileSystemWatcher(@ "C:Temp");
_watcher.Changed += new FileSystemEventHandler(Directory_Changed);
}
void Directory_Changed(object sender, FileSystemEventArgs e) {
_logger.LogMessage(e.FullPath + " was changed");
}
}
Dalam hal ini, yang paling penting untuk diperhatikan adalah kita diberikan konstruktor yang kita butuhkan, yang mengimplementasikan
ILogger
. Tapi, sekali lagi, perhatikan: kami tidak peduli kemana log itu pergi, atau bagaimana itu dibuat. Kami hanya dapat memprogram dengan antarmuka dalam pikiran dan tidak memikirkan hal lain.
Jadi, untuk membuat instance milik kita
DirectoryWatcher
, kita juga membutuhkan implementasi yang sudah jadi
ILogger
. Mari kita lanjutkan dan buat instance yang mencatat pesan ke file teks:
public class TextFileLogger: ILogger {
public void LogMessage(string message) {
using(FileStream stream = new FileStream("log.txt", FileMode.Append)) {
StreamWriter writer = new StreamWriter(stream);
writer.WriteLine(message);
writer.Flush();
}
}
}
Mari buat yang lain yang menulis pesan ke event log Windows:
public class EventFileLogger: ILogger {
private string _sourceName;
public EventFileLogger(string sourceName) {
_sourceName = sourceName;
}
public void LogMessage(string message) {
if (!EventLog.SourceExists(_sourceName)) {
EventLog.CreateEventSource(_sourceName, "Application");
}
EventLog.WriteEntry(_sourceName, message);
}
}
Kami sekarang memiliki dua implementasi terpisah yang mencatat pesan dengan cara yang sangat berbeda, tetapi keduanya melakukannya
ILogger
, yang berarti salah satunya dapat digunakan di mana pun sebuah instance diperlukan
ILogger
. Selanjutnya, Anda dapat membuat sebuah instance
DirectoryWatcher
dan memintanya untuk menggunakan salah satu logger kami:
ILogger logger = new TextFileLogger();
DirectoryWatcher watcher = new DirectoryWatcher(logger);
Atau, cukup dengan mengubah sisi kanan baris pertama, kita dapat menggunakan implementasi yang berbeda:
ILogger logger = new EventFileLogger();
DirectoryWatcher watcher = new DirectoryWatcher(logger);
Semua ini terjadi tanpa perubahan apa pun pada implementasi DirectoryWatcher, dan ini adalah hal yang paling penting. Kami memasukkan implementasi logger kami ke konsumen sehingga konsumen tidak perlu membuat instance sendiri. Contoh yang diperlihatkan memang sepele, tetapi bayangkan bagaimana rasanya menggunakan teknik seperti itu dalam proyek berskala besar di mana Anda memiliki banyak ketergantungan, dan konsumen yang menggunakannya berkali-kali lipat. Dan kemudian tiba-tiba ada permintaan untuk mengubah metode yang mencatat pesan (katakanlah, sekarang pesan harus dicatat ke server SQL untuk tujuan audit). Jika Anda tidak menggunakan injeksi ketergantungan dalam satu bentuk atau lainnya, maka Anda harus meninjau kode dengan hati-hati dan membuat perubahan di mana pun logger sebenarnya dibuat dan kemudian digunakan. Pada proyek besar, pekerjaan seperti itu bisa jadi rumit dan rawan kesalahan.Dengan DI, Anda cukup mengubah dependensi di satu tempat, dan aplikasi lainnya akan benar-benar menyerap perubahan dan segera mulai menggunakan metode logging baru.
Intinya, ini memecahkan masalah perangkat lunak klasik dari ketergantungan yang berat, dan DI memungkinkan Anda membuat kode yang digabungkan secara longgar yang sangat fleksibel dan mudah dimodifikasi.
Kontainer injeksi ketergantungan
Banyak kerangka kerja injeksi DI yang dapat Anda unduh dan gunakan selangkah lebih maju dan menggunakan wadah untuk injeksi ketergantungan. Intinya, ini adalah kelas yang menyimpan pemetaan tipe dan mengembalikan implementasi terdaftar untuk tipe tertentu. Dalam contoh sederhana kita, kita akan dapat menanyakan kontainer untuk sebuah contoh
ILogger
, dan itu akan mengembalikan kita contoh
TextFileLogger
, atau contoh apa pun yang kita inisialisasi penampung.
Dalam hal ini, kami memiliki keuntungan bahwa kami dapat mendaftarkan semua jenis pemetaan di satu tempat, biasanya di mana peristiwa peluncuran aplikasi terjadi, dan ini akan memungkinkan kami untuk dengan cepat dan jelas melihat dependensi apa yang kami miliki dalam sistem. Selain itu, dalam banyak framework profesional, Anda dapat mengonfigurasi masa pakai objek tersebut, baik dengan membuat instance baru dengan setiap permintaan baru, atau menggunakan kembali satu instance dalam beberapa panggilan.
Penampung biasanya dibuat sedemikian rupa sehingga kita dapat mengakses 'resolver' (jenis entitas yang memungkinkan kita meminta instance) dari mana saja dalam proyek.
Terakhir, kerangka kerja profesional biasanya mendukung fenomena subdependensi.- dalam hal ini, ketergantungan itu sendiri memiliki satu atau beberapa ketergantungan pada tipe lain, yang juga dikenal oleh penampung. Dalam kasus ini, resolver dapat memenuhi dependensi tersebut juga, memberi Anda kembali rangkaian lengkap dependensi yang dibuat dengan benar yang sesuai dengan pemetaan tipe Anda.
Mari buat sendiri wadah DI yang sangat sederhana untuk melihat bagaimana semuanya bekerja. Implementasi seperti itu tidak mendukung dependensi bertingkat, tetapi memungkinkan Anda untuk memetakan antarmuka ke implementasi, dan kemudian meminta implementasi ini sendiri:
public class SimpleDIContainer {
Dictionary < Type, object > _map;
public SimpleDIContainer() {
_map = new Dictionary < Type, object > ();
}
/// <summary>
/// , .
/// </summary>
/// <typeparam name="TIn">The interface type</typeparam>
/// <typeparam name="TOut">The implementation type</typeparam>
/// <param name="args">Optional arguments for the creation of the implementation type.</param>
public void Map <TIn, TOut> (params object[] args) {
if (!_map.ContainsKey(typeof(TIn))) {
object instance = Activator.CreateInstance(typeof(TOut), args);
_map[typeof(TIn)] = instance;
}
}
/// <summary>
/// , T
/// </summary>
/// <typeparam name="T">The interface type</typeparam>
public T GetService<T> () where T: class {
if (_map.ContainsKey(typeof(T))) return _map[typeof(T)] as T;
else throw new ApplicationException("The type " + typeof(T).FullName + " is not registered in the container");
}
}
Selanjutnya, kita bisa menulis program kecil yang membuat wadah, menampilkan tipe, dan kemudian meminta layanan. Sekali lagi, contoh sederhana dan ringkas, tapi bayangkan seperti apa tampilannya dalam aplikasi yang jauh lebih besar:
public class SimpleDIContainer {
Dictionary <Type, object> _map;
public SimpleDIContainer() {
_map = new Dictionary < Type, object > ();
}
/// <summary>
/// , .
/// </summary>
/// <typeparam name="TIn">The interface type</typeparam>
/// <typeparam name="TOut">The implementation type</typeparam>
/// <param name="args">Optional arguments for the creation of the implementation type.</param>
public void Map <TIn, TOut> (params object[] args) {
if (!_map.ContainsKey(typeof(TIn))) {
object instance = Activator.CreateInstance(typeof(TOut), args);
_map[typeof(TIn)] = instance;
}
}
/// <summary>
/// , T
/// </summary>
/// <typeparam name="T">The interface type</typeparam>
public T GetService <T> () where T: class {
if (_map.ContainsKey(typeof(T))) return _map[typeof(T)] as T;
else throw new ApplicationException("The type " + typeof(T).FullName + " is not registered in the container");
}
}
Saya sarankan untuk tetap berpegang pada pola ini saat menambahkan dependensi baru ke proyek Anda. Ketika proyek Anda bertambah besar, Anda akan melihat sendiri betapa mudahnya mengelola komponen yang digabungkan secara longgar. Fleksibilitas yang cukup besar diperoleh, dan proyek itu sendiri pada akhirnya jauh lebih mudah untuk dipelihara, dimodifikasi, dan disesuaikan dengan kondisi baru.