Dependency Inject (DI) "ramah" perpustakaan

230

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?

Pete
sumber
Referensi baru untuk artikel tautan rusak (SOLID): butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
M.Hassan

Jawaban:

360

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:

public class Service : IService
{
    private readonly ISomeDependency dep;

    public Service(ISomeDependency dep)
    {
        if (dep == null)
        {
            throw new ArgumentNullException("dep");
        }

        this.dep = dep;
    }

    public ISomeDependency Dependency
    {
        get { return this.dep; }
    }
}

Perhatikan bagaimana kelas Layanan menjamin invariannya. Setelah sebuah instance dibuat, ketergantungan dijamin akan tersedia karena kombinasi Klausa Penjaga dan readonlykata 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:

public class MyFacade
{
    private IMyDependency dep;

    public MyFacade()
    {
        this.dep = new DefaultDependency();
    }

    public MyFacade WithDependency(IMyDependency dependency)
    {
        this.dep = dependency;
        return this;
    }

    public Foo CreateFoo()
    {
        return new Foo(this.dep);
    }
}

Ini akan memungkinkan pengguna untuk membuat Foo default dengan menulis

var foo = new MyFacade().CreateFoo();

Namun, akan sangat mudah ditemukan bahwa mungkin untuk menyediakan ketergantungan khusus, dan Anda dapat menulis

var foo = new MyFacade().WithDependency(new CustomDependency()).CreateFoo();

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 .

Mark Seemann
sumber
21
Walaupun ini kedengarannya bagus di atas kertas, menurut pengalaman saya begitu Anda memiliki banyak komponen dalam berinteraksi dengan cara yang kompleks, Anda berakhir dengan banyak pabrik untuk dikelola, membuat pemeliharaan lebih sulit. Plus, pabrik harus mengelola gaya hidup komponen yang mereka buat, setelah Anda menginstal perpustakaan dalam wadah nyata ini akan bertentangan dengan manajemen gaya hidup wadah itu sendiri. Pabrik dan fasad menghalangi wadah yang sebenarnya.
Mauricio Scheffer
4
Saya belum menemukan proyek yang melakukan semua ini.
Mauricio Scheffer
31
Nah, begitulah cara kami mengembangkan perangkat lunak di Safewhere, jadi kami tidak membagikan pengalaman Anda ...
Mark Seemann
19
Saya berpikir bahwa fasad harus dikodekan dengan tangan karena mereka mewakili kombinasi komponen yang diketahui (dan mungkin umum). Wadah DI tidak perlu karena semuanya dapat disambungkan dengan tangan (pikirkan DIM Poor Man). Ingatlah bahwa Facade hanyalah kelas kenyamanan opsional untuk pengguna API Anda. Pengguna mahir mungkin masih ingin melewati Facade dan memasang komponen sesuai keinginan mereka. Mereka mungkin ingin menggunakan DI Contaier mereka sendiri untuk ini, jadi saya pikir itu tidak baik untuk memaksa suatu wadah DI tertentu pada mereka jika mereka tidak akan menggunakannya. Kemungkinan tetapi tidak disarankan
Mark Seemann
8
Ini mungkin satu jawaban terbaik yang pernah saya lihat di SO.
Nick Hodges
40

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:

public class Service
{
    public Service()
    {
    }

    public void DoSomething()
    {
        SqlConnection connection = new SqlConnection("some connection string");
        WindowsIdentity identity = WindowsIdentity.GetCurrent();
        // Do something with connection and identity variables
    }
}

Anda menulis seperti ini:

public class Service
{
    public Service(IDbConnection connection, IIdentity identity)
    {
        this.Connection = connection;
        this.Identity = identity;
    }

    public void DoSomething()
    {
        // Do something with Connection and Identity properties
    }

    protected IDbConnection Connection { get; private set; }
    protected IIdentity Identity { get; private set; }
}

