Wiem, że ten wątek jest w tym momencie dość stary, ale pomyślałem, że włączy się z moimi przemyśleniami na ten temat. TL; DR jest taki, że ze względu na nietypowy, dynamiczny charakter JavaScript, możesz naprawdę zrobić wiele bez uciekania się do wzorca wstrzykiwania zależności (DI) lub korzystania z frameworku DI. Ponieważ jednak aplikacja staje się większa i bardziej złożona, DI może zdecydowanie pomóc w utrzymaniu kodu.
DI w C #
Aby zrozumieć, dlaczego DI nie jest tak potrzebna w JavaScript, warto przyjrzeć się silnie napisanemu językowi, np. C #. (Przepraszam tych, którzy nie znają języka C #, ale powinno być wystarczająco łatwe do naśladowania.) Załóżmy, że mamy aplikację opisującą samochód i jego klakson. Zdefiniowałbyś dwie klasy:
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();
}
}
Istnieje kilka problemów z pisaniem kodu w ten sposób.
Car
Klasa jest ściśle sprzężony z konkretnego wdrożenia róg w Horn
klasie. Jeśli chcemy zmienić rodzaj klaksonu używanego przez samochód, musimy zmodyfikować Car
klasę, nawet jeśli jego użycie nie zmieni się. Utrudnia to również testowanie, ponieważ nie możemy przetestować Car
klasy w oderwaniu od jej zależności, Horn
klasy.
Car
Klasa jest odpowiedzialna za cykl życia Horn
klasy. W prostym przykładzie, takim jak ten, nie jest to duży problem, ale w rzeczywistych aplikacjach zależności będą miały zależności, które będą miały zależności itp. Car
Klasa musiałaby być odpowiedzialna za utworzenie całego drzewa swoich zależności. Jest to nie tylko skomplikowane i powtarzalne, ale narusza „pojedynczą odpowiedzialność” klasy. Powinien koncentrować się na byciu samochodem, a nie na tworzeniu instancji.
- Nie ma możliwości ponownego użycia tych samych instancji zależności. Ponownie, nie jest to ważne w tej zabawkowej aplikacji, ale rozważ połączenie z bazą danych. Zazwyczaj masz jedną instancję, która jest współużytkowana przez aplikację.
Refaktoryzujmy to, aby zastosować wzorzec wstrzykiwania zależności.
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();
}
}
Zrobiliśmy tutaj dwie kluczowe rzeczy. Po pierwsze, wprowadziliśmy interfejs, który Horn
implementuje nasza klasa. To pozwala nam kodować Car
klasę do interfejsu zamiast konkretnej implementacji. Teraz kod może wziąć wszystko, co implementuje IHorn
. Po drugie, usunęliśmy instancję klaksonu Car
i przekazaliśmy ją zamiast tego. To rozwiązuje powyższe problemy i pozostawia głównej funkcji aplikacji zarządzanie konkretnymi instancjami i ich cyklami życia.
Oznacza to, że mógłby wprowadzić nowy rodzaj klaksonu do użycia w samochodzie bez dotykania Car
klasy:
class FrenchHorn : IHorn
{
public void Honk()
{
Console.WriteLine("le beep!");
}
}
Main może FrenchHorn
zamiast tego wstrzyknąć instancję klasy. To również znacznie upraszcza testowanie. Możesz utworzyć MockHorn
klasę do wstrzyknięcia do Car
konstruktora, aby upewnić się, że testujesz tylko Car
klasę w izolacji.
Powyższy przykład pokazuje ręczne wstrzykiwanie zależności. Zazwyczaj DI odbywa się za pomocą frameworka (np. Unity lub Ninject w świecie C #). Ramy te wykonają wszystkie okablowanie zależności, przechodząc po wykresie zależności i tworząc instancje zgodnie z potrzebami.
Standardowy sposób Node.js
Teraz spójrzmy na ten sam przykład w Node.js. Prawdopodobnie podzielilibyśmy nasz kod na 3 moduły:
// 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();
Ponieważ JavaScript jest bez typu, nie mamy takiego samego ścisłego sprzężenia, jakie mieliśmy wcześniej. Interfejsy nie są potrzebne (ani one nie istnieją), ponieważ car
moduł będzie próbował wywołać honk
metodę niezależnie od tego, co horn
moduł eksportuje.
Dodatkowo, ponieważ Node require
buforuje wszystko, moduły są zasadniczo singletonami przechowywanymi w kontenerze. Każdy inny moduł, który wykonuje a require
na horn
module, otrzyma dokładnie to samo wystąpienie. To sprawia, że udostępnianie pojedynczych obiektów, takich jak połączenia z bazą danych, jest bardzo łatwe.
Teraz nadal istnieje problem, że car
moduł jest odpowiedzialny za pobieranie własnej zależności horn
. Jeśli chcesz, aby samochód używał innego modułu dla klaksonu, musisz zmienić require
instrukcję w car
module. Nie jest to zbyt powszechne, ale powoduje problemy z testowaniem.
Zwykle ludzie radzą sobie z problemem testowania za pomocą proxyquire . Ze względu na dynamiczny charakter JavaScript, proxyquire przechwytuje wywołania wymagające i zwraca zamiast tego wszystkie kody pośredniczące / próbne.
var proxyquire = require('proxyquire');
var hornStub = {
honk: function () {
console.log("test beep!");
}
};
var car = proxyquire('./car', { './horn': hornStub });
// Now make test assertions on car...
To więcej niż wystarcza dla większości aplikacji. Jeśli działa w Twojej aplikacji, idź z nią. Jednak z mojego doświadczenia wynika, że aplikacje stają się większe i bardziej złożone, utrzymanie takiego kodu staje się trudniejsze.
DI w JavaScript
Node.js jest bardzo elastyczny. Jeśli powyższa metoda nie jest zadowalająca, możesz napisać moduły przy użyciu wzorca wstrzykiwania zależności. W tym wzorze każdy moduł eksportuje funkcję fabryki (lub konstruktora klasy).
// 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();
Jest to bardzo analogiczne do wcześniejszej metody C #, ponieważ index.js
moduł jest odpowiedzialny za np. Cykle życia i okablowanie. Testowanie jednostkowe jest dość proste, ponieważ można po prostu przekazać symulacje / kody pośredniczące do funkcji. Ponownie, jeśli jest to wystarczająco dobre dla twojej aplikacji, idź z nim.
Bolus DI Framework
W przeciwieństwie do C #, nie ma ustalonych standardowych ram DI, które mogłyby pomóc w zarządzaniu zależnościami. Istnieje wiele struktur w rejestrze npm, ale żadna z nich nie jest szeroko rozpowszechniona. Wiele z tych opcji cytowano już w innych odpowiedziach.
Nie byłem szczególnie zadowolony z żadnej z dostępnych opcji, więc napisałem własną, zwaną bolusem . Bolus został zaprojektowany do pracy z kodem napisanym powyżej w stylu DI i stara się być bardzo SUCHY i bardzo prosty. Używając dokładnie tego samego car.js
i horn.js
powyższych modułów, możesz przepisać index.js
moduł bolusem jako:
// index.js
var Injector = require("bolus");
var injector = new Injector();
injector.registerPath("**/*.js");
var car = injector.resolve("car");
car.honkHorn();
Podstawową ideą jest to, że tworzysz wtryskiwacz. Rejestrujesz wszystkie swoje moduły we wtryskiwaczu. Następnie po prostu rozwiązujesz to, czego potrzebujesz. Bolus przejdzie po wykresie zależności i w razie potrzeby utworzy i wstrzykuje zależności. Nie oszczędzasz dużo w takim przykładzie zabawki, ale w dużych aplikacjach ze skomplikowanymi drzewami zależności oszczędności są ogromne.
Bolus obsługuje wiele fajnych funkcji, takich jak opcjonalne zależności i globalne testy, ale są dwie kluczowe korzyści, które widziałem w porównaniu ze standardowym podejściem do Node.js. Po pierwsze, jeśli masz wiele podobnych aplikacji, możesz utworzyć prywatny moduł npm dla swojej bazy, który tworzy wtryskiwacz i rejestruje na nim przydatne obiekty. Następnie określone aplikacje mogą dodawać, zastępować i rozwiązywać w razie potrzeby, podobnie jak w AngularJSwtryskiwacz działa. Po drugie, możesz użyć bolusa do zarządzania różnymi kontekstami zależności. Na przykład można użyć oprogramowania pośredniego do utworzenia wtryskiwacza potomnego na żądanie, zarejestrować identyfikator użytkownika, identyfikator sesji, rejestrator itp. We wtryskiwaczu wraz z dowolnymi modułami zależnymi od nich. Następnie określ, czego potrzebujesz, aby obsłużyć żądania. Daje to instancje modułów na żądanie i zapobiega przekazywaniu rejestratora itp. Do każdego wywołania funkcji modułu.