Saya ingin memastikan bahwa saya memahami konsep injeksi ketergantungan (DI). Yah, saya benar-benar mengerti konsepnya, DI tidak rumit: Anda membuat antarmuka lalu melewati implementasi antarmuka saya ke kelas yang menggunakannya. Cara umum untuk melewatinya adalah dengan konstruktor tetapi Anda juga dapat melewatinya dengan setter atau metode lain.
Yang saya tidak yakin untuk mengerti adalah kapan harus menggunakan DI.
Penggunaan 1: Tentu saja menggunakan DI jika Anda memiliki beberapa implementasi antarmuka Anda tampaknya logis. Anda memiliki repositori untuk SQL Server Anda kemudian yang lain untuk database Oracle Anda. Keduanya berbagi antarmuka yang sama dan Anda "menyuntikkan" (ini adalah istilah yang digunakan) yang Anda inginkan saat runtime. Ini bahkan bukan DI, ini adalah pemrograman OO dasar di sini.
Penggunaan 2: Ketika Anda memiliki lapisan bisnis dengan banyak layanan dengan semua metode spesifiknya, praktik terbaiknya adalah membuat antarmuka untuk setiap layanan dan juga menyuntikkan implementasinya walaupun ini unik. Karena ini lebih baik untuk pemeliharaan. Ini penggunaan kedua yang tidak saya mengerti.
Saya memiliki sekitar 50 kelas bisnis. Tidak ada yang umum di antara mereka. Beberapa repositori mendapatkan atau menyimpan data dalam 3 database berbeda. Beberapa membaca atau menulis file. Beberapa melakukan tindakan bisnis murni. Ada juga validator dan pembantu khusus. Tantangannya adalah manajemen memori karena beberapa kelas dipasang dari lokasi yang berbeda. Validator dapat memanggil beberapa repositori dan validator lain yang dapat memanggil repositori yang sama lagi.
Contoh: Lapisan bisnis
public class SiteService : Service, ICrud<Site>
{
public Site Read(Item item, Site site)
{
return beper4DbContext.Site
.AsNoTracking()
.SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
}
public Site Read(string itemCode, string siteCode)
{
using (var itemService = new ItemService())
{
var item = itemService.Read(itemCode);
return Read(item, site);
}
}
}
public class ItemSiteService : Service, ICrud<Site>
{
public ItemSite Read(Item item, Site site)
{
return beper4DbContext.ItemSite
.AsNoTracking()
.SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
}
public ItemSite Read(string itemCode, string siteCode)
{
using (var itemService = new ItemService())
using (var siteService = new SiteService())
{
var item = itemService.Read(itemCode);
var site = siteService.Read(itemCode, siteCode);
return Read(item, site);
}
}
}
Pengendali
public class ItemSiteController : BaseController
{
[Route("api/Item/{itemCode}/ItemSite/{siteCode}")]
public IHttpActionResult Get(string itemCode, string siteCode)
{
using (var service = new ItemSiteService())
{
var itemSite = service.Read(itemCode, siteCode);
return Ok(itemSite);
}
}
}
Contoh ini sangat mendasar tetapi Anda melihat bagaimana saya dapat dengan mudah membuat 2 instance itemService untuk mendapatkan itemSite. Kemudian juga setiap layanan datang dengan konteks DB-nya. Jadi panggilan ini akan membuat 3 DbContext. 3 Koneksi.
Ide pertama saya adalah membuat singleton untuk menulis ulang semua kode ini seperti di bawah. Kode lebih mudah dibaca dan yang paling penting sistem singleton hanya membuat satu instance dari setiap layanan yang digunakan dan membuatnya pada panggilan pertama. Sempurna, kecuali saya masih memiliki konteks berbeda tetapi saya dapat melakukan sistem yang sama untuk konteks saya. Sudah selesai.
Lapisan bisnis
public class SiteService : Service, ICrud<Site>
{
public Site Read(Item item, Site site)
{
return beper4DbContext.Site
.AsNoTracking()
.SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
}
public Site Read(string itemCode, string siteCode)
{
var item = ItemService.Instance.Read(itemCode);
return Read(item, site);
}
}
public class ItemSiteService : Service, ICrud<Site>
{
public ItemSite Read(Item item, Site site)
{
return beper4DbContext.ItemSite
.AsNoTracking()
.SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
}
public ItemSite Read(string itemCode, string siteCode)
{
var item = ItemService.Instance.Read(itemCode);
var site = SiteService.Instance.Read(itemCode, siteCode);
return Read(item, site);
}
}
Pengendali
public class ItemSiteController : BaseController
{
[Route("api/Item/{itemCode}/ItemSite/{siteCode}")]
public IHttpActionResult Get(string itemCode, string siteCode)
{
var itemSite = service.Instance.Read(itemCode, siteCode);
return Ok(itemSite);
}
}
Beberapa orang mengatakan kepada saya sesuai dengan praktik yang baik saya harus menggunakan DI dengan contoh tunggal dan menggunakan singleton adalah praktik yang buruk. Saya harus membuat antarmuka untuk setiap kelas bisnis dan instantiate dengan bantuan wadah DI. Betulkah? DI ini menyederhanakan kode saya. Sulit untuk dipercaya.
sumber
Jawaban:
Use case yang paling "populer" untuk DI (terlepas dari penggunaan pola "strategi" yang telah Anda jelaskan) mungkin adalah unit testing.
Bahkan jika Anda berpikir hanya akan ada satu implementasi "nyata" untuk antarmuka yang disuntikkan, jika Anda melakukan pengujian unit, biasanya ada yang kedua: implementasi "tiruan" dengan satu-satunya tujuan membuat tes terisolasi mungkin dilakukan. Itu memberi Anda keuntungan karena tidak harus berurusan dengan kompleksitas, bug yang mungkin, dan mungkin dampak kinerja komponen "nyata".
Jadi tidak, DI bukan untuk meningkatkan keterbacaan, itu digunakan untuk meningkatkan testabilitas (- tentu saja, tidak secara eksklusif).
Ini bukan tujuan itu sendiri. Jika kelas Anda
ItemService
sangat sederhana, yang tidak membuat jaringan eksternal atau akses basis data, sehingga tidak menghalangi tes unit penulisan untuk sesuatu sepertiSiteService
, menguji yang terakhir dalam isolasi mungkin sepadan dengan usaha, sehingga DI tidak akan perlu. Namun, jikaItemService
mengakses situs lain dengan jaringan, Anda mungkin ingin unit tesSiteService
dipisahkan dari itu, yang dapat dicapai dengan mengganti "nyata"ItemService
denganMockItemService
, yang memberikan beberapa barang palsu kode keras.Izinkan saya menunjukkan hal lain: dalam contoh Anda, orang mungkin berpendapat bahwa seseorang tidak perlu DI di sini untuk menguji logika bisnis inti - contoh selalu menunjukkan dua varian
Read
metode, satu dengan logika bisnis nyata yang terlibat (yang dapat berupa unit diuji tanpa DI), dan yang hanya kode "lem" untuk menghubungkanItemService
ke logika sebelumnya. Dalam kasus yang ditunjukkan, itu memang argumen yang valid terhadap DI - pada kenyataannya, jika DI dapat dihindari tanpa mengorbankan testabilitas seperti itu, maka lanjutkan. Tetapi tidak semua kode dunia nyata sangat sederhana, dan seringkali DI adalah solusi paling sederhana untuk mencapai unit testability yang "cukup".sumber
Dengan tidak menggunakan injeksi dependensi, Anda membiarkan diri membuat koneksi permanen ke objek lain. Koneksi yang bisa Anda sembunyikan di dalam mana mereka akan mengejutkan orang. Koneksi yang hanya dapat diubah dengan menulis ulang apa yang Anda buat.
Daripada itu, Anda dapat menggunakan injeksi dependensi (atau referensi yang lewat jika Anda sudah tua seperti saya) untuk membuat apa yang dibutuhkan suatu objek secara eksplisit tanpa memaksanya untuk menentukan bagaimana kebutuhannya harus dipenuhi.
Ini memaksa Anda untuk menerima banyak parameter. Bahkan yang dengan standar yang jelas. Dalam C # Anda beruntung memiliki argumen nama dan opsional . Itu berarti Anda memiliki argumen default. Jika Anda tidak keberatan terikat secara statis dengan default Anda, bahkan ketika Anda tidak menggunakannya, Anda dapat mengizinkan DI tanpa kewalahan dengan pilihan. Ini mengikuti konvensi tentang konfigurasi .
Pengujian bukanlah pembenaran yang baik untuk DI. Saat Anda berpikir itu adalah seseorang akan menjual kerangka kerja mengejek ahli yang menggunakan refleksi atau sihir lain untuk meyakinkan Anda bahwa Anda dapat kembali ke cara Anda bekerja sebelumnya dan menggunakan sihir untuk melakukan sisanya.
Digunakan dengan benar, pengujian bisa menjadi cara yang baik untuk menunjukkan apakah suatu desain terisolasi. Tapi bukan itu intinya. Itu tidak menghentikan tenaga penjualan untuk mencoba membuktikan bahwa dengan sihir yang cukup semuanya terisolasi. Pertahankan sihir seminimal mungkin.
Maksud dari isolasi ini adalah untuk mengelola perubahan. Sangat menyenangkan jika satu perubahan dapat dilakukan di satu tempat. Tidak baik harus mengikuti file demi file berharap bahwa kegilaan akan berakhir.
Tempatkan saya di toko yang menolak untuk melakukan pengujian unit dan saya masih akan melakukan DI. Saya melakukannya karena itu memungkinkan saya memisahkan apa yang diperlukan dari bagaimana hal itu dilakukan. Pengujian atau tidak ada pengujian Saya ingin isolasi itu.
sumber
Pandangan helikopter DI hanya kemampuan untuk menukar implementasi untuk antarmuka . Meskipun ini tentu saja merupakan anugerah untuk pengujian, ada manfaat potensial lainnya:
Implementasi versi objek
Jika metode Anda menerima parameter antarmuka di lapisan tengah, Anda bebas untuk melewati implementasi apa pun yang Anda suka di lapisan atas yang mengurangi jumlah kode yang perlu ditulis untuk menukar implementasi. Memang ini adalah manfaat dari antarmuka, tetapi jika kode ditulis dengan DI dalam pikiran, Anda akan mendapatkan manfaat ini di luar kotak.
Mengurangi jumlah objek yang perlu melewati lapisan
Meskipun ini terutama berlaku untuk kerangka kerja DI, jika objek A membutuhkan instance objek B , dimungkinkan untuk melakukan query kernel (atau apa pun) untuk menghasilkan objek B dengan cepat daripada meneruskannya melalui lapisan. Ini mengurangi jumlah kode yang perlu ditulis dan diuji. Itu juga membuat lapisan yang tidak peduli tentang objek B bersih.
sumber
Tidak perlu menggunakan antarmuka untuk menggunakan DI. Tujuan utama DI adalah untuk memisahkan konstruksi dan penggunaan objek.
Menggunakan lajang sangat disukai dalam banyak kasus. Salah satu alasannya adalah menjadi sangat sulit untuk mendapatkan gambaran umum tentang dependensi yang dimiliki suatu kelas.
Dalam contoh Anda, ItemSiteController dapat dengan mudah mengambil ItemSiteService sebagai argumen konstruktor. Ini memungkinkan Anda menghindari biaya pembuatan objek, tetapi menghindari ketidakfleksibelan singleton. Hal yang sama berlaku untuk ItemSiteService, jika membutuhkan ItemService dan SiteService, masukkan mereka ke dalam konstruktor.
Manfaatnya paling besar ketika semua benda menggunakan injeksi ketergantungan. Ini memungkinkan Anda untuk memusatkan konstruksi ke modul khusus, atau mendelegasikannya ke wadah DI.
Hirarki ketergantungan mungkin terlihat seperti ini:
Perhatikan bahwa hanya ada satu kelas tanpa parameter konstruktor, dan hanya satu antarmuka. Saat mengkonfigurasi wadah DI, Anda dapat memutuskan penyimpanan apa yang akan digunakan, atau apakah caching harus digunakan, dll. Pengujian lebih mudah karena Anda dapat memutuskan basis data apa yang akan digunakan, atau menggunakan jenis penyimpanan lain. Anda juga dapat mengkonfigurasi wadah DI untuk memperlakukan objek sebagai lajang jika diperlukan, dalam konteks objek wadah.
sumber
Anda mengisolasi sistem eksternal.
Ya, gunakan DI sini. Jika masuk ke jaringan, basis data, sistem file, proses lain, input pengguna, dll. Anda ingin mengisolasinya.
Menggunakan DI akan memudahkan pengujian karena Anda akan dengan mudah mengejek sistem eksternal ini. Tidak, saya tidak mengatakan itu adalah langkah pertama menuju unit testing. Anda juga tidak dapat melakukan pengujian tanpa melakukan ini.
Lebih lanjut, bahkan jika Anda hanya memiliki satu database, menggunakan DI akan membantu Anda pada hari Anda ingin bermigrasi. Jadi, ya, DI.
Tentu, DI dapat membantu Anda. Saya akan berdebat tentang wadah.
Mungkin, sesuatu yang patut dicatat, adalah bahwa ketergantungan injeksi dengan jenis beton masih injeksi ketergantungan. Yang penting adalah Anda dapat membuat instance kustom. Itu tidak harus injeksi antarmuka (meskipun injeksi antarmuka lebih fleksibel, itu tidak berarti Anda harus menggunakannya di mana-mana).
Gagasan membuat antarmuka eksplisit untuk masing-masing dan setiap kelas harus mati. Bahkan, jika Anda hanya memiliki satu implementasi antarmuka ... YAGNI . Menambahkan antarmuka relatif murah, bisa dilakukan saat Anda membutuhkannya. Bahkan, saya sarankan menunggu sampai Anda memiliki dua atau tiga kandidat implementasi sehingga Anda memiliki gagasan yang lebih baik tentang hal-hal apa yang umum di antara mereka.
Namun, sisi lain dari itu, adalah Anda dapat membuat antarmuka yang cocok dengan yang dibutuhkan kode klien. Jika kode klien hanya membutuhkan beberapa anggota kelas, Anda dapat memiliki antarmuka hanya untuk itu. Itu akan menyebabkan pemisahan antarmuka yang lebih baik .
Wadah?
Anda tahu Anda tidak membutuhkannya.
Mari kita bawa ke trade-off. Ada kasus di mana mereka tidak layak. Anda akan meminta kelas Anda mengambil dependensi yang diperlukan pada konstruktor. Dan itu cukup bagus.
Saya benar-benar bukan penggemar atribut anotasi untuk "setter injection", apalagi yang pihak ketiga, saya mengerti mungkin diperlukan untuk implementasi di luar kendali Anda ... namun, jika Anda memutuskan untuk mengubah perpustakaan, ini harus diubah.
Akhirnya Anda akan mulai membangun rutinitas untuk membuat objek-objek ini, karena untuk membuatnya, pertama-tama Anda perlu membuat yang lain ini, dan bagi yang Anda perlukan lagi ...
Nah, ketika itu terjadi, Anda ingin meletakkan semua logika itu di satu tempat dan menggunakannya kembali. Anda ingin satu sumber kebenaran tentang bagaimana Anda membuat objek Anda. Dan Anda mendapatkannya dengan tidak mengulangi diri Anda sendiri . Itu akan menyederhanakan kode Anda. Masuk akal, bukan?
Nah, di mana Anda menempatkan logika itu? Insting pertama adalah memiliki Service Locator . Implementasi sederhana adalah singleton dengan kamus read-only dari pabrik . Implementasi yang lebih kompleks dapat menggunakan refleksi untuk membuat pabrik ketika Anda belum menyediakannya.
Namun, menggunakan pelacak tunggal atau statis akan berarti bahwa Anda akan melakukan sesuatu seperti
var x = IoC.Resolve<?>
setiap tempat Anda perlu membuat instance. Yaitu menambahkan kopling yang kuat ke pelacak lokasi Anda / wadah / injektor. Itu benar-benar dapat membuat unit test lebih sulit.Anda menginginkan injektor yang Anda instantiate, dan simpan hanya untuk digunakan pada controller. Anda tidak ingin itu masuk jauh ke dalam kode. Itu sebenarnya bisa membuat pengujian lebih sulit. Jika beberapa bagian dari kode Anda memerlukannya untuk instantiate sesuatu, itu harus mengharapkan contoh (atau sebagian besar pabrik) pada konstruktornya.
Dan jika Anda memiliki banyak parameter pada konstruktor ... lihat apakah Anda memiliki parameter yang bepergian bersama. Kemungkinannya adalah Anda dapat menggabungkan parameter menjadi tipe deskriptor (tipe nilai idealnya).
sumber