Yaitu, Anda melakukan dua hal ketika menulis kode:

  1. Andalkan antarmuka bukan kelas kapan pun Anda berpikir bahwa implementasi mungkin perlu diubah;

  2. 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>mengimplementasikan IList<T>. Jika Anda mendesain kelas Anda untuk menggunakan IList<T>(atau IEnumerable<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 menerima IList<T>argumen konstruktor, seperti BindingList<T>, yang digunakan untuk beberapa fitur pengikatan data.

  • Linq to SQL dan EF dibangun seluruhnya di sekitar IDbConnectiondan 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 INameCreationServiceatau IExtenderProviderService. Anda bahkan tidak benar-benar tahu apa kelas konkret itu . .NET sebenarnya memiliki wadah IoC sendiri IContainer,, yang digunakan untuk ini, dan Componentkelas memiliki GetServicemetode yang merupakan pencari lokasi sebenarnya. Tentu saja, tidak ada yang mencegah Anda menggunakan salah satu atau semua antarmuka ini tanpa IContaineratau 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 IServiceBehavioryang 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. IContaineradalah pilihan yang bagus untuk ini karena ini merupakan bagian dari .NET Framework itu sendiri.

Aaronaught
sumber
2
Abstraksi nyata untuk sebuah wadah adalah IServiceProvider, bukan IContainer.
Mauricio Scheffer
2
@Mauricio: Anda tentu saja benar, tetapi cobalah menjelaskan kepada seseorang yang tidak pernah menggunakan sistem LWC mengapa IContainersebenarnya bukan wadah dalam satu paragraf. ;)
Aaronaught
Aaron, hanya ingin tahu mengapa Anda menggunakan set pribadi; alih-alih hanya menentukan bidang sebagai hanya baca?
jr3
@Jreeter: Maksud Anda mengapa private readonlybidangnya 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 menulis private readonlybidang dan properti dengan getter / setter eksplisit, tapi ... membuang-buang ruang dalam contoh, dan lebih banyak kode untuk dipelihara dalam praktik, tanpa manfaat nyata.
Aaronaught
Terima kasih telah mengklarifikasi! Saya berasumsi bahwa bidang yang hanya bisa dibaca dapat diakses oleh anak.
jr3
5

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 newmeningkatkan 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.

Mauricio Scheffer
sumber
12
Sementara saya menghargai pendapat Anda, saya menemukan bahwa Anda terlalu umum "semua jawaban lain di sini sudah usang dan harus dihindari" sangat naif. Jika kita belajar sesuatu sejak itu, itu injeksi ketergantungan adalah jawaban untuk keterbatasan bahasa. Tanpa mengembangkan atau mengabaikan bahasa kita saat ini, sepertinya kita perlu DI untuk meratakan arsitektur kita dan mencapai modularitas. IoC sebagai konsep umum hanyalah konsekuensi dari tujuan-tujuan ini dan tentunya yang diinginkan. Kontainer IoC sama sekali tidak diperlukan untuk IoC atau DI, dan kegunaannya tergantung pada komposisi modul yang kami buat.
mentega
@tne Anda menyebut saya "sangat naif" adalah ad-hominem yang tidak diterima. Saya telah menulis aplikasi yang kompleks tanpa DI atau IoC di C #, VB.NET, Java (bahasa yang Anda pikir "perlu" IoC tampaknya dilihat dari deskripsi Anda), ini jelas bukan tentang batasan bahasa. Ini tentang batasan konseptual. Saya mengundang Anda untuk belajar tentang pemrograman fungsional daripada menggunakan serangan ad-hominem dan argumen yang buruk.
Mauricio Scheffer
18
Saya sebutkan saya menemukan Anda pernyataan untuk menjadi naif; ini tidak mengatakan apa-apa tentang Anda secara pribadi dan semua tentang pendapat saya. Tolong jangan merasa dihina oleh orang asing acak. Menyiratkan bahwa saya tidak mengetahui semua pendekatan pemrograman fungsional yang relevan juga tidak membantu; Anda tidak tahu itu, dan Anda akan salah. Saya tahu Anda berpengetahuan luas di sana (dengan cepat melihat blog Anda), itulah sebabnya saya merasa terganggu karena seorang pria yang tampaknya berpengetahuan luas akan mengatakan hal-hal seperti "kita tidak perlu IOC". IOC terjadi secara harfiah di mana-mana dalam pemrograman fungsional (..)
mentega
4
dan dalam perangkat lunak tingkat tinggi secara umum. Sebenarnya, bahasa fungsional (dan banyak gaya lainnya) memang membuktikan bahwa mereka menyelesaikan IoC tanpa DI, saya pikir kita berdua setuju dengan itu, dan hanya itu yang ingin saya katakan. Jika Anda berhasil tanpa DI dalam bahasa yang Anda sebutkan, bagaimana Anda melakukannya? Saya berasumsi tidak dengan menggunakan 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.)
mentega
1
Kontainer IoC adalah kamus global yang dapat berubah, menghilangkan parametrikitas sebagai alat untuk alasan, oleh karena itu harus dihindari. IoC (konsep) seperti pada "jangan panggil kami, kami akan menghubungi Anda" tidak berlaku secara teknis setiap kali Anda melewati suatu fungsi sebagai nilai. Alih-alih DI, modelkan domain Anda dengan ADT lalu tulis interpreter (cf Gratis).
Mauricio Scheffer
1

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.

Igor Zevaka
sumber