Apa cara "benar" untuk menerapkan DI dalam.

22

Saya mencari untuk menerapkan injeksi ketergantungan dalam aplikasi yang relatif besar tetapi tidak memiliki pengalaman di dalamnya. Saya mempelajari konsep dan beberapa implementasi IoC dan injeksi ketergantungan yang tersedia, seperti Unity dan Ninject. Namun, ada satu hal yang membuat saya terhindar. Bagaimana saya harus mengatur pembuatan instance dalam aplikasi saya?

Apa yang saya pikirkan adalah bahwa saya dapat membuat beberapa pabrik tertentu yang akan berisi logika membuat objek untuk beberapa tipe kelas tertentu. Pada dasarnya kelas statis dengan metode yang memanggil metode Ninject Get () dari instance kernel statis di kelas ini.

Apakah ini akan menjadi pendekatan yang tepat untuk menerapkan injeksi ketergantungan dalam aplikasi saya atau haruskah saya menerapkannya sesuai dengan prinsip lain?

pengguna3223738
sumber
5
Saya tidak berpikir bahwa ada yang cara yang benar, tetapi banyak cara yang tepat, tergantung pada proyek Anda. Saya akan mematuhi yang lain dan menyarankan injeksi konstruktor, karena Anda akan dapat memastikan bahwa setiap ketergantungan disuntikkan pada satu titik tunggal. Plus, jika tanda tangan konstruktor tumbuh terlalu lama, Anda akan tahu bahwa kelas terlalu banyak.
Paul Kertscher
Sulit untuk menjawab tanpa mengetahui jenis proyek .net yang Anda bangun. Sebagai contoh, jawaban yang bagus untuk WPF mungkin jawaban yang buruk untuk MVC.
JMK
Sangat menyenangkan untuk mengatur semua registrasi dependensi ke dalam modul DI untuk solusi atau untuk setiap proyek, dan mungkin satu untuk beberapa tes tergantung pada Anda ingin menguji. Oh ya, tentu saja Anda harus menggunakan injeksi konstruktor, hal-hal lain adalah untuk penggunaan yang lebih maju / gila.
Mark Rogers

Jawaban:

30

Jangan berpikir tentang alat yang akan Anda gunakan. Anda dapat melakukan DI tanpa wadah IoC.

Poin pertama: Mark Seemann memiliki buku yang sangat bagus tentang DI di .Net

Kedua: komposisi root. Pastikan bahwa seluruh pengaturan dilakukan pada titik masuk proyek. Sisa kode Anda harus tahu tentang suntikan, bukan tentang alat apa pun yang sedang digunakan.

Ketiga: Injeksi Konstruktor adalah cara yang paling mungkin untuk dilakukan (ada beberapa kasus di mana Anda tidak menginginkannya, tetapi tidak sebanyak itu).

Keempat: melihat ke dalam menggunakan pabrik lambda dan fitur serupa lainnya untuk menghindari membuat antarmuka / kelas yang tidak dibutuhkan untuk tujuan injeksi tunggal.

Miyamoto Akira
sumber
5
Semua saran bagus; terutama bagian pertama: belajar bagaimana melakukan DI murni, dan kemudian mulai melihat wadah IOC yang dapat mengurangi jumlah kode boilerplate yang diperlukan oleh pendekatan itu.
David Arno
6
... atau lewati semua wadah IoC - untuk menjaga semua manfaat verifikasi statis.
Den
Saya suka saran ini adalah titik awal yang baik, sebelum masuk ke DI, dan saya benar-benar memiliki buku Mark Seemann.
Mengintai
Saya dapat jawaban kedua ini. Kami berhasil menggunakan poor-mans-DI (bootstrapper tulisan tangan) dalam aplikasi yang cukup besar dengan merobek bagian-bagian dari logika bootstrapper ke dalam modul yang menyusun aplikasi.
Gendut
1
Hati-hati. Menggunakan suntikan lambda bisa menjadi jalur cepat menuju kegilaan, terutama dari jenis kerusakan desain yang diinduksi oleh tes. Aku tahu. Saya telah menempuh jalan itu.
jpmc26
13

