Tôi biết chủ đề này khá cũ ở thời điểm này, nhưng tôi đoán rằng tôi đã đồng ý với suy nghĩ của mình về điều này. TL; DR là do tính chất động, không được kiểm soát của JavaScript, bạn thực sự có thể làm được khá nhiều việc mà không cần dùng đến mẫu tiêm phụ thuộc (DI) hoặc sử dụng khung DI. Tuy nhiên, khi một ứng dụng phát triển lớn hơn và phức tạp hơn, DI chắc chắn có thể giúp duy trì mã của bạn.
DI trong C #
Để hiểu lý do tại sao DI không phải là một nhu cầu lớn trong JavaScript, thật hữu ích khi xem xét một ngôn ngữ được gõ mạnh như C #. (Xin lỗi những người không biết C #, nhưng nó cũng đủ dễ để theo dõi.) Giả sử chúng tôi có một ứng dụng mô tả một chiếc xe và còi của nó. Bạn sẽ định nghĩa hai lớp:
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();
}
}
Có vài vấn đề với việc viết mã theo cách này.
- Các
Car
lớp học được kết hợp chặt chẽ với việc thực hiện cụ thể của sừng trong Horn
lớp. Nếu chúng ta muốn thay đổi loại còi được sử dụng bởi ô tô, chúng ta phải sửa đổi Car
lớp mặc dù cách sử dụng còi không thay đổi. Điều này cũng làm cho việc kiểm tra trở nên khó khăn vì chúng ta không thể kiểm tra Car
lớp một cách tách biệt với sự phụ thuộc của nó, Horn
lớp.
- Các
Car
lớp chịu trách nhiệm về vòng đời của Horn
lớp. Trong một ví dụ đơn giản như thế này không phải là vấn đề lớn, nhưng trong các ứng dụng thực tế, phụ thuộc sẽ có phụ thuộc, sẽ có phụ thuộc, v.v.Car
Lớp sẽ cần có trách nhiệm tạo toàn bộ cây phụ thuộc của nó. Điều này không chỉ phức tạp và lặp đi lặp lại mà còn vi phạm "trách nhiệm duy nhất" của lớp. Nó nên tập trung vào việc là một chiếc xe, không tạo ra các trường hợp.
- Không có cách nào để sử dụng lại các trường hợp phụ thuộc tương tự. Một lần nữa, điều này không quan trọng trong ứng dụng đồ chơi này, nhưng hãy xem xét kết nối cơ sở dữ liệu. Bạn thường có một trường hợp duy nhất được chia sẻ trên ứng dụng của bạn.
Bây giờ, hãy cấu trúc lại cái này để sử dụng mẫu tiêm phụ thuộc.
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();
}
}
Chúng tôi đã thực hiện hai điều quan trọng ở đây. Đầu tiên, chúng tôi đã giới thiệu một giao diện mà Horn
lớp chúng tôi thực hiện. Điều này cho phép chúng tôi mã Car
lớp cho giao diện thay vì thực hiện cụ thể. Bây giờ mã có thể mất bất cứ điều gì thực hiện IHorn
. Thứ hai, chúng tôi đã lấy tiếng còi ra khỏiCar
và thay vào đó. Điều này giải quyết các vấn đề ở trên và để nó cho chức năng chính của ứng dụng để quản lý các trường hợp cụ thể và vòng đời của chúng.
Điều này có nghĩa là có thể giới thiệu một loại còi mới cho xe sử dụng mà không cần chạm vào Car
lớp:
class FrenchHorn : IHorn
{
public void Honk()
{
Console.WriteLine("le beep!");
}
}
Chính chỉ có thể tiêm một thể hiện của FrenchHorn
lớp thay thế. Điều này cũng đơn giản hóa đáng kể việc thử nghiệm. Bạn có thể tạo một MockHorn
lớp để tiêm vàoCar
tạo để đảm bảo bạn đang kiểm tra chỉCar
lớp đó.
Ví dụ trên cho thấy tiêm phụ thuộc thủ công. Thông thường DI được thực hiện với một khung (ví dụ: Unity hoặc Ninject trong thế giới C #). Các khung này sẽ thực hiện tất cả các hệ thống dây phụ thuộc cho bạn bằng cách đi qua biểu đồ phụ thuộc của bạn và tạo các thể hiện khi cần thiết.
Cách Node.js tiêu chuẩn
Bây giờ hãy xem xét ví dụ tương tự trong Node.js. Chúng tôi có thể sẽ chia mã của chúng tôi thành 3 mô-đun:
// 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();
Bởi vì JavaScript được tháo gỡ, chúng tôi không có sự kết hợp chặt chẽ giống như trước đây. Không cần giao diện (cũng không tồn tại) vì car
mô-đun sẽ chỉ cố gắng gọi honk
phương thức trên bất cứ điều gìhorn
mô-đun xuất.
Ngoài ra, vì require
lưu trữ tất cả mọi thứ của Node , các mô-đun về cơ bản là các singletons được lưu trữ trong một thùng chứa. Bất kỳ mô-đun nào khác thực hiện một require
trênhorn
mô-đun sẽ nhận được cùng một ví dụ chính xác. Điều này làm cho việc chia sẻ các đối tượng singleton như kết nối cơ sở dữ liệu rất dễ dàng.
Bây giờ vẫn còn vấn đề là car
mô-đun chịu trách nhiệm tìm nạp phụ thuộc của chính nó horn
. Nếu bạn muốn chiếc xe sử dụng một mô-đun khác cho còi của nó, bạn phải thay đổi require
tuyên bố trongcar
mô-đun. Đây không phải là một điều rất phổ biến để làm, nhưng nó gây ra vấn đề với thử nghiệm.
Cách thông thường mọi người xử lý vấn đề kiểm tra là với proxyquire . Do tính chất động của JavaScript, proxyquire chặn các cuộc gọi để yêu cầu và trả lại bất kỳ sơ khai / giả nào bạn cung cấp thay thế.
var proxyquire = require('proxyquire');
var hornStub = {
honk: function () {
console.log("test beep!");
}
};
var car = proxyquire('./car', { './horn': hornStub });
// Now make test assertions on car...
Điều này là quá đủ cho hầu hết các ứng dụng. Nếu nó hoạt động cho ứng dụng của bạn thì hãy đi với nó. Tuy nhiên, theo kinh nghiệm của tôi khi các ứng dụng phát triển lớn hơn và phức tạp hơn, việc duy trì mã như thế này trở nên khó khăn hơn.
DI trong JavaScript
Node.js rất linh hoạt. Nếu bạn không hài lòng với phương pháp trên, bạn có thể viết các mô-đun của mình bằng cách sử dụng mẫu tiêm phụ thuộc. Trong mẫu này, mỗi mô-đun xuất một hàm nhà máy (hoặc một hàm tạo lớp).
// 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();
Điều này rất giống với phương pháp C # trước đó ở chỗ index.js
mô-đun chịu trách nhiệm cho vòng đời và hệ thống dây điện. Kiểm thử đơn vị khá đơn giản vì bạn chỉ có thể chuyển qua các mô hình / sơ khai cho các chức năng. Một lần nữa, nếu điều này là đủ tốt cho ứng dụng của bạn đi với nó.
Khung Bolus DI
Không giống như C #, không có khung DI tiêu chuẩn nào được thiết lập để giúp quản lý phụ thuộc của bạn. Có một số khung trong sổ đăng ký npm nhưng không có khung nào được áp dụng rộng rãi. Nhiều lựa chọn trong số này đã được trích dẫn trong các câu trả lời khác.
Tôi không đặc biệt hài lòng với bất kỳ tùy chọn nào có sẵn nên tôi đã viết bolus của riêng mình . Bolus được thiết kế để hoạt động với mã được viết theo kiểu DI ở trên và cố gắng rất DRY và rất đơn giản. Sử dụng chính xác car.js
và horn.js
các mô-đun ở trên, bạn có thể viết lại index.js
mô-đun với bolus như:
// index.js
var Injector = require("bolus");
var injector = new Injector();
injector.registerPath("**/*.js");
var car = injector.resolve("car");
car.honkHorn();
Ý tưởng cơ bản là bạn tạo ra một kim phun. Bạn đăng ký tất cả các mô-đun của bạn trong kim phun. Sau đó, bạn chỉ cần giải quyết những gì bạn cần. Bolus sẽ đi theo biểu đồ phụ thuộc và tạo và tiêm phụ thuộc khi cần thiết. Bạn không tiết kiệm nhiều trong một ví dụ đồ chơi như thế này, nhưng trong các ứng dụng lớn với cây phụ thuộc phức tạp, số tiền tiết kiệm là rất lớn.
Bolus hỗ trợ một loạt các tính năng tiện lợi như phụ thuộc tùy chọn và toàn cầu thử nghiệm, nhưng có hai lợi ích chính mà tôi đã thấy liên quan đến cách tiếp cận Node.js tiêu chuẩn. Đầu tiên, nếu bạn có nhiều ứng dụng tương tự, bạn có thể tạo mô-đun npm riêng cho cơ sở của mình để tạo ra một trình tiêm và đăng ký các đối tượng hữu ích trên đó. Sau đó, các ứng dụng cụ thể của bạn có thể thêm, ghi đè và giải quyết khi cần giống như cách AngularJScông việc kim phun. Thứ hai, bạn có thể sử dụng bolus để quản lý các bối cảnh phụ thuộc khác nhau. Ví dụ: bạn có thể sử dụng phần mềm trung gian để tạo một trình tiêm con theo yêu cầu, đăng ký id người dùng, id phiên, logger, v.v. trên trình tiêm cùng với bất kỳ mô-đun nào tùy thuộc vào các mô-đun đó. Sau đó giải quyết những gì bạn cần để phục vụ yêu cầu. Điều này cung cấp cho bạn các phiên bản của các mô-đun của bạn theo yêu cầu và ngăn chặn việc phải chuyển bộ ghi, v.v. cho mỗi cuộc gọi chức năng mô-đun.