Saya sedang merenungkan desain perpustakaan C #, yang akan memiliki beberapa fungsi tingkat tinggi yang berbeda. Tentu saja, fungsi-fungsi tingkat tinggi akan diimplementasikan menggunakan prinsip-prinsip desain kelas SOLID sebanyak mungkin. Dengan demikian, mungkin akan ada kelas yang ditujukan bagi konsumen untuk digunakan secara langsung secara teratur, dan "kelas pendukung" yang merupakan dependensi dari kelas "pengguna akhir" yang lebih umum.
Pertanyaannya adalah, apa cara terbaik untuk mendesain perpustakaan sehingga:
- DI Agnostik - Meskipun menambahkan "dukungan" dasar untuk satu atau dua perpustakaan DI umum (StructureMap, Ninject, dll) tampaknya masuk akal, saya ingin konsumen dapat menggunakan perpustakaan dengan kerangka kerja DI.
- Tidak dapat digunakan - Jika pengguna perpustakaan tidak menggunakan DI, perpustakaan harus tetap semudah mungkin digunakan, mengurangi jumlah pekerjaan yang harus dilakukan pengguna untuk membuat semua dependensi "tidak penting" ini hanya untuk sampai ke kelas "nyata" yang ingin mereka gunakan.
Pemikiran saya saat ini adalah untuk menyediakan beberapa "modul registrasi DI" untuk perpustakaan DI umum (misalnya registri StructureMap, modul Ninject), dan satu set atau kelas Pabrik yang non-DI dan berisi sambungan ke beberapa pabrik tersebut.
Pikiran?
Jawaban:
Ini sebenarnya mudah dilakukan setelah Anda memahami bahwa DI adalah tentang pola dan prinsip , bukan teknologi.
Untuk merancang API dengan cara AG-agnostik DI, ikuti prinsip-prinsip umum ini:
Program ke antarmuka, bukan implementasi
Prinsip ini sebenarnya adalah kutipan (dari memori meskipun) dari Pola Desain , tetapi harus selalu menjadi tujuan nyata Anda . DI hanyalah sarana untuk mencapai tujuan itu .
Terapkan Prinsip Hollywood
Prinsip Hollywood dalam istilah DI mengatakan: Jangan panggil DI Container, itu akan memanggil Anda .
Jangan pernah secara langsung meminta dependensi dengan memanggil kontainer dari dalam kode Anda. Tanyakan secara implisit dengan menggunakan Constructor Injection .
Gunakan Injeksi Konstruktor
Ketika Anda membutuhkan dependensi, mintalah secara statis melalui konstruktor:
Perhatikan bagaimana kelas Layanan menjamin invariannya. Setelah sebuah instance dibuat, ketergantungan dijamin akan tersedia karena kombinasi Klausa Penjaga dan
readonly
kata kunci.Gunakan Pabrik Abstrak jika Anda membutuhkan objek berumur pendek
Ketergantungan yang disuntikkan dengan Constructor Injection cenderung berumur panjang, tetapi kadang-kadang Anda membutuhkan objek berumur pendek, atau untuk membangun ketergantungan berdasarkan pada nilai yang hanya diketahui pada saat run-time.
Lihat ini untuk informasi lebih lanjut.
Menulis hanya pada Momen Bertanggung Jawab Terakhir
Jagalah agar benda-benda tetap terpisah sampai akhir. Biasanya, Anda bisa menunggu dan memasang semuanya di titik masuk aplikasi. Ini disebut Root Komposisi .
Lebih detail di sini:
Sederhanakan menggunakan Facade
Jika Anda merasa bahwa API yang dihasilkan menjadi terlalu rumit untuk pengguna pemula, Anda selalu dapat memberikan beberapa kelas Fasad yang merangkum kombinasi ketergantungan umum.
Untuk memberikan Fasad yang fleksibel dengan tingkat penemuan yang tinggi, Anda dapat mempertimbangkan untuk menyediakan Pembuat Fluent. Sesuatu seperti ini:
Ini akan memungkinkan pengguna untuk membuat Foo default dengan menulis
Namun, akan sangat mudah ditemukan bahwa mungkin untuk menyediakan ketergantungan khusus, dan Anda dapat menulis
Jika Anda membayangkan bahwa kelas MyFacade merangkum banyak dependensi yang berbeda, saya harap jelas bagaimana ini akan memberikan default yang tepat sambil tetap membuat diperpanjang dapat ditemukan.
FWIW, lama setelah menulis jawaban ini, saya memperluas konsep-konsep di sini dan menulis posting blog yang lebih panjang tentang DI-Friendly Libraries , dan sebuah postingan tentang DI-Friendly Frameworks .
sumber
Istilah "injeksi ketergantungan" tidak secara spesifik berkaitan dengan wadah IoC sama sekali, meskipun Anda cenderung melihat mereka disebutkan bersama. Ini berarti bahwa alih-alih menulis kode Anda seperti ini:
Anda menulis seperti ini:
Yaitu, Anda melakukan dua hal ketika menulis kode:
Andalkan antarmuka bukan kelas kapan pun Anda berpikir bahwa implementasi mungkin perlu diubah;
Alih-alih membuat instance antarmuka ini di dalam kelas, berikan mereka sebagai argumen konstruktor (atau, mereka dapat ditugaskan ke properti publik; yang pertama adalah injeksi konstruktor , yang terakhir adalah injeksi properti ).
Tak satu pun dari ini mengandaikan keberadaan perpustakaan DI, dan itu tidak benar-benar membuat kode lebih sulit untuk menulis tanpa satu.
Jika Anda mencari contoh ini, tidak terlihat lagi .NET Framework itu sendiri:
List<T>
mengimplementasikanIList<T>
. Jika Anda mendesain kelas Anda untuk menggunakanIList<T>
(atauIEnumerable<T>
), Anda dapat mengambil keuntungan dari konsep-konsep seperti lazy-loading, seperti Linq to SQL, Linq to Entities, dan NHibernate semua dilakukan di belakang layar, biasanya melalui injeksi properti. Beberapa kelas framework sebenarnya menerimaIList<T>
argumen konstruktor, sepertiBindingList<T>
, yang digunakan untuk beberapa fitur pengikatan data.Linq to SQL dan EF dibangun seluruhnya di sekitar
IDbConnection
dan antarmuka terkait, yang dapat diteruskan melalui konstruktor publik. Anda tidak perlu menggunakannya; konstruktor default berfungsi dengan baik dengan string koneksi yang berada di file konfigurasi di suatu tempat.Jika Anda pernah bekerja pada komponen WinForms Anda berurusan dengan "layanan", suka
INameCreationService
atauIExtenderProviderService
. Anda bahkan tidak benar-benar tahu apa kelas konkret itu . .NET sebenarnya memiliki wadah IoC sendiriIContainer
,, yang digunakan untuk ini, danComponent
kelas memilikiGetService
metode yang merupakan pencari lokasi sebenarnya. Tentu saja, tidak ada yang mencegah Anda menggunakan salah satu atau semua antarmuka ini tanpaIContainer
atau pelacak itu. Layanan itu sendiri hanya longgar digabungkan dengan wadah.Kontrak dalam WCF sepenuhnya dibangun di sekitar antarmuka. Kelas layanan konkret yang sebenarnya biasanya dirujuk dengan nama dalam file konfigurasi, yang pada dasarnya adalah DI. Banyak orang tidak menyadari hal ini tetapi sangat mungkin untuk menukar sistem konfigurasi ini dengan wadah IoC lainnya. Mungkin yang lebih menarik, perilaku layanan adalah contoh
IServiceBehavior
yang dapat ditambahkan nanti. Sekali lagi, Anda dapat dengan mudah mentransfer ini ke wadah IoC dan membuatnya memilih perilaku yang relevan, tetapi fitur ini sepenuhnya dapat digunakan tanpa wadah.Demikian seterusnya dan seterusnya. Anda akan menemukan DI di semua tempat di .NET, hanya saja biasanya dilakukan dengan mulus sehingga Anda bahkan tidak menganggapnya sebagai DI.
Jika Anda ingin merancang pustaka berkemampuan DI-Anda untuk kegunaan maksimum, maka saran terbaik mungkin untuk memasok implementasi IoC default Anda sendiri menggunakan wadah yang ringan.
IContainer
adalah pilihan yang bagus untuk ini karena ini merupakan bagian dari .NET Framework itu sendiri.sumber
IContainer
sebenarnya bukan wadah dalam satu paragraf. ;)private readonly
bidangnya tidak? Itu bagus jika mereka hanya digunakan oleh kelas mendeklarasikan tetapi OP menetapkan bahwa ini adalah kode kerangka kerja / tingkat perpustakaan, yang menyiratkan subclassing. Dalam kasus seperti itu Anda biasanya menginginkan dependensi penting yang terpapar pada subkelas. Saya bisa menulisprivate readonly
bidang dan properti dengan getter / setter eksplisit, tapi ... membuang-buang ruang dalam contoh, dan lebih banyak kode untuk dipelihara dalam praktik, tanpa manfaat nyata.EDIT 2015 : waktu telah berlalu, saya menyadari sekarang bahwa semua ini adalah kesalahan besar. Kontainer IoC mengerikan dan DI adalah cara yang sangat buruk untuk menangani efek samping. Secara efektif, semua jawaban di sini (dan pertanyaan itu sendiri) harus dihindari. Sadarilah efek samping, pisahkan dari kode murni, dan segala sesuatu yang lain ada di tempatnya atau tidak relevan dan kompleksitas yang tidak perlu.
Jawaban asli berikut:
Saya harus menghadapi keputusan yang sama saat mengembangkan SolrNet . Saya mulai dengan tujuan menjadi ramah-DI dan agnostik-wadah, tetapi ketika saya menambahkan semakin banyak komponen internal, pabrik-pabrik internal dengan cepat menjadi tidak terkelola dan perpustakaan yang dihasilkan tidak fleksibel.
Saya akhirnya menulis wadah IoC saya sendiri yang sangat sederhana sambil juga menyediakan fasilitas Windsor dan modul Ninject . Mengintegrasikan perpustakaan dengan wadah lain hanya masalah pengkabelan komponen dengan benar, jadi saya dapat dengan mudah mengintegrasikannya dengan Autofac, Unity, StructureMap, apa pun.
Kelemahan dari ini adalah saya kehilangan kemampuan untuk
new
meningkatkan layanan. Saya juga mengambil ketergantungan pada CommonServiceLocator yang bisa saya hindari (saya mungkin akan menolaknya di masa depan) untuk membuat wadah yang tertanam lebih mudah diimplementasikan.Lebih detail di posting blog ini .
MassTransit tampaknya mengandalkan sesuatu yang serupa. Ini memiliki antarmuka IObjectBuilder yang benar-benar CommonerviceLocator's IServiceLocator dengan beberapa metode lagi, kemudian mengimplementasikannya untuk setiap wadah, yaitu NinjectObjectBuilder dan modul / fasilitas reguler, yaitu MassTransitModule . Maka itu bergantung pada IObjectBuilder untuk instantiate apa yang dibutuhkan. Ini adalah pendekatan yang valid tentu saja, tetapi secara pribadi saya tidak terlalu menyukainya karena itu sebenarnya terlalu banyak beredar, menggunakannya sebagai pencari lokasi.
MonoRail juga mengimplementasikan wadahnya sendiri , yang mengimplementasikan IServiceProvider tua yang baik . Wadah ini digunakan di seluruh kerangka kerja ini melalui antarmuka yang memaparkan layanan terkenal . Untuk mendapatkan wadah beton, ia memiliki pelacak penyedia layanan bawaan . Fasilitas Windsor menunjukkan lokasi penyedia layanan ini ke Windsor, menjadikannya penyedia layanan yang dipilih.
Intinya: tidak ada solusi yang sempurna. Seperti halnya keputusan desain, masalah ini menuntut keseimbangan antara fleksibilitas, pemeliharaan dan kenyamanan.
sumber
ServiceLocator
. Jika Anda menerapkan teknik fungsional, Anda berakhir dengan setara dengan penutupan, yang merupakan kelas ketergantungan-disuntikkan (di mana dependensi adalah variabel tertutup). Bagaimana lagi? (Aku benar-benar ingin tahu, karena saya tidak suka DI baik.)Apa yang akan saya lakukan adalah mendesain perpustakaan saya dalam wadah agnostik DI cara untuk membatasi ketergantungan pada wadah sebanyak mungkin. Ini memungkinkan untuk menukar dengan wadah DI untuk yang lain jika perlu.
Kemudian paparkan layer di atas logika DI kepada pengguna perpustakaan sehingga mereka dapat menggunakan kerangka apa pun yang Anda pilih melalui antarmuka Anda. Dengan cara ini mereka masih dapat menggunakan fungsi DI yang Anda tampilkan dan mereka bebas menggunakan kerangka kerja lain untuk tujuan mereka sendiri.
Mengizinkan pengguna perpustakaan untuk memasang kerangka DI mereka sendiri tampaknya agak salah bagi saya karena secara dramatis meningkatkan jumlah pemeliharaan. Ini juga kemudian menjadi lebih dari lingkungan plugin daripada DI lurus.
sumber