Ada dua bagian untuk pertanyaan Anda - bagaimana menerapkan DI dengan benar, dan bagaimana cara membuat aplikasi besar untuk menggunakan DI.

Bagian pertama dijawab dengan baik oleh @Miyamoto Akira (terutama rekomendasi untuk membaca buku "ketergantungan injeksi dalam .net" karya Mark Seemann. Marks blog juga merupakan sumber gratis yang bagus.

Bagian kedua jauh lebih rumit.

Langkah pertama yang baik adalah dengan memindahkan semua instantiasi ke konstruktor kelas - tidak menyuntikkan dependansi, hanya memastikan Anda hanya memanggil new konstruktor.

Ini akan menyoroti semua pelanggaran SRP yang telah Anda buat, sehingga Anda dapat mulai memecah kelas menjadi kolaborator yang lebih kecil.

Masalah berikutnya yang Anda akan temukan adalah kelas yang bergantung pada parameter runtime untuk konstruksi. Anda biasanya dapat memperbaikinya dengan membuat pabrik sederhana, sering denganFunc<param,type> , menginisialisasi mereka di konstruktor dan memanggil mereka dalam metode.

Langkah selanjutnya adalah membuat antarmuka untuk dependancies Anda, dan menambahkan konstruktor kedua ke kelas Anda yang kecuali antarmuka ini. Konstruktor tanpa parameter Anda akan membuat instance konkret baru dan meneruskannya ke konstruktor baru. Ini biasa disebut 'B * stard Injection' atau 'Poor mans DI'.

Ini akan memberi Anda kemampuan untuk melakukan beberapa pengujian unit, dan jika itu adalah tujuan utama refactor, mungkin Anda berhenti di situ. Kode baru akan ditulis dengan injeksi konstruktor, tetapi kode lama Anda dapat terus berfungsi seperti yang tertulis tetapi masih dapat diuji.

Anda tentu saja bisa melangkah lebih jauh. Jika Anda bermaksud menggunakan wadah IOC, maka langkah selanjutnya adalah mengganti semua panggilan langsungnew dalam konstruktor tanpa parameter Anda dengan panggilan statis ke wadah IOC, pada dasarnya (ab) menggunakannya sebagai pencari lokasi.

Ini akan memunculkan lebih banyak kasus parameter konstruktor runtime untuk ditangani seperti sebelumnya.

Setelah ini selesai, Anda dapat mulai menghapus konstruktor tanpa parameter, dan refactor menjadi DI murni.

Pada akhirnya ini akan menjadi banyak pekerjaan, jadi pastikan Anda memutuskan mengapa Anda ingin melakukannya, dan memprioritaskan bagian basis kode yang akan mendapat manfaat paling besar dari refactor

Steve
sumber
3
Terima kasih atas jawaban yang sangat terperinci. Anda memberi saya beberapa ide tentang cara mendekati masalah yang saya hadapi. Seluruh arsitektur aplikasi sudah dibangun dengan mempertimbangkan IoC. Alasan utama saya ingin menggunakan DI bahkan bukan pengujian unit, itu datang sebagai bonus tetapi lebih kepada kemampuan untuk menukar implementasi yang berbeda untuk antarmuka yang berbeda, yang didefinisikan dalam inti aplikasi saya, dengan upaya sesedikit mungkin. Aplikasi yang dimaksud berfungsi di lingkungan yang terus berubah dan saya sering harus menukar bagian aplikasi dengan menggunakan implementasi baru sesuai dengan perubahan di lingkungan.
user3223738
1
Senang saya bisa membantu, dan ya saya setuju bahwa keuntungan terbesar DI adalah kopling longgar dan kemampuan untuk mengkonfigurasi ulang dengan mudah yang dibawanya, dengan pengujian unit menjadi efek samping yang bagus.
Steve
1

Pertama saya ingin menyebutkan bahwa Anda membuat ini secara signifikan lebih sulit pada diri Anda sendiri dengan refactoring proyek yang sudah ada daripada memulai proyek baru.

Anda bilang itu aplikasi besar, jadi pilih komponen kecil untuk memulainya. Lebih disukai komponen 'leaf-node' yang tidak digunakan oleh yang lain. Saya tidak tahu keadaan pengujian otomatis pada aplikasi ini, tetapi Anda akan melanggar semua tes unit untuk komponen ini. Jadi bersiaplah untuk itu. Langkah 0 adalah menulis tes integrasi untuk komponen yang akan Anda modifikasi jika belum ada. Sebagai upaya terakhir (tidak ada infrastruktur pengujian; tidak ada persetujuan untuk menulisnya), cari tahu serangkaian tes manual yang dapat Anda lakukan yang memverifikasi komponen ini berfungsi.

Cara paling sederhana untuk menyatakan tujuan Anda untuk refactor DI adalah Anda ingin menghapus semua instance dari operator 'baru' dari komponen ini. Ini umumnya terbagi dalam dua kategori:

  1. Variabel anggota invarian: Ini adalah variabel yang ditetapkan satu kali (biasanya dalam konstruktor) dan tidak dipindahkan untuk masa hidup objek. Untuk ini, Anda dapat menyuntikkan instance objek ke dalam konstruktor. Anda umumnya tidak bertanggung jawab untuk membuang benda-benda ini (saya tidak ingin mengatakan tidak pernah di sini, tetapi Anda benar-benar tidak harus memiliki tanggung jawab itu).

  2. Variant variabel anggota / variabel metode: Ini adalah variabel yang akan mengumpulkan sampah di beberapa titik selama masa hidup objek. Untuk ini, Anda akan ingin menyuntikkan pabrik ke kelas Anda untuk memberikan contoh ini. Anda bertanggung jawab untuk membuang benda yang dibuat oleh pabrik.

Kontainer IoC Anda (ninject sepertinya) akan bertanggung jawab untuk membuat instance objek-objek tersebut dan mengimplementasikan antarmuka pabrik Anda. Apa pun yang menggunakan komponen yang telah Anda modifikasi perlu tahu tentang wadah IoC sehingga dapat mengambil komponen Anda.

Setelah Anda menyelesaikan hal di atas, Anda akan dapat memperoleh manfaat apa pun yang Anda harapkan dari DI dalam komponen yang Anda pilih. Sekarang akan menjadi saat yang tepat untuk menambah / memperbaiki tes unit tersebut. Jika ada unit test yang ada, Anda harus membuat keputusan tentang apakah Anda ingin menambalnya bersama dengan menyuntikkan benda nyata atau menulis tes unit baru menggunakan tiruan.

'Cukup' ulangi di atas untuk setiap komponen aplikasi Anda, pindahkan referensi ke wadah IoC saat Anda pergi sampai hanya kebutuhan utama yang perlu mengetahuinya.

Jon
sumber
1
Saran yang bagus: mulailah dengan komponen kecil daripada 'BIG re-write'
Daniel Hollinrake
0

Pendekatan yang benar adalah dengan menggunakan injeksi konstruktor, jika Anda menggunakannya

Apa yang saya pikirkan adalah bahwa saya dapat membuat beberapa pabrik tertentu yang akan berisi logika membuat objek untuk beberapa tipe kelas tertentu. Pada dasarnya kelas statis dengan metode yang memanggil metode Ninject Get () dari instance kernel statis di kelas ini.

maka Anda berakhir dengan pencari lokasi layanan, daripada injeksi ketergantungan.

Pelican Terbang Rendah
sumber
Yakin. Injeksi konstruktor. Katakanlah saya memiliki kelas yang menerima implementasi antarmuka sebagai salah satu argumen. Tapi saya masih perlu membuat contoh implementasi antarmuka dan meneruskannya ke konstruktor ini di suatu tempat. Lebih disukai itu harus sepotong kode terpusat.
user3223738
2
Tidak, ketika Anda menginisialisasi wadah DI, Anda harus menentukan implementasi antarmuka, dan wadah DI akan membuat instance dan menyuntikkan ke konstruktor.
Pelican Terbang Rendah
Secara pribadi, saya menemukan injeksi konstruktor terlalu sering digunakan. Saya sudah terlalu sering melihatnya sehingga 10 layanan berbeda disuntikkan dan hanya satu yang benar-benar diperlukan untuk pemanggilan fungsi - mengapa bukan bagian dari argumen fungsi itu?
urbanhusky
2
Jika 10 layanan berbeda disuntikkan, itu karena seseorang melanggar SRP, yang harus dipecah menjadi komponen yang lebih kecil.
Pelican Terbang Rendah
1
@ Fabio Pertanyaannya adalah apa yang akan membeli Anda. Saya belum melihat contoh di mana memiliki kelas raksasa yang berurusan dengan selusin hal yang sama sekali berbeda adalah desain yang bagus. Satu-satunya hal yang DI lakukan adalah membuat semua pelanggaran itu menjadi lebih jelas.
Voo
0

Anda mengatakan ingin menggunakannya tetapi jangan nyatakan mengapa.

DI tidak lebih dari menyediakan mekanisme untuk menghasilkan konkret dari antarmuka.

Ini sendiri berasal dari DIP . Jika kode Anda sudah ditulis dalam gaya ini dan Anda memiliki satu tempat di mana konkresi dihasilkan, DI tidak membawa apa-apa lagi ke pesta. Menambahkan kode kerangka DI di sini hanya akan mengasapi dan mengaburkan basis kode Anda.

Dengan asumsi Anda memang ingin menggunakannya, Anda biasanya mengatur pabrik / pembangun / wadah (atau apa pun) di awal aplikasi sehingga terlihat jelas.

NB sangat mudah untuk roll sendiri jika Anda inginkan daripada komit ke Ninject / StructureMap atau apa pun. Namun, jika Anda memiliki pergantian staf yang masuk akal, hal itu dapat membuat roda untuk menggunakan kerangka kerja yang diakui atau setidaknya menulisnya dengan gaya itu sehingga tidak terlalu banyak kurva belajar.

Robbie Dee
sumber
0

Sebenarnya, cara yang "benar" adalah untuk TIDAK menggunakan pabrik sama sekali kecuali sama sekali tidak ada pilihan lain (seperti dalam pengujian unit dan mengejek tertentu - untuk kode produksi Anda TIDAK menggunakan pabrik)! Melakukannya sebenarnya adalah anti-pola dan harus dihindari dengan cara apa pun. Inti dari wadah DI adalah untuk memungkinkan gadget melakukan pekerjaan untuk Anda.

Seperti yang dinyatakan di atas dalam posting sebelumnya, Anda ingin gadget IoC Anda memikul tanggung jawab untuk pembuatan berbagai objek dependen di aplikasi Anda. Itu berarti membiarkan gadget DI Anda membuat dan mengelola berbagai instance itu sendiri. Ini adalah seluruh titik di belakang DI - objek Anda harus TIDAK PERNAH tahu cara membuat dan / atau mengelola objek yang mereka andalkan. Untuk melakukan sebaliknya istirahat longgar kopling.

Mengonversi aplikasi yang sudah ada ke semua DI adalah langkah besar, tetapi mengesampingkan kesulitan yang jelas dalam melakukannya, Anda juga ingin (hanya untuk membuat hidup Anda sedikit lebih mudah) untuk menjelajahi alat DI yang akan melakukan sebagian besar binding Anda secara otomatis (inti dari sesuatu seperti Ninject adalah "kernel.Bind<someInterface>().To<someConcreteClass>()"panggilan yang Anda buat untuk mencocokkan deklarasi antarmuka Anda dengan kelas-kelas konkret yang ingin Anda gunakan untuk mengimplementasikan antarmuka tersebut. Panggilan "Bind" itulah yang memungkinkan gadget DI Anda untuk mencegat panggilan konstruktor dan menyediakan instances objek dependen yang diperlukan.Sebuah konstruktor khas (kode semu yang ditampilkan di sini) untuk beberapa kelas mungkin:

public class SomeClass
{
  private ISomeClassA _ClassA;
  private ISomeOtherClassB _ClassB;

  public SomeClass(ISomeClassA aInstanceOfA, ISomeOtherClassB aInstanceOfB)
  {
    if (aInstanceOfA == null)
      throw new NullArgumentException();
    if (aInstanceOfB == null)
      throw new NullArgumentException();
    _ClassA = aInstanceOfA;
    _ClassB = aInstanceOfB;
  }

  public void DoSomething()
  {
    _ClassA.PerformSomeAction();
    _ClassB.PerformSomeOtherActionUsingTheInstanceOfClassA(_ClassA);
  }
}

Perhatikan bahwa tidak ada tempat di dalam kode itu adalah kode apa pun yang membuat / mengelola / merilis baik instance SomeConcreteClassA atau SomeOtherConcreteClassB. Faktanya, tidak ada kelas konkret yang direferensikan. Jadi ... di mana keajaiban itu terjadi?

Di bagian permulaan aplikasi Anda, hal-hal berikut terjadi (sekali lagi, ini adalah kode semu tetapi cukup dekat dengan hal (Ninject) yang sebenarnya ...):

public void StartUp()
{
  kernel.Bind<ISomeClassA>().To<SomeConcreteClassA>();
  kernel.Bind<ISomeOtherClassB>().To<SomeOtherConcreteClassB>();
}

Sedikit kode di sana memberitahu Ninject gadget untuk mencari konstruktor, memindai mereka, mencari contoh antarmuka yang telah dikonfigurasikan untuk menangani (itulah panggilan "Bind") dan kemudian membuat dan mengganti instance kelas beton di mana pun instance dirujuk.

Ada alat bagus yang melengkapi Ninject dengan sangat baik yang disebut Ninject.Extensions.Conventions (paket NuGet lain) yang akan melakukan sebagian besar pekerjaan ini untuk Anda. Bukan untuk mengambil dari pengalaman belajar yang luar biasa yang akan Anda lalui saat Anda membangun ini sendiri, tetapi untuk memulai, ini mungkin merupakan alat untuk menyelidiki.

Jika ingatanku, Unity (secara resmi dari Microsoft sekarang merupakan proyek Open Source) memiliki satu atau dua pemanggilan metode yang melakukan hal yang sama, alat lain memiliki pembantu yang serupa.

Apa pun jalur yang Anda pilih, bacalah buku Mark Seemann untuk sebagian besar pelatihan DI Anda, namun, harus ditunjukkan bahwa bahkan "Yang Luar Biasa" dari dunia rekayasa perangkat lunak (seperti Mark) dapat membuat kesalahan mencolok - Mark lupa semua tentang Ninject dalam bukunya jadi di sini adalah sumber daya lain yang ditulis hanya untuk Ninject. Saya memilikinya dan ini adalah bacaan yang bagus: Menguasai Ninject untuk Dependency Injection

Fred
sumber
0

Tidak ada "jalan yang benar", tetapi ada beberapa prinsip sederhana untuk diikuti:

  • Buat root komposisi pada startup aplikasi
  • Setelah root komposisi dibuat, buang referensi ke wadah DI / kernel (atau setidaknya enkapsulasi sehingga tidak dapat diakses langsung dari aplikasi Anda)
  • Jangan membuat instance melalui "baru"
  • Berikan semua dependensi yang diperlukan sebagai abstraksi ke konstruktor

Itu saja. Yang pasti, itu adalah prinsip bukan hukum, tetapi jika Anda mengikutinya Anda dapat yakin bahwa Anda melakukan DI (perbaiki saya jika saya salah).


Jadi, bagaimana cara membuat objek selama runtime tanpa "baru" dan tanpa mengetahui wadah DI?

Dalam hal NInject, ada ekstensi pabrik yang menyediakan pembuatan pabrik. Tentu, pabrik yang dibuat masih memiliki referensi internal ke kernel, tetapi itu tidak dapat diakses dari aplikasi Anda.

JanDotNet
sumber