Ketergantungan injeksi (DI) adalah pola yang terkenal dan modis. Sebagian besar insinyur mengetahui kelebihannya, seperti:
- Membuat isolasi dalam pengujian unit mungkin / mudah
- Mendefinisikan dependensi kelas secara eksplisit
- Memfasilitasi desain yang baik ( prinsip tanggung jawab tunggal (SRP) misalnya)
- Mengaktifkan implementasi switching dengan cepat (
DbLogger
alih-alihConsoleLogger
misalnya)
Saya rasa ada konsensus luas industri bahwa DI adalah pola yang baik dan bermanfaat. Tidak terlalu banyak kritik saat ini. Kerugian yang disebutkan dalam komunitas biasanya kecil. Beberapa dari mereka:
- Peningkatan jumlah kelas
- Pembuatan antarmuka yang tidak perlu
Saat ini kami membahas desain arsitektur dengan kolega saya. Dia cukup konservatif, tetapi berpikiran terbuka. Dia suka mempertanyakan hal-hal, yang saya anggap baik, karena banyak orang di TI hanya menyalin tren terbaru, mengulangi kelebihannya dan secara umum tidak berpikir terlalu banyak - jangan menganalisis terlalu dalam.
Hal-hal yang ingin saya tanyakan adalah:
- Haruskah kita menggunakan injeksi ketergantungan ketika kita hanya memiliki satu implementasi?
- Haruskah kita melarang membuat objek baru kecuali bahasa / kerangka kerja?
- Apakah menyuntikkan satu ide buruk implementasi (katakanlah kita hanya memiliki satu implementasi sehingga kita tidak ingin membuat antarmuka "kosong") jika kita tidak berencana untuk menguji unit kelas tertentu?
UserService
kelas itu hanya merupakan tempat untuk logika. Itu akan menyuntikkan koneksi database dan tes berjalan di dalam transaksi yang dibatalkan. Banyak yang menyebut praktik buruk ini, tetapi saya menemukan bahwa ini bekerja dengan sangat baik. Tidak perlu memutarbalikkan kode Anda hanya untuk pengujian dan Anda mendapatkan kekuatan pencarian bug dari tes integrasi.Jawaban:
Pertama, saya ingin memisahkan pendekatan desain dari konsep kerangka kerja. Injeksi ketergantungan pada level paling sederhana dan paling mendasar hanyalah:
Itu dia. Perhatikan, bahwa tidak ada yang memerlukan antarmuka, kerangka kerja, gaya injeksi apa pun, dll. Agar adil, saya pertama kali mempelajari tentang pola ini 20 tahun yang lalu. Itu bukan hal baru.
Karena lebih dari 2 orang memiliki kebingungan atas istilah orang tua dan anak, dalam konteks injeksi ketergantungan:
Ketergantungan injeksi adalah pola untuk komposisi objek .
Mengapa antarmuka?
Antarmuka adalah kontrak. Mereka ada untuk membatasi seberapa erat dua objek dapat digabungkan. Tidak setiap ketergantungan memerlukan antarmuka, tetapi mereka membantu dengan menulis kode modular.
Saat Anda menambahkan dalam konsep pengujian unit, Anda mungkin memiliki dua implementasi konseptual untuk antarmuka tertentu: objek nyata yang ingin Anda gunakan dalam aplikasi Anda, dan objek tiruan atau stubbed yang Anda gunakan untuk menguji kode yang bergantung pada objek. Itu saja bisa menjadi pembenaran yang cukup untuk antarmuka.
Mengapa kerangka kerja
Inisialisasi menginisialisasi dan menyediakan dependensi ke objek anak dapat menakutkan ketika ada banyak dari mereka. Kerangka memberikan manfaat berikut:
Mereka juga memiliki kelemahan berikut:
Semua yang mengatakan, ada trade-off. Untuk proyek kecil di mana tidak ada banyak bagian yang bergerak, dan ada sedikit alasan untuk menggunakan kerangka kerja DI. Namun, untuk proyek yang lebih rumit di mana ada komponen tertentu yang sudah dibuat untuk Anda, kerangka kerjanya dapat dibenarkan.
Bagaimana dengan [artikel acak di Internet]?
Bagaimana dengan itu? Sering kali orang bisa terlalu bersemangat dan menambahkan banyak batasan dan mencaci maki Anda jika Anda tidak melakukan hal-hal dengan "satu cara yang benar". Tidak ada satu cara yang benar. Lihat apakah Anda dapat mengekstraksi sesuatu yang bermanfaat dari artikel dan mengabaikan hal-hal yang tidak Anda setujui.
Singkatnya, pikirkan sendiri dan cobalah.
Bekerja dengan "kepala lama"
Belajar sebanyak mungkin. Apa yang akan Anda temukan dengan banyak pengembang yang memasuki usia 70-an adalah bahwa mereka telah belajar untuk tidak bersikap dogmatis tentang banyak hal. Mereka memiliki metode yang telah mereka gunakan selama beberapa dekade yang menghasilkan hasil yang benar.
Saya memiliki hak istimewa untuk bekerja dengan beberapa di antaranya, dan mereka dapat memberikan umpan balik yang sangat jujur dan masuk akal. Dan di mana mereka melihat nilai, mereka menambahkan alat-alat itu ke daftar lagu mereka.
sumber
Injeksi ketergantungan, seperti kebanyakan pola, merupakan solusi untuk masalah . Jadi mulailah dengan menanyakan apakah Anda bahkan memiliki masalah sejak awal. Jika tidak, maka menggunakan pola yang paling mungkin akan membuat kode lebih buruk .
Pertimbangkan terlebih dahulu jika Anda dapat mengurangi atau menghilangkan ketergantungan. Semua hal lain dianggap sama, kami ingin setiap komponen dalam sistem memiliki dependensi sesedikit mungkin. Dan jika ketergantungannya hilang, pertanyaan tentang menyuntikkan atau tidak menjadi diperdebatkan!
Pertimbangkan sebuah modul yang mengunduh beberapa data dari layanan eksternal, mem-parsingnya, dan melakukan beberapa analisis kompleks, dan menulis untuk menghasilkan sebuah file.
Sekarang, jika ketergantungan pada layanan eksternal hardcoded, maka akan sangat sulit untuk menguji unit pemrosesan internal modul ini. Jadi Anda mungkin memutuskan untuk menyuntikkan layanan eksternal dan sistem file sebagai dependensi antarmuka, yang akan memungkinkan Anda untuk menyuntikkan ejekan sebagai gantinya yang pada gilirannya membuat unit-pengujian logika internal menjadi mungkin.
Tetapi solusi yang jauh lebih baik adalah dengan memisahkan analisis dari input / output. Jika analisis diekstraksi ke modul tanpa efek samping, akan lebih mudah untuk diuji. Perhatikan bahwa mengejek adalah bau kode - tidak selalu dapat dihindari, tetapi secara umum, lebih baik jika Anda dapat menguji tanpa mengandalkan mengejek. Jadi dengan menghilangkan dependensi, Anda menghindari masalah yang seharusnya diringankan DI. Perhatikan bahwa desain seperti itu juga mematuhi SRP jauh lebih baik.
Saya ingin menekankan bahwa DI tidak selalu memfasilitasi SRP atau prinsip desain bagus lainnya seperti pemisahan masalah, kohesi tinggi / kopling rendah dan sebagainya. Mungkin juga memiliki efek sebaliknya. Pertimbangkan kelas A yang menggunakan kelas B lain secara internal. B hanya digunakan oleh A dan karena itu sepenuhnya dienkapsulasi dan dapat dianggap sebagai detail implementasi. Jika Anda mengubah ini untuk menyuntikkan B ke konstruktor A, maka Anda telah mengekspos detail implementasi ini dan sekarang pengetahuan tentang ketergantungan ini dan tentang bagaimana menginisialisasi B, masa pakai B dan seterusnya harus ada beberapa tempat lain dalam sistem secara terpisah. dari A. Jadi Anda memiliki keseluruhan arsitektur yang lebih buruk dengan masalah yang bocor.
Di sisi lain ada beberapa kasus di mana DI benar-benar bermanfaat. Misalnya untuk layanan global dengan efek samping seperti logger.
Masalahnya adalah ketika pola dan arsitektur menjadi tujuan dalam diri mereka sendiri daripada alat. Hanya bertanya, "Haruskah kita menggunakan DI?" adalah jenis menempatkan kereta di depan kuda. Anda harus bertanya: "Apakah kita punya masalah?" dan "Apa solusi terbaik untuk masalah ini?"
Sebagian dari pertanyaan Anda adalah: "Haruskah kita membuat antarmuka yang berlebihan untuk memenuhi tuntutan pola?" Anda mungkin sudah menyadari jawaban untuk ini - sama sekali tidak ! Siapa pun yang memberi tahu Anda sebaliknya mencoba menjual sesuatu kepada Anda - kemungkinan besar jam konsultasi mahal. Antarmuka hanya memiliki nilai jika mewakili abstraksi. Antarmuka yang hanya meniru permukaan kelas tunggal disebut "antarmuka header" dan ini adalah antipattern yang dikenal.
sumber
A
kegunaanB
dalam produksi, tetapi hanya diuji terhadapMockB
, pengujian kami tidak memberi tahu kami apakah itu akan berfungsi dalam produksi. Ketika komponen murni (tanpa efek samping) dari model domain menyuntikkan dan mengejek satu sama lain, hasilnya adalah pemborosan besar waktu semua orang, basis kode yang membengkak dan rapuh, dan kepercayaan yang rendah pada sistem yang dihasilkan. Mengejek batas sistem, bukan di antara potongan acak dari sistem yang sama.Dalam pengalaman saya, ada sejumlah kelemahan injeksi ketergantungan.
Pertama, menggunakan DI tidak menyederhanakan pengujian otomatis sebanyak yang diiklankan. Unit menguji sebuah kelas dengan implementasi tiruan dari sebuah antarmuka memungkinkan Anda memvalidasi bagaimana kelas itu akan berinteraksi dengan antarmuka. Yaitu, ini memungkinkan Anda menguji unit bagaimana kelas yang diuji menggunakan kontrak yang disediakan oleh antarmuka. Namun, ini memberikan jaminan yang jauh lebih besar bahwa input dari kelas yang diuji ke antarmuka adalah seperti yang diharapkan. Ini memberikan jaminan yang agak buruk bahwa kelas yang diuji merespons seperti yang diharapkan untuk keluar dari antarmuka karena hampir secara universal mengejek keluaran, yang sendiri tunduk pada bug, penyederhanaan yang berlebihan, dan sebagainya. Singkatnya, itu TIDAK membiarkan Anda memvalidasi bahwa kelas akan berperilaku seperti yang diharapkan dengan implementasi nyata dari antarmuka.
Kedua, DI membuatnya lebih sulit untuk dinavigasi melalui kode. Ketika mencoba menavigasi ke definisi kelas yang digunakan sebagai input ke fungsi, antarmuka dapat berupa apa saja mulai dari gangguan kecil (mis. Di mana ada satu implementasi) hingga tenggang waktu utama (mis. Ketika antarmuka yang terlalu umum seperti IDisposable digunakan) ketika mencoba menemukan implementasi aktual yang digunakan. Ini dapat mengubah latihan sederhana seperti "Saya perlu memperbaiki pengecualian referensi nol dalam kode yang terjadi tepat setelah pernyataan logging ini dicetak" menjadi upaya sehari penuh.
Ketiga, penggunaan DI dan framework adalah pedang bermata dua. Ini dapat sangat mengurangi jumlah kode pelat-ketel yang dibutuhkan untuk operasi umum. Namun, hal ini dilakukan dengan mengorbankan kebutuhan pengetahuan terperinci tentang kerangka kerja DI tertentu untuk memahami bagaimana operasi umum ini sebenarnya disatukan. Memahami bagaimana dependensi dimuat ke dalam kerangka kerja dan menambahkan dependensi baru ke dalam kerangka kerja untuk menyuntikkan dapat memerlukan membaca cukup banyak bahan latar belakang dan mengikuti beberapa tutorial dasar tentang kerangka kerja. Ini dapat mengubah beberapa tugas sederhana menjadi yang agak memakan waktu.
sumber
Saya mengikuti saran Mark Seemann dari "Dependency injection in .NET" - untuk menduga.
DI harus digunakan ketika Anda memiliki 'ketergantungan yang tidak stabil', misalnya ada peluang yang masuk akal untuk berubah.
Jadi jika Anda berpikir Anda mungkin memiliki lebih dari satu implementasi di masa depan atau implementasinya mungkin berubah, gunakan DI. Kalau tidak,
new
tidak apa-apa.sumber
Hewan kesayangan saya yang terbesar tentang DI sudah disebutkan dalam beberapa jawaban secara sepintas, tetapi saya akan sedikit mengembangkannya di sini. DI (seperti yang kebanyakan dilakukan hari ini, dengan wadah dll) benar-benar, benar-benar menyakitkan keterbacaan kode. Dan keterbacaan kode bisa dibilang alasan di balik sebagian besar inovasi pemrograman saat ini. Seperti kata seseorang - menulis kode itu mudah. Membaca kode itu sulit. Tetapi ini juga sangat penting, kecuali jika Anda menulis semacam utilitas kecil sekali pakai sekali pakai.
Masalah dengan DI dalam hal ini adalah bahwa itu buram. Wadahnya adalah kotak hitam. Objek hanya muncul dari suatu tempat dan Anda tidak tahu - siapa yang membangunnya dan kapan? Apa yang diteruskan ke konstruktor? Dengan siapa saya berbagi contoh ini? Siapa tahu...
Dan ketika Anda bekerja terutama dengan antarmuka, semua fitur "pergi ke definisi" dari IDE Anda terbakar. Sangat sulit untuk melacak aliran program tanpa menjalankannya dan hanya melangkah untuk melihat hanya implementasi WHICH dari antarmuka yang digunakan di tempat khusus ini. Dan kadang-kadang ada beberapa kendala teknis yang mencegah Anda melangkah. Dan bahkan jika Anda bisa, jika melibatkan melalui usus memutar dari wadah DI, seluruh urusan dengan cepat menjadi latihan frustrasi.
Untuk bekerja secara efisien dengan sepotong kode yang menggunakan DI, Anda harus terbiasa dengannya dan sudah tahu apa yang terjadi.
sumber
Meskipun DI secara umum jelas merupakan hal yang baik, saya sarankan untuk tidak menggunakannya secara membabi buta untuk semuanya. Misalnya, saya tidak pernah menyuntikkan penebang. Salah satu keunggulan DI adalah membuat dependensi eksplisit dan jelas. Tidak ada gunanya mendaftar
ILogger
sebagai ketergantungan dari hampir setiap kelas - hanya berantakan. Adalah tanggung jawab pencatat untuk memberikan fleksibilitas yang Anda butuhkan. Semua penebang saya adalah anggota final statis, saya dapat mempertimbangkan menyuntikkan penebang ketika saya membutuhkan yang tidak statis.Ini adalah kerugian dari kerangka DI yang diberikan atau kerangka kerja yang mengejek, bukan dari DI itu sendiri. Di sebagian besar tempat, kelas saya bergantung pada kelas beton, yang berarti tidak diperlukan pelat ketel. Guice (framework Java DI) secara default mengikat sebuah kelas untuk dirinya sendiri dan saya hanya perlu mengesampingkan pengikatan dalam tes (atau mentransfernya secara manual sebagai gantinya).
Saya hanya membuat antarmuka saat dibutuhkan (yang agak jarang). Ini berarti bahwa kadang-kadang, saya harus mengganti semua kemunculan kelas dengan sebuah antarmuka, tetapi IDE dapat melakukan ini untuk saya.
Ya, tapi hindari boilerplate yang ditambahkan .
Tidak. Akan ada banyak kelas nilai (tidak berubah) dan data (bisa berubah), di mana instance baru saja dibuat dan diedarkan dan di mana tidak ada gunanya menyuntikkan mereka - karena mereka tidak pernah disimpan di objek lain (atau hanya di lainnya benda seperti itu).
Untuk mereka, Anda mungkin perlu menyuntikkan pabrik, tetapi sebagian besar waktu itu tidak masuk akal (bayangkan misalnya,
@Value class NamedUrl {private final String name; private final URL url;}
Anda benar-benar tidak memerlukan pabrik di sini dan tidak ada yang disuntikkan).IMHO tidak apa-apa asalkan tidak menyebabkan kode mengasapi. Jangan menyuntikkan dependensi tetapi jangan membuat antarmuka (dan tidak ada XML konfigurasi gila!) Karena Anda dapat melakukannya nanti tanpa kerumitan.
Sebenarnya, dalam proyek saya saat ini, ada empat kelas (dari ratusan), yang saya putuskan untuk dikecualikan dari DI karena mereka kelas sederhana yang digunakan di terlalu banyak tempat, termasuk objek data.
Kerugian lain dari kebanyakan kerangka kerja DI adalah overhead runtime. Ini dapat dipindahkan ke waktu kompilasi (untuk Java, ada Belati , tidak tahu tentang bahasa lain).
Namun kelemahan lain adalah keajaiban terjadi di mana-mana, yang dapat disetel ke bawah (misalnya, saya menonaktifkan pembuatan proxy saat menggunakan Guice).
sumber
Saya harus mengatakan bahwa menurut pendapat saya, seluruh konsep Dependency Injection terlalu dibesar-besarkan.
DI adalah hari modern yang setara dengan nilai-nilai global. Hal-hal yang Anda suntikkan adalah lajang global dan objek kode murni, jika tidak, Anda tidak dapat menyuntikkannya. Sebagian besar penggunaan DI dipaksakan pada Anda untuk menggunakan perpustakaan yang diberikan (JPA, Data Musim Semi, dll). Untuk sebagian besar, DI menyediakan lingkungan yang sempurna untuk memelihara dan mengolah spageti.
Dalam semua kejujuran, cara termudah untuk menguji kelas adalah untuk memastikan bahwa semua dependensi dibuat dalam metode yang dapat diganti. Kemudian buat kelas Uji yang berasal dari kelas aktual dan timpa metode itu.
Kemudian Anda instantiate kelas Test dan menguji semua metodenya. Ini tidak akan jelas bagi sebagian dari Anda - metode yang Anda uji adalah yang dimiliki kelas yang sedang diuji. Dan semua tes metode ini terjadi dalam file kelas tunggal - kelas pengujian unit yang terkait dengan kelas yang diuji. Tidak ada overhead di sini - ini adalah cara pengujian unit bekerja.
Dalam kode, konsep ini terlihat seperti ini ...
Hasilnya persis sama dengan menggunakan DI - yaitu, ClassUnderTest dikonfigurasikan untuk pengujian.
Satu-satunya perbedaan adalah bahwa kode ini benar-benar ringkas, benar-benar dienkapsulasi, lebih mudah dikodekan, lebih mudah dipahami, lebih cepat, menggunakan lebih sedikit memori, tidak memerlukan konfigurasi alternatif, tidak memerlukan kerangka kerja apa pun, tidak akan pernah menjadi penyebab halaman 4 (WTF!) Tumpukan jejak yang mencakup persis ZERO (0) kelas yang Anda tulis, dan benar-benar jelas bagi siapa pun yang memiliki pengetahuan OO sekecil apa pun, dari pemula hingga Guru (Anda akan berpikir, tetapi akan keliru).
Yang sedang berkata, tentu saja kita tidak bisa menggunakannya - itu terlalu jelas dan tidak cukup trendi.
Namun, pada akhirnya, kekhawatiran terbesar saya dengan DI adalah bahwa dari proyek-proyek yang saya lihat gagal total, semuanya adalah basis kode besar di mana DI adalah lem yang menyatukan semuanya. DI bukan arsitektur - itu benar-benar hanya relevan dalam beberapa situasi, yang sebagian besar dipaksakan pada Anda untuk menggunakan perpustakaan lain (JPA, Data Musim Semi, dll). Sebagian besar, dalam basis kode yang dirancang dengan baik, sebagian besar penggunaan DI akan terjadi pada tingkat di bawah di mana kegiatan pengembangan harian Anda berlangsung.
sumber
bar
hanya dapat dipanggil setelahfoo
, maka itu adalah API yang buruk. Ia mengklaim menyediakan fungsionalitas (bar
), tetapi kami tidak dapat menggunakannya (karenafoo
mungkin tidak dipanggil). Jika Anda ingin tetap denganinitializeDependencies
pola (anti?) Anda, Anda setidaknya harus membuatnya pribadi / dilindungi dan menyebutnya secara otomatis dari konstruktor, sehingga API tulus.Pertanyaan Anda sebenarnya adalah "Apakah pengujian unit buruk?"
99% dari kelas alternatif Anda untuk disuntikkan akan menjadi tiruan yang memungkinkan pengujian unit.
Jika Anda melakukan pengujian unit tanpa DI, Anda memiliki masalah tentang cara mendapatkan kelas untuk menggunakan data mocked atau layanan mocked. Katakanlah 'bagian dari logika' karena Anda mungkin tidak memisahkannya menjadi layanan.
Ada beberapa cara alternatif untuk melakukan ini, tetapi DI adalah yang bagus dan fleksibel. Setelah Anda menggunakannya untuk pengujian, Anda sebenarnya terpaksa menggunakannya di mana-mana, karena Anda memerlukan beberapa kode, bahkan itu disebut 'poor man's DI' yang membuat tipe konkret.
Sulit membayangkan kerugian yang begitu buruk sehingga keuntungan dari pengujian unit kewalahan.
sumber
new
di dalam metode, kita tahu bahwa kode yang sama berjalan dalam produksi berjalan selama pengujian.Order
contoh saya adalah unit test: ini menguji metode (total
metodeOrder
). Anda mengeluh karena memanggil kode dari 2 kelas, respons saya begitu apa ? Kami tidak menguji "2 kelas sekaligus", kami sedang mengujitotal
metode. Kita seharusnya tidak peduli bagaimana metode melakukan tugasnya: itu adalah detail implementasi; mengujinya menyebabkan kerapuhan, kopling ketat, dll. Kami hanya peduli dengan perilaku metode (nilai balik dan efek samping), bukan kelas / modul / register CPU / dll. itu digunakan dalam proses.