Seberapa banyak Injeksi Ketergantungan?

38

Saya bekerja dalam sebuah proyek yang menggunakan Injeksi Ketergantungan (Musim Semi) untuk segala sesuatu yang merupakan ketergantungan suatu kelas. Kami berada pada titik di mana file konfigurasi Spring telah berkembang menjadi sekitar 4000 baris. Belum lama ini saya menonton salah satu pembicaraan Paman Bob di YouTube (sayangnya, saya tidak dapat menemukan tautannya) di mana ia merekomendasikan untuk menyuntikkan hanya beberapa dependensi pusat (mis. Pabrik, basis data, ...) ke dalam komponen Utama dari mana mereka kemudian akan didistribusikan.

Keuntungan dari pendekatan ini adalah decoupling kerangka kerja DI dari sebagian besar aplikasi dan juga membuat Spring config cleaner karena pabrik akan mengandung lebih banyak dari apa yang ada di konfigurasi sebelumnya. Sebaliknya, ini akan mengakibatkan penyebaran logika penciptaan di antara banyak kelas pabrik dan pengujian mungkin menjadi lebih sulit.

Jadi pertanyaan saya sebenarnya adalah kelebihan atau kekurangan apa yang Anda lihat dalam pendekatan yang satu atau yang lain. Apakah ada praktik terbaik? Terimakasih banyak untuk jawabanmu!

Antimon
sumber
3
Memiliki file konfigurasi 4000 baris bukan kesalahan injeksi ketergantungan ... ada banyak cara untuk memperbaikinya: membuat modular file menjadi beberapa file yang lebih kecil, beralih ke injeksi berbasis anotasi, gunakan file JavaConfig alih-alih xml. Pada dasarnya, masalah yang Anda alami adalah kegagalan untuk mengelola kompleksitas dan ukuran kebutuhan injeksi ketergantungan aplikasi Anda.
Maybe_Factor
1
@Maybe_Factor TL; DR membagi proyek menjadi komponen yang lebih kecil.
Walfrat

Jawaban:

44

Seperti biasa, Itu Tergantung ™. Jawabannya tergantung pada masalah yang coba dipecahkan. Dalam jawaban ini, saya akan mencoba mengatasi beberapa kekuatan motivasi yang umum:

Mendukung basis kode yang lebih kecil

Jika Anda memiliki 4.000 baris kode konfigurasi Spring, saya kira basis kode memiliki ribuan kelas.

Ini bukan masalah yang bisa Anda atasi setelah fakta, tetapi sebagai aturan praktis, saya cenderung lebih suka aplikasi yang lebih kecil, dengan basis kode yang lebih kecil. Jika Anda menyukai Domain-Driven Design , Anda dapat, misalnya, membuat basis kode per konteks terbatas.

Saya mendasarkan saran ini pada pengalaman saya yang terbatas, karena saya telah menulis kode lini bisnis berbasis web untuk sebagian besar karir saya. Saya bisa membayangkan bahwa jika Anda mengembangkan aplikasi desktop, atau sistem embedded, atau lainnya, hal-hal itu lebih sulit untuk dipisahkan.

Sementara saya menyadari bahwa saran pertama ini mudah dan paling tidak praktis, saya juga percaya itu adalah yang paling penting, dan itulah sebabnya saya memasukkannya. Kompleksitas kode bervariasi secara non-linear (mungkin secara eksponensial) dengan ukuran basis kode.

Favour Pure DI

Sementara saya masih menyadari bahwa pertanyaan ini menyajikan situasi yang ada, saya merekomendasikan Pure DI . Jangan gunakan Wadah DI, tetapi jika Anda melakukannya, setidaknya gunakan untuk menerapkan komposisi berbasis konvensi .

Saya tidak memiliki pengalaman praktis dengan Spring, tapi saya berasumsi bahwa dengan file konfigurasi , file XML tersirat.

Mengkonfigurasi dependensi menggunakan XML adalah yang terburuk dari kedua dunia. Pertama, Anda kehilangan keamanan tipe waktu kompilasi, tetapi Anda tidak mendapatkan apa pun. File konfigurasi XML dapat dengan mudah sebesar kode yang coba diganti.

Dibandingkan dengan masalah yang ingin ditangani, file konfigurasi injeksi dependensi menempati tempat yang salah pada jam kompleksitas konfigurasi .

Kasus untuk injeksi ketergantungan berbutir kasar

Saya bisa membuat kasus injeksi ketergantungan kasar. Saya juga dapat membuat case untuk injeksi dependensi berbutir halus (lihat bagian selanjutnya).

Jika Anda hanya menyuntikkan beberapa dependensi 'sentral', maka sebagian besar kelas mungkin terlihat seperti ini:

public class Foo
{
    private readonly Bar bar;

    public Foo()
    {
        this.bar = new Bar();
    }

    // Members go here...
}

Ini masih cocok Pola Desain 's mendukung komposisi objek atas warisan kelas , karena Foomenyusun Bar. Dari perspektif rawatan, ini masih dapat dianggap dapat dipertahankan, karena jika Anda perlu mengubah komposisi, Anda cukup mengedit kode sumber untuk Foo.

Ini hampir tidak bisa dirawat daripada injeksi ketergantungan. Bahkan, saya akan mengatakan bahwa lebih mudah untuk langsung mengedit kelas yang menggunakan Bar, daripada harus mengikuti tipuan yang melekat dengan injeksi ketergantungan.

Dalam edisi pertama buku saya tentang Ketergantungan Injeksi , saya membuat perbedaan antara dependensi stabil dan volatil.

Ketergantungan yang mudah menguap adalah ketergantungan yang harus Anda pertimbangkan untuk disuntikkan. Mereka termasuk

  • Ketergantungan yang harus dikonfigurasi ulang setelah kompilasi
  • Ketergantungan dikembangkan secara paralel oleh tim lain
  • Ketergantungan dengan perilaku non-deterministik, atau perilaku dengan efek samping

Di lain pihak, dependensi yang stabil adalah dependensi yang berperilaku baik. Dalam arti tertentu, Anda dapat berargumen bahwa perbedaan ini membuat kasus untuk ketergantungan ketergantungan berbutir kasar, meskipun saya harus mengakui bahwa saya tidak sepenuhnya menyadari bahwa ketika saya menulis buku.

Namun, dari perspektif pengujian, ini membuat pengujian unit lebih sulit. Anda tidak dapat lagi menggunakan unit test Fooindependen Bar. Seperti yang dijelaskan JB Rainsberger , tes integrasi mengalami ledakan kompleksitas kombinatorik. Anda benar-benar harus menulis puluhan ribu kasus uji jika Anda ingin mencakup semua jalur melalui integrasi bahkan 4-5 kelas.

Argumen lawannya adalah bahwa seringkali, tugas Anda bukanlah memprogram kelas. Tugas Anda adalah mengembangkan sistem yang memecahkan beberapa masalah khusus. Ini adalah motivasi di balik Pengembangan Perilaku-Didorong (BDD).

Pandangan lain tentang hal ini disampaikan oleh DHH, yang mengklaim bahwa TDD mengarah pada kerusakan desain yang disebabkan oleh tes . Dia juga menyukai pengujian integrasi berbutir kasar.

Jika Anda mengambil perspektif ini tentang pengembangan perangkat lunak, maka injeksi ketergantungan kasar menjadi masuk akal.

Kasus untuk injeksi ketergantungan berbutir halus

Injeksi ketergantungan berbutir halus, di sisi lain, dapat digambarkan sebagai menyuntikkan semua hal!

Perhatian utama saya tentang injeksi ketergantungan kasar adalah kritik yang diungkapkan oleh JB Rainsberger. Anda tidak dapat mencakup semua jalur kode dengan tes integrasi, karena Anda harus menulis ribuan, atau puluhan ribu, kasus uji untuk mencakup semua jalur kode.

Para pendukung BDD akan melawan dengan argumen bahwa Anda tidak perlu menutup semua jalur kode dengan tes. Anda hanya perlu menutup yang menghasilkan nilai bisnis.

Dalam pengalaman saya, bagaimanapun, semua jalur kode 'eksotis' juga akan mengeksekusi dalam penyebaran volume tinggi, dan jika tidak diuji, banyak dari mereka akan memiliki cacat dan menyebabkan pengecualian run-time (sering pengecualian referensi-nol).

Ini telah menyebabkan saya mendukung injeksi ketergantungan berbutir halus, karena memungkinkan saya untuk menguji invarian dari semua objek secara terpisah.

Mendukung pemrograman fungsional

Sementara saya condong ke arah injeksi ketergantungan berbutir halus, saya telah menggeser penekanan saya pada pemrograman fungsional, di antara alasan-alasan lain karena secara intrinsik dapat diuji .

Semakin Anda bergerak menuju kode SOLID, semakin fungsional jadinya . Cepat atau lambat, Anda sebaiknya mengambil risiko. Arsitektur fungsional adalah arsitektur Ports and Adapters , dan injeksi dependensi juga merupakan upaya Port dan Adapters . Perbedaannya, bagaimanapun, adalah bahwa bahasa seperti Haskell memberlakukan arsitektur itu melalui sistem tipenya.

Mendukung pemrograman fungsional yang diketik secara statis

Pada titik ini, saya pada dasarnya sudah menyerah pada pemrograman berorientasi objek (OOP), meskipun banyak masalah OOP secara intrinsik digabungkan ke bahasa mainstream seperti Java dan C # lebih dari konsep itu sendiri.

Masalah dengan bahasa OOP utama adalah bahwa hampir tidak mungkin untuk menghindari masalah ledakan kombinatorik, yang, jika tidak diuji, mengarah ke pengecualian run-time. Bahasa yang diketik secara statis seperti Haskell dan F #, di sisi lain, memungkinkan Anda untuk menyandikan banyak titik keputusan dalam sistem tipe. Ini berarti bahwa alih-alih harus menulis ribuan tes, kompiler hanya akan memberi tahu Anda apakah Anda telah berurusan dengan semua jalur kode yang mungkin (sampai batas tertentu; itu bukan peluru perak).

Juga, injeksi ketergantungan tidak berfungsi . Pemrograman fungsional sejati harus menolak seluruh gagasan dependensi . Hasilnya adalah kode yang lebih sederhana.

Ringkasan

Jika terpaksa bekerja dengan C #, saya lebih suka injeksi dependensi berbutir halus karena memungkinkan saya untuk menutupi seluruh basis kode dengan sejumlah kasus uji yang dapat dikelola.

Pada akhirnya, motivasi saya adalah umpan balik yang cepat. Namun, pengujian unit bukan satu-satunya cara untuk mendapatkan umpan balik .

Mark Seemann
sumber
1
Saya sangat suka jawaban ini, dan terutama pernyataan ini yang digunakan dalam konteks Spring: "Mengkonfigurasi dependensi menggunakan XML adalah yang terburuk dari kedua dunia. Pertama, Anda kehilangan jenis keamanan waktu kompilasi, tetapi Anda tidak mendapatkan apa-apa. Konfigurasi XML file dapat dengan mudah sebesar kode yang coba diganti. "
Thomas Carlisle
@ThomasCarlisle bukan titik konfigurasi XML yang Anda tidak perlu menyentuh kode (bahkan mengkompilasi) untuk mengubahnya? Saya tidak pernah (atau hampir tidak pernah) menggunakannya karena dua hal yang disebutkan Markus, tetapi Anda mendapatkan sesuatu sebagai balasannya.
El Mac
1

Kelas pengaturan DI besar adalah masalah. Tetapi pikirkan kode yang digantikannya.

Anda harus instantiate semua layanan tersebut dan yang lainnya. Kode untuk melakukannya akan berada di app.main()titik awal dan disuntikkan secara manual, atau digabungkan secara ketat seperti this.myService = new MyService();di dalam kelas.

Kurangi ukuran kelas pengaturan Anda dengan membaginya menjadi beberapa kelas pengaturan dan memanggil mereka dari titik awal program. yaitu.

main()
{
   var c = new diContainer();
   var service1 = diSetupClass.SetupService1(c);
   var service2 = diSetupClass.SetupService2(c, service1); //if service1 is required by service2
   //etc

   //main logic
}

Anda tidak perlu merujuk service1 atau 2 kecuali untuk meneruskannya ke metode pengaturan ci lainnya.

Ini lebih baik daripada mencoba untuk mendapatkannya dari wadah karena memberlakukan perintah memanggil pengaturan.

Ewan
sumber
1
Bagi mereka yang ingin melihat konsep ini lebih jauh (membangun pohon kelas Anda pada titik awal program), istilah terbaik untuk mencari adalah "root komposisi"
e_i_pi
@ Ewan civariabel apa itu ?
superjos
kelas pengaturan ci Anda dengan metode statis
Ewan
urgensi harus di. saya terus berpikir 'wadah'
Ewan