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
Car
kelas erat digabungkan dengan pelaksanaan tertentu dari tanduk di Horn
kelas. Jika kita ingin mengubah jenis klakson yang digunakan oleh mobil, kita harus memodifikasi Car
kelasnya meskipun penggunaan klakson tidak berubah. Ini juga membuat pengujian sulit karena kita tidak dapat menguji Car
kelas secara terpisah dari ketergantungannya, Horn
kelas.
- The
Car
kelas bertanggung jawab untuk siklus hidup dari Horn
kelas. Dalam contoh sederhana seperti ini bukan masalah besar, tetapi dalam aplikasi nyata dependensi akan memiliki dependensi, yang akan memiliki dependensi, dll. Car
Kelas 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 Horn
mengimplementasikan kelas kami . Ini memungkinkan kita mengkodekan Car
kelas ke antarmuka alih-alih implementasi tertentu. Sekarang kode dapat mengambil apa pun yang mengimplementasikan IHorn
. Kedua, kami telah mengambil instantiasi klakson Car
dan 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 Car
kelas:
class FrenchHorn : IHorn
{
public void Honk()
{
Console.WriteLine("le beep!");
}
}
Main hanya bisa menyuntikkan instance FrenchHorn
kelas sebagai gantinya. Ini juga secara dramatis menyederhanakan pengujian. Anda bisa membuat MockHorn
kelas untuk disuntikkan ke Car
konstruktor untuk memastikan Anda menguji hanya Car
kelas 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 car
modul hanya akan mencoba memanggil honk
metode apa pun yang horn
diekspor modul.
Selain itu, karena require
cache Node semuanya, modul pada dasarnya adalah lajang yang disimpan dalam sebuah wadah. Modul lain yang melakukan require
pada horn
modul akan mendapatkan contoh yang sama persis. Ini membuat berbagi objek tunggal seperti koneksi basis data sangat mudah.
Sekarang masih ada masalah bahwa car
modul bertanggung jawab untuk mengambil ketergantungannya sendiri horn
. Jika Anda ingin mobil menggunakan modul berbeda untuk klaksonnya, Anda harus mengubah require
pernyataan di car
modul. 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.js
modul 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.js
dan horn.js
modul yang sama persis di atas, Anda dapat menulis ulang index.js
modul 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.