Saya tahu utas ini cukup lama pada saat ini, tetapi saya pikir saya akan berpadu dengan pemikiran saya tentang ini. TL; DR adalah bahwa karena sifat JavaScript yang tidak diketik dan dinamis, Anda sebenarnya dapat melakukan cukup banyak tanpa menggunakan pola ketergantungan injeksi (DI) atau menggunakan kerangka kerja DI. Namun, ketika aplikasi tumbuh lebih besar dan lebih kompleks, DI pasti dapat membantu pemeliharaan kode Anda.
DI dalam C #
Untuk memahami mengapa DI tidak sebesar kebutuhan dalam JavaScript, sangat membantu untuk melihat bahasa yang sangat diketik seperti C #. (Permintaan maaf kepada mereka yang tidak tahu C #, tetapi seharusnya cukup mudah untuk diikuti.) Katakanlah kita memiliki aplikasi yang menggambarkan mobil dan klaksonnya. Anda akan mendefinisikan dua kelas:
class Horn
{
public void Honk()
{
Console.WriteLine("beep!");
}
}
class Car
{
private Horn horn;
public Car()
{
this.horn = new Horn();
}
public void HonkHorn()
{
this.horn.Honk();
}
}
class Program
{
static void Main()
{
var car = new Car();
car.HonkHorn();
}
}
Ada beberapa masalah dengan penulisan kode dengan cara ini.
- The
Carkelas erat digabungkan dengan pelaksanaan tertentu dari tanduk di Hornkelas. Jika kita ingin mengubah jenis klakson yang digunakan oleh mobil, kita harus memodifikasi Carkelasnya meskipun penggunaan klakson tidak berubah. Ini juga membuat pengujian sulit karena kita tidak dapat menguji Carkelas secara terpisah dari ketergantungannya, Hornkelas.
- The
Carkelas bertanggung jawab untuk siklus hidup dari Hornkelas. Dalam contoh sederhana seperti ini bukan masalah besar, tetapi dalam aplikasi nyata dependensi akan memiliki dependensi, yang akan memiliki dependensi, dll. CarKelas akan perlu bertanggung jawab untuk membuat seluruh pohon dependensi. Ini tidak hanya rumit dan berulang, tetapi juga melanggar "tanggung jawab tunggal" kelas. Seharusnya fokus pada menjadi mobil, bukan menciptakan instance.
- Tidak ada cara untuk menggunakan kembali contoh ketergantungan yang sama. Sekali lagi, ini tidak penting dalam aplikasi mainan ini, tetapi pertimbangkan koneksi basis data. Anda biasanya memiliki satu instance yang dibagikan di seluruh aplikasi Anda.
Sekarang, mari kita refactor ini untuk menggunakan pola injeksi ketergantungan.
interface IHorn
{
void Honk();
}
class Horn : IHorn
{
public void Honk()
{
Console.WriteLine("beep!");
}
}
class Car
{
private IHorn horn;
public Car(IHorn horn)
{
this.horn = horn;
}
public void HonkHorn()
{
this.horn.Honk();
}
}
class Program
{
static void Main()
{
var horn = new Horn();
var car = new Car(horn);
car.HonkHorn();
}
}
Kami telah melakukan dua hal penting di sini. Pertama, kami telah memperkenalkan antarmuka yang Hornmengimplementasikan kelas kami . Ini memungkinkan kita mengkodekan Carkelas ke antarmuka alih-alih implementasi tertentu. Sekarang kode dapat mengambil apa pun yang mengimplementasikan IHorn. Kedua, kami telah mengambil instantiasi klakson Cardan membagikannya. Ini menyelesaikan masalah di atas dan membiarkannya ke fungsi utama aplikasi untuk mengelola instance spesifik dan siklus hidupnya.
Apa artinya ini yang akan memperkenalkan klakson tipe baru untuk digunakan mobil tanpa menyentuh Carkelas:
class FrenchHorn : IHorn
{
public void Honk()
{
Console.WriteLine("le beep!");
}
}
Main hanya bisa menyuntikkan instance FrenchHornkelas sebagai gantinya. Ini juga secara dramatis menyederhanakan pengujian. Anda bisa membuat MockHornkelas untuk disuntikkan ke Carkonstruktor untuk memastikan Anda menguji hanya Carkelas secara terpisah.
Contoh di atas menunjukkan injeksi ketergantungan manual. Biasanya DI dilakukan dengan kerangka kerja (misalnya Unity atau Ninject di dunia C #). Kerangka kerja ini akan melakukan semua kabel dependensi untuk Anda dengan menapaki grafik dependensi Anda dan membuat instance yang diperlukan.
Cara Node.js Standar
Sekarang mari kita lihat contoh yang sama di Node.js. Kami mungkin akan memecah kode kami menjadi 3 modul:
// horn.js
module.exports = {
honk: function () {
console.log("beep!");
}
};
// car.js
var horn = require("./horn");
module.exports = {
honkHorn: function () {
horn.honk();
}
};
// index.js
var car = require("./car");
car.honkHorn();
Karena JavaScript tidak diketik, kami tidak memiliki kopling ketat yang sama seperti yang kami miliki sebelumnya. Tidak perlu antarmuka (juga tidak ada) karena carmodul hanya akan mencoba memanggil honkmetode apa pun yang horndiekspor modul.
Selain itu, karena requirecache Node semuanya, modul pada dasarnya adalah lajang yang disimpan dalam sebuah wadah. Modul lain yang melakukan requirepada hornmodul akan mendapatkan contoh yang sama persis. Ini membuat berbagi objek tunggal seperti koneksi basis data sangat mudah.
Sekarang masih ada masalah bahwa carmodul bertanggung jawab untuk mengambil ketergantungannya sendiri horn. Jika Anda ingin mobil menggunakan modul berbeda untuk klaksonnya, Anda harus mengubah requirepernyataan di carmodul. Ini bukan hal yang sangat umum untuk dilakukan, tetapi hal itu menyebabkan masalah dengan pengujian.
Cara biasa orang menangani masalah pengujian adalah dengan proxyquire . Karena sifat dinamis dari JavaScript, proksi meminta intersep panggilan untuk meminta dan mengembalikan setiap bertopik / mengejek yang Anda berikan.
var proxyquire = require('proxyquire');
var hornStub = {
honk: function () {
console.log("test beep!");
}
};
var car = proxyquire('./car', { './horn': hornStub });
// Now make test assertions on car...
Ini lebih dari cukup untuk sebagian besar aplikasi. Jika itu berfungsi untuk aplikasi Anda, maka ikutilah. Namun, dalam pengalaman saya ketika aplikasi tumbuh lebih besar dan lebih kompleks, mempertahankan kode seperti ini menjadi lebih sulit.
DI dalam JavaScript
Node.js sangat fleksibel. Jika Anda tidak puas dengan metode di atas, Anda dapat menulis modul menggunakan pola injeksi ketergantungan. Dalam pola ini, setiap modul mengekspor fungsi pabrik (atau konstruktor kelas).
// horn.js
module.exports = function () {
return {
honk: function () {
console.log("beep!");
}
};
};
// car.js
module.exports = function (horn) {
return {
honkHorn: function () {
horn.honk();
}
};
};
// index.js
var horn = require("./horn")();
var car = require("./car")(horn);
car.honkHorn();
Ini sangat analog dengan metode C # sebelumnya dalam index.jsmodul yang bertanggung jawab untuk siklus hidup dan kabel misalnya. Pengujian unit cukup sederhana karena Anda bisa mengoper / bertopik pada fungsi. Sekali lagi, jika ini cukup baik untuk aplikasi Anda, ikutilah.
Kerangka Kerja Bolus DI
Tidak seperti C #, tidak ada kerangka kerja DI standar yang ditetapkan untuk membantu manajemen ketergantungan Anda. Ada sejumlah kerangka kerja dalam registri npm tetapi tidak ada yang memiliki adopsi luas. Banyak dari opsi ini telah dikutip di jawaban lain.
Saya tidak terlalu senang dengan salah satu opsi yang tersedia jadi saya menulis bolus saya sendiri . Bolus dirancang untuk bekerja dengan kode yang ditulis dengan gaya DI di atas dan mencoba menjadi sangat KERING dan sangat sederhana. Dengan menggunakan modul car.jsdan horn.jsmodul yang sama persis di atas, Anda dapat menulis ulang index.jsmodul dengan bolus sebagai:
// index.js
var Injector = require("bolus");
var injector = new Injector();
injector.registerPath("**/*.js");
var car = injector.resolve("car");
car.honkHorn();
Ide dasarnya adalah Anda membuat injektor. Anda mendaftarkan semua modul Anda di injektor. Maka Anda cukup menyelesaikan apa yang Anda butuhkan. Bolus akan menjalankan grafik dependensi dan membuat serta menyuntikkan dependensi sesuai kebutuhan. Anda tidak menyimpan banyak dalam contoh mainan seperti ini, tetapi dalam aplikasi besar dengan pohon ketergantungan yang rumit, penghematannya sangat besar.
Bolus mendukung banyak fitur bagus seperti dependensi opsional dan uji global, tetapi ada dua manfaat utama yang saya lihat relatif terhadap pendekatan standar Node.js. Pertama, jika Anda memiliki banyak aplikasi serupa, Anda dapat membuat modul npm pribadi untuk basis Anda yang membuat injektor dan mendaftarkan objek yang berguna di dalamnya. Kemudian aplikasi spesifik Anda dapat menambah, menimpa, dan menyelesaikan sesuai kebutuhan seperti cara AngularJSinjektor bekerja. Kedua, Anda dapat menggunakan bolus untuk mengelola berbagai konteks dependensi. Misalnya, Anda bisa menggunakan middleware untuk membuat injektor anak per permintaan, mendaftarkan id pengguna, id sesi, logger, dll. Pada injektor bersama dengan modul apa pun tergantung pada mereka. Kemudian selesaikan apa yang Anda butuhkan untuk melayani permintaan. Ini memberi Anda contoh modul Anda per permintaan dan mencegah harus lulus logger, dll. Sepanjang setiap panggilan fungsi modul.