Czy potrzebuję wstrzyknięcia zależności w NodeJS, czy jak poradzić sobie z…?


219

Obecnie tworzę eksperymentalne projekty z nodejs. Z Springem zaprogramowałem wiele aplikacji Java EE i doceniam łatwość wstrzykiwania zależności.

Teraz jestem ciekawy: jak wykonać wstrzyknięcie zależności za pomocą węzła? Lub: Czy w ogóle go potrzebuję? Czy istnieje koncepcja zastępująca, ponieważ styl programowania jest inny?

Mówię o prostych rzeczach, jak na przykład udostępnianie obiektu połączenia z bazą danych, ale nie znalazłem rozwiązania, które mnie zadowoli.


1
Jeśli zdecydujesz się na użycie DI, OpenTable niedawno otworzył do niego bibliotekę: github.com/opentable/spur-ioc Użyłem go (pracuję tam) i mogę powiedzieć, że jest dość prosty i świetny do testowania.
tybro0103

Odpowiedzi:


107

Krótko mówiąc, nie potrzebujesz kontenera wstrzykiwania zależności ani lokalizatora usług, tak jak w języku C # / Java. Ponieważ Node.js wykorzystuje module pattern, nie jest konieczne wprowadzanie konstruktora lub właściwości. Chociaż nadal możesz.

Wspaniałą rzeczą w JS jest to, że możesz modyfikować praktycznie wszystko, aby osiągnąć to, co chcesz. Jest to przydatne przy testowaniu.

Oto mój bardzo kiepski wymyślony przykład.

MyClass.js:

var fs = require('fs');

MyClass.prototype.errorFileExists = function(dir) {
    var dirsOrFiles = fs.readdirSync(dir);
    for (var d in dirsOrFiles) {
        if (d === 'error.txt') return true;
    }
    return false;
};

MyClass.test.js:

describe('MyClass', function(){
    it('should return an error if error.txt is found in the directory', function(done){
        var mc = new MyClass();
        assert(mc.errorFileExists('/tmp/mydir')); //true
    });
});

Zauważ, jak to MyClasszależy od fsmodułu? Jak wspomniano w @ShatyemShekhar, rzeczywiście można wykonać iniekcję konstruktora lub właściwości, tak jak w innych językach. Ale nie jest to konieczne w Javascript.

W takim przypadku możesz zrobić dwie rzeczy.

Możesz usunąć fs.readdirSyncmetodę lub możesz zwrócić zupełnie inny moduł podczas wywoływania require.

Metoda 1:

var oldmethod = fs.readdirSync;
fs.readdirSync = function(dir) { 
    return ['somefile.txt', 'error.txt', 'anotherfile.txt']; 
};

*** PERFORM TEST ***
*** RESTORE METHOD AFTER TEST ****
fs.readddirSync = oldmethod;

Metoda 2:

var oldrequire = require
require = function(module) {
    if (module === 'fs') {
        return {
            readdirSync: function(dir) { 
                return ['somefile.txt', 'error.txt', 'anotherfile.txt']; 
            };
        };
    } else
        return oldrequire(module);

}

Kluczem jest wykorzystanie siły Node.js i Javascript. Uwaga: jestem facetem CoffeeScript, więc moja składnia JS może być gdzieś niepoprawna. Nie mówię też, że to najlepszy sposób, ale taki jest sposób. Guru JavaScript mogą być w stanie wejść do innych rozwiązań.

Aktualizacja:

To powinno odpowiedzieć na twoje konkretne pytanie dotyczące połączeń z bazą danych. Stworzę osobny moduł do kapsułkowania logiki połączenia z bazą danych. Coś takiego:

MyDbConnection.js: (pamiętaj, aby wybrać lepszą nazwę)

var db = require('whichever_db_vendor_i_use');

module.exports.fetchConnection() = function() {
    //logic to test connection

    //do I want to connection pool?

    //do I need only one connection throughout the lifecyle of my application?

    return db.createConnection(port, host, databasename); //<--- values typically from a config file    
}

Następnie każdy moduł, który wymaga połączenia z bazą danych, po prostu zawiera Twój MyDbConnectionmoduł.

SuperCoolWebApp.js:

var dbCon = require('./lib/mydbconnection'); //wherever the file is stored

//now do something with the connection
var connection = dbCon.fetchConnection(); //mydbconnection.js is responsible for pooling, reusing, whatever your app use case is

//come TEST time of SuperCoolWebApp, you can set the require or return whatever you want, or, like I said, use an actual connection to a TEST database. 

Nie podążaj za tym przykładem dosłownie. To kiepski przykład próby komunikowania, że ​​używasz modulewzorca do zarządzania swoimi zależnościami. Mam nadzieję, że to pomoże trochę więcej.


42
Dotyczy to testowania, ale DI ma inne zalety; za pomocą DI można zaprogramować interfejs, a nie implementację.
moteutsch

3
@moteutsch Nie wiem, dlaczego miałbyś to robić, ponieważ JS nie ma pojęcia interfejsów takich jak większość języków statycznych. Wszystko, co naprawdę masz, to implementacje, nawet jeśli chcesz użyć wcześniej uzgodnionego, udokumentowanego „interfejsu”.
JP Richardson

16
@JPRichardson Jak napisać komponent korzystający z programu rejestrującego bez konieczności korzystania z żadnej biblioteki? Jeśli ja require('my_logger_library'), osoby korzystające z mojego komponentu będą musiały zastąpić wymóg korzystania z własnej biblioteki. Zamiast tego mogę pozwolić ludziom na przekazanie wywołania zwrotnego, które otacza implementację programu rejestrującego do metody „konstruktor” lub „init”. Taki jest cel DI.
moteutsch,

4
Od połowy 2014 r. - npmjs.org/package/proxyquire sprawia, że ​​drwiny z „wymagających” zależności są banalne.
arcseldon

4
Nie rozumiem, zastąpienie wymagania w jednym module nie zastępuje go w innym. Jeśli w moim teście ustawię opcję wymaganą dla funkcji, a następnie wymagam przetestowania modułu, to w testowanym obiekcie nie będą używane instrukcje wymagane, nie używaj zestawu funkcji w module testowym. Jak to wstrzykuje zależności?
HMR,

72

requirejest sposobem zarządzania zależnościami w node.js i pewnie to jest intuicyjny i skuteczny, ale ma też swoje ograniczenia.

Radzę rzucić okiem na niektóre z dostępnych dziś pojemników do wstrzykiwania zależności, aby Node.js zorientował się, jakie są ich zalety / wady. Niektórzy z nich są:

Żeby wymienić tylko kilka.

Teraz prawdziwe pytanie brzmi: co można osiągnąć za pomocą kontenera DI Node.js w porównaniu do prostego require?

Plusy:

  • lepsza testowalność: moduły akceptują swoje zależności jako dane wejściowe
  • Odwrócenie kontroli: zdecyduj, jak podłączyć moduły bez dotykania głównego kodu aplikacji.
  • konfigurowalny algorytm rozwiązywania modułów: zależności mają „wirtualne” identyfikatory, zwykle nie są powiązane ze ścieżką w systemie plików.
  • Lepsza rozszerzalność: włączona przez IoC i „wirtualne” identyfikatory.
  • Możliwe inne fantazyjne rzeczy:
    • Inicjalizacja asynchroniczna
    • Zarządzanie cyklem życia modułu
    • Rozszerzalność samego pojemnika DI
    • Może łatwo wdrożyć abstrakcje wyższego poziomu (np. AOP)

Cons:

  • W odróżnieniu od „doświadczenia” Node.js: nie używanie requirezdecydowanie sprawia wrażenie odbiegającego od sposobu myślenia Node.
  • Związek między zależnością a jej implementacją nie zawsze jest wyraźny. Zależność może zostać rozwiązana w czasie wykonywania i wpływać na nią różne parametry. Kod staje się trudniejszy do zrozumienia i debugowania
  • Wolniejszy czas uruchamiania
  • Dojrzałość (w tej chwili): żadne z obecnych rozwiązań nie jest obecnie tak popularne, więc nie ma zbyt wielu samouczków, żadnego ekosystemu, nie przetestowano w walce.
  • Niektóre kontenery DI nie będą dobrze współpracować z programami pakującymi moduły, takimi jak Browserify i Webpack.

Podobnie jak w przypadku wszystkich elementów związanych z tworzeniem oprogramowania, wybór pomiędzy DI lub requirezależy od wymagań, złożoności systemu i stylu programowania.


3
Czy uważasz, że sytuacja znacznie się zmieniła od 2009 roku?
Juho Vepsäläinen,

13
Masz na myśli od 10 dni temu? :)
Mario,

2
Nieee 9 grudnia ... Powinien był wiedzieć.
Juho Vepsäläinen,

4
„Zaimplementowałem” DI za pomocą wzorca module.exports = function (deps) {}. Tak, to działa, ale nie jest całkiem idealne.
Juho Vepsäläinen,

3
moduły akceptują ich zależności jako dane wejściowe, a zależności nie są dla mnie wyraźnymi dźwiękami jak sprzeczność.
Anton Rudeshko

53

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.

  1. CarKlasa jest ściśle sprzężony z konkretnego wdrożenia róg w Hornklasie. Jeśli chcemy zmienić rodzaj klaksonu używanego przez samochód, musimy zmodyfikować Carklasę, nawet jeśli jego użycie nie zmieni się. Utrudnia to również testowanie, ponieważ nie możemy przetestować Carklasy w oderwaniu od jej zależności, Hornklasy.
  2. CarKlasa jest odpowiedzialna za cykl życia Hornklasy. 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. CarKlasa 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.
  3. 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 Hornimplementuje nasza klasa. To pozwala nam kodować Carklasę do interfejsu zamiast konkretnej implementacji. Teraz kod może wziąć wszystko, co implementuje IHorn. Po drugie, usunęliśmy instancję klaksonu Cari 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 Carklasy:

class FrenchHorn : IHorn
{
    public void Honk()
    {
        Console.WriteLine("le beep!");
    }
}

Main może FrenchHornzamiast tego wstrzyknąć instancję klasy. To również znacznie upraszcza testowanie. Możesz utworzyć MockHornklasę do wstrzyknięcia do Carkonstruktora, aby upewnić się, że testujesz tylko Carklasę 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ż carmoduł będzie próbował wywołać honkmetodę niezależnie od tego, co hornmoduł eksportuje.

Dodatkowo, ponieważ Node requirebuforuje wszystko, moduły są zasadniczo singletonami przechowywanymi w kontenerze. Każdy inny moduł, który wykonuje a requirena hornmodule, 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 carmoduł 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ć requireinstrukcję w carmodule. 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.jsmoduł 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.jsi horn.jspowyższych modułów, możesz przepisać index.jsmoduł 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.


1
prawdą jest również, że istnieją alternatywy dla proxyquiretakich, sinonktóre pozwalają na wykonywanie bardzo zwięzłych prób, np. let readFileStub = sinon.stub(fs, 'readFile').yields(new Error('something went wrong'));wtedy kolejne wywołania fs.readFilezwrócą błąd, dopóki nie cofniesz kodu pośredniczącego readFileStub.restore(). Osobiście uważam, że DI ma wątpliwe zastosowanie, ponieważ wydaje mi się, że prawie wymaga użycia klas / obiektów, co jest wątpliwe, biorąc pod uwagę funkcjonalne pochodzenie javascript.
Kevin

Dzięki za tę dobrą + szczegółową odpowiedź. Prawie mi tego brakowało, kiedy po raz pierwszy przeczytałem nagłówek DI w C # .
Konstantin A. Magg

1
Świetna odpowiedź. Zastanawiam się, jakie są Twoje myśli są w roku 2019. W przypadku dużych projektów, jak kwestia osobistych preferencji, co wolisz - DI / IoC w węźle, lub po prostu stubbing / szydząc z jest, rewire, proxyquire, itd.? Dzięki.
Jamie Corkhill,

Świetnie wyważona odpowiedź! Dziękuję Ci.
Johnny Oshika

36

Napisałem również moduł, aby to osiągnąć, nazywa się to rewire . Wystarczy użyć, npm install rewirea następnie:

var rewire = require("rewire"),
    myModule = rewire("./path/to/myModule.js"); // exactly like require()

// Your module will now export a special setter and getter for private variables.
myModule.__set__("myPrivateVar", 123);
myModule.__get__("myPrivateVar"); // = 123


// This allows you to mock almost everything within the module e.g. the fs-module.
// Just pass the variable name as first parameter and your mock as second.
myModule.__set__("fs", {
    readFile: function (path, encoding, cb) {
        cb(null, "Success!");
    }
});
myModule.readSomethingFromFileSystem(function (err, data) {
    console.log(data); // = Success!
});

Zainspirował mnie aplikator Nathana MacInnesa, ale zastosowałem inne podejście. Nie używam vmdo ewaluacji modułu testowego, w rzeczywistości używam własnych wymagań węzła. W ten sposób twój moduł zachowuje się dokładnie tak samo jak przy użyciu require()(z wyjątkiem twoich modyfikacji). Debugowanie jest również w pełni obsługiwane.


7
Od połowy 2014 r. - npmjs.org/package/proxyquire sprawia, że ​​drwiny z „wymagających” zależności są banalne.
arcseldon

proxyquire też jest fajne! Używają teraz wewnętrznego modułu, który jest znacznie lepszy niż vm węzła. Ale w końcu to tylko kwestia stylu. Lubię, aby mój moduł używał oryginalnego wymagania i później zamienił zależności. Ponadto rewire pozwala również na przesłonięcie globali.
Johannes Ewald

Bardzo interesujące jest szukanie czegoś takiego do użycia w pracy, czy ten moduł wpływa również na moduły niższego szczebla?
akst

dla proxyquire W opisie napisano, że jest używany do testowania: „Wymagają nodejs proxy, aby umożliwić zastąpienie zależności podczas testowania ”. nie dla DI, prawda?
Marwen Trabelsi

17

Właśnie w tym celu zbudowałem elektrolit . Inne dostępne rozwiązania iniekcji zależności były zbyt inwazyjne dla moich upodobań, a bałagan w świecie requirejest moim szczególnym zarzutem.

Electrolyte obejmuje moduły, w szczególności te, które eksportują funkcję „konfiguracji”, jak widać w oprogramowaniu pośrednim Connect / Express. Zasadniczo tego typu moduły są tylko fabrykami dla niektórych zwracanych obiektów.

Na przykład moduł, który tworzy połączenie z bazą danych:

var mysql = require('mysql');

exports = module.exports = function(settings) {
  var connection = mysql.createConnection({
    host: settings.dbHost,
    port: settings.dbPort
  });

  connection.connect(function(err) {
    if (err) { throw err; }
  });

  return connection;
}

exports['@singleton'] = true;
exports['@require'] = [ 'settings' ];

Na dole są adnotacje , dodatkowy kawałek metadanych, które Electrolyte wykorzystuje do tworzenia instancji i wstrzykiwania zależności, automatycznie łącząc ze sobą elementy aplikacji.

Aby utworzyć połączenie z bazą danych:

var db = electrolyte.create('database');

Elektrolit przechodzi tranzytowo @requirezależności i wprowadza instancje jako argumenty do eksportowanej funkcji.

Kluczem jest to, że jest to minimalnie inwazyjne. Ten moduł jest całkowicie użyteczny, niezależnie od samego elektrolitu. Oznacza to, że twoje testy jednostkowe mogą przetestować tylko testowany moduł , przekazując pozorowane obiekty bez potrzeby dodatkowych zależności w celu ponownego połączenia wewnętrznych.

Podczas uruchamiania pełnej aplikacji Electrolyte wkracza na poziomie między modułami, łącząc elementy bez potrzeby globalizacji, singletonów lub nadmiernej hydrauliki.


1
Czy wyjaśniłbyś, co dzieje się w kodzie, który opublikowałeś, gdy zadzwonił telefon connect()? Chociaż nie jestem zaznajomiony z MySql API dla Węzła, oczekiwałbym, że to wywołanie będzie asynchroniczne, więc ilustracja nie jest do końca jasna.
Andrey Agibalov

obecnie używa elektrolitu. Twierdzisz, że moduły INJECT można łatwo eksportować za pomocą eksportu [„@ wymagają”]. Ale jeśli muszę zepsuć jeden z wymaganych modułów, w jaki sposób można to osiągnąć w elektrolicie. Obecnie, jeśli potrzebujemy modułów, można to łatwo osiągnąć. Ale w przypadku elektrolitu jest to ogromny minus ... Czy masz przykłady, w których możemy użyć zaległej wersji modułów i przekazać ją dynamicznie podczas tworzenia instancji / ioc.use z przypadków testowych. Tak więc w zasadzie w teście jednostkowym, jeśli moglibyśmy zrobić ioc.create („nazwa modulatora”), a następnie wykonać wstrzyknięcie modułów zależnych (ale skróconych) byłoby idealne ...
user1102171

1
Nie zadzwoniłbyś ioc.createz testu jednostkowego. Test jednostkowy powinien testować tylko testowany moduł i nie powinien wprowadzać innych zależności, w tym elektrolitu. Postępując zgodnie z tą radą, zrobiłbyśobjToTest = require('modulename')(mockObj1, mockObj2);
Jared Hanson,

8

Sam na to spojrzałem. Nie podoba mi się wprowadzanie bibliotek narzędzi zależnych od magii, które zapewniają mechanizmy przejmowania importu moich modułów. Zamiast tego opracowałem „wytyczne projektowe” dla mojego zespołu, aby raczej wyraźnie określić, jakie zależności można wyśmiewać, wprowadzając eksport funkcji fabrycznych w moich modułach.

W dużym stopniu korzystam z funkcji ES6 w zakresie parametrów i destrukcji, aby uniknąć jakiejś płyty kotłowej i zapewnić nazwany mechanizm zastępowania zależności.

Oto przykład:

import foo from './utils/foo';
import bob from './utils/bob';

// We export a factory which accepts our dependencies.
export const factory = (dependencies = {}) => {
  const {
    // The 'bob' dependency.  We default to the standard 'bob' imp if not provided.
    $bob = bob, 
    // Instead of exposing the whole 'foo' api, we only provide a mechanism
    // with which to override the specific part of foo we care about.
    $doSomething = foo.doSomething // defaults to standard imp if none provided.
  } = dependencies;  

  return function bar() {
    return $bob($doSomething());
  }
}

// The default implementation, which would end up using default deps.
export default factory();

A oto przykład jego użycia

import { factory } from './bar';

const underTest = factory({ $bob: () => 'BOB!' }); // only override bob!
const result = underTest();

Przepraszam za składnię ES6 dla tych, którzy jej nie znają.


Naprawdę bardzo pomysłowy!
Arnold

4

Niedawno sprawdziłem ten wątek z tego samego powodu, co OP - większość bibliotek, które napotkałem, tymczasowo przepisuje instrukcję request. Z tą metodą miałem mieszane stopnie sukcesu, więc skończyło się na następującym podejściu.

W kontekście aplikacji ekspresowej - zawijam app.js w pliku bootstrap.js:

var path = require('path');
var myapp = require('./app.js');

var loader = require('./server/services/loader.js');

// give the loader the root directory
// and an object mapping module names 
// to paths relative to that root
loader.init(path.normalize(__dirname), require('./server/config/loader.js')); 

myapp.start();

Mapa obiektów przekazana do modułu ładującego wygląda następująco:

// live loader config
module.exports = {
    'dataBaseService': '/lib/dataBaseService.js'
}

// test loader config
module.exports = {
    'dataBaseService': '/mocks/dataBaseService.js'
    'otherService' : {other: 'service'} // takes objects too...
};

Wtedy zamiast bezpośredniego połączenia wymagają ...

var myDatabaseService = loader.load('dataBaseService');

Jeśli w module ładującym nie ma aliasu - domyślnie będzie to zwykłe wymaganie. Ma to dwie zalety: mogę zamieniać w dowolnej wersji klasy i eliminuje to konieczność używania względnych nazw ścieżek w całej aplikacji (więc jeśli potrzebuję niestandardowej biblioteki poniżej lub powyżej bieżącego pliku, nie muszę przechodzić , i polecenie spowoduje buforowanie modułu pod tym samym kluczem). Pozwala mi również określić symulacje w dowolnym momencie aplikacji, a nie w bezpośrednim pakiecie testowym.

Właśnie dla wygody opublikowałem mały moduł npm:

https://npmjs.org/package/nodejs-simple-loader


3

W rzeczywistości możesz przetestować plik node.js bez kontenera IoC, ponieważ JavaScript jest naprawdę dynamicznym językiem programowania i możesz modyfikować prawie wszystko w czasie wykonywania.

Rozważ następujące:

import UserRepository from "./dal/user_repository";

class UserController {
    constructor() {
        this._repository = new UserRepository();
    }
    getUsers() {
        this._repository.getAll();
    }
}

export default UserController;

Możesz więc zastąpić sprzężenie między komponentami w czasie wykonywania. Lubię myśleć, że powinniśmy dążyć do oddzielenia naszych modułów JavaScript.

Jedynym sposobem na osiągnięcie rzeczywistego oddzielenia jest usunięcie odniesienia do UserRepository:

class UserController {
    constructor(userRepository) {
        this._repository = userRepository;
    }
    getUsers() {
        this._repository.getAll();
    }
}

export default UserController;

Oznacza to, że gdzieś indziej będziesz musiał zrobić kompozycję obiektu:

import UserRepository from "./dal/user_repository";
import UserController from "./dal/user_controller";

export default new UserController(new UserRepository());

Podoba mi się pomysł przekazania kompozycji obiektu do kontenera IoC. Możesz dowiedzieć się więcej o tym pomyśle w artykule Aktualny stan inwersji zależności w JavaScript . Artykuł próbuje obalić niektóre „mity o kontenerach IoC JavaScript”:

Mit 1: W JavaScript nie ma miejsca na kontenery IoC

Mit 2: Nie potrzebujemy kontenerów IoC, mamy już moduły ładujące!

Mit 3: Odwrócenie zależności === wstrzykiwanie zależności

Jeśli podoba Ci się pomysł użycia kontenera IoC, możesz zajrzeć do InversifyJS. Najnowsza wersja (2.0.0) obsługuje wiele przypadków użycia:

  • Moduły jądra
  • Oprogramowanie pośrednie jądra
  • Użyj klas, literałów łańcuchowych lub symboli jako identyfikatorów zależności
  • Wtrysk stałych wartości
  • Iniekcja konstruktorów klas
  • Wtrysk fabryk
  • Fabryka samochodów
  • Iniekcja dostawców (fabryka asynchroniczna)
  • Procedury obsługi aktywacji (używane do wstrzykiwania serwerów proxy)
  • Wielokrotne zastrzyki
  • Oznaczone wiązania
  • Niestandardowe dekoratory tagów
  • Nazwane wiązania
  • Wiązania kontekstowe
  • Przyjazne wyjątki (np. Zależności cykliczne)

Możesz dowiedzieć się więcej na ten temat w InversifyJS .


2

Dla ES6 opracowałem ten kontener https://github.com/zazoomauro/node-dependency-injection

import {ContainerBuilder} from 'node-dependency-injection'

let container = new ContainerBuilder()
container.register('mailer', 'Mailer')

Następnie możesz ustawić na przykład wybór transportu w kontenerze:

import {ContainerBuilder} from 'node-dependency-injection'

let container = new ContainerBuilder()
container
  .register('mailer', 'Mailer')
  .addArgument('sendmail')

Ta klasa jest teraz znacznie bardziej elastyczna, ponieważ oddzieliłeś wybór transportu od implementacji do kontenera.

Teraz, gdy usługa pocztowa znajduje się w kontenerze, możesz wstrzyknąć ją jako zależność od innych klas. Jeśli masz taką klasę NewsletterManager:

class NewsletterManager {
    construct (mailer, fs) {
        this._mailer = mailer
        this._fs = fs
    }
}

export default NewsletterManager

Podczas definiowania usługi newsletter_manager usługa pocztowa jeszcze nie istnieje. Użyj klasy referencyjnej, aby powiedzieć kontenerowi, aby wstrzyknął usługę pocztową podczas inicjowania menedżera biuletynu:

import {ContainerBuilder, Reference, PackageReference} from 'node-dependency-injection'
import Mailer from './Mailer'
import NewsletterManager from './NewsletterManager'

let container = new ContainerBuilder()

container
  .register('mailer', Mailer)
  .addArgument('sendmail')

container
  .register('newsletter_manager', NewsletterManager)
  .addArgument(new Reference('mailer'))
  .addArgument(new PackageReference('fs-extra'))

Możesz również skonfigurować kontener z plikami konfiguracyjnymi, takimi jak pliki Yaml, Json lub JS

Kontener serwisowy można skompilować z różnych powodów. Przyczyny te obejmują sprawdzanie potencjalnych problemów, takich jak odwołania cykliczne i zwiększanie wydajności kontenera.

container.compile()

1

To zależy od projektu twojej aplikacji. Możesz oczywiście wykonać zastrzyk podobny do java, w którym tworzysz obiekt klasy z zależnością przekazywaną w konstruktorze w ten sposób.

function Cache(store) {
   this._store = store;
}

var cache = new Cache(mysqlStore);

Jeśli nie robisz OOP w javascript, możesz utworzyć funkcję init, która wszystko konfiguruje.

Istnieje jednak inne podejście, które można zastosować częściej w systemie opartym na zdarzeniach, takim jak node.js. Jeśli możesz wymodelować swoją aplikację, aby działała (tylko przez większość czasu) na zdarzeniach, wszystko, co musisz zrobić, to skonfigurować wszystko (co zwykle robię przez wywołanie funkcji init) i emitować zdarzenia z kodu pośredniczącego. To sprawia, że ​​testowanie jest łatwiejsze i czytelniejsze.


Dziękuję za odpowiedź, ale nie do końca rozumiem twoją drugą część twojej odpowiedzi.
Erik

1

Zawsze podobała mi się prostota koncepcji IoC - „Nie musisz nic wiedzieć o środowisku, ktoś zadzwoni w razie potrzeby”

Ale wszystkie implementacje IoC, które widziałem, robiły dokładnie odwrotnie - zaśmiecają kod jeszcze większą liczbą rzeczy niż bez niego. Tak więc stworzyłem własny IoC, który działa tak, jak chciałbym - pozostaje ukryty i niewidoczny przez 90% czasu .

Jest używany w frameworku internetowym MonoJS http://monojs.org

Mówię o prostych rzeczach, jak na przykład udostępnianie obiektu połączenia z bazą danych, ale nie znalazłem rozwiązania, które mnie zadowoli.

Robi się to w ten sposób - zarejestruj komponent raz w konfiguracji.

app.register 'db', -> 
  require('mongodb').connect config.dbPath

I używaj go w dowolnym miejscu

app.db.findSomething()

Pełny kod definicji komponentu (z DB Connection i innymi komponentami) można zobaczyć tutaj https://github.com/sinizinairina/mono/blob/master/mono.coffee

Jest to jedyne miejsce, w którym musisz powiedzieć IoC, co ma robić, po czym wszystkie te komponenty zostaną utworzone i okablowane automatycznie i nie będziesz już musiał widzieć kodu aplikacji IoC.

Samo IoC https://github.com/alexeypetrushin/miconjs


6
Chociaż reklamowany jako DI, wygląda to bardziej jak lokalizator usług.
KyorCode

2
Wygląda wspaniale, szkoda, że ​​jest tylko w coffescript
Rafael P. Miranda

1

Myślę, że nadal potrzebujemy Wstrzykiwania zależności w Nodejs, ponieważ rozluźnia on zależności między usługami i sprawia, że ​​aplikacja jest bardziej przejrzysta.

Zainspirowany przez Spring Framework , implementuję również własny moduł do obsługi wstrzykiwania zależności w Nodejs. Mój moduł jest w stanie wykryć code changesi auto reloadusługi bez ponownego uruchomienia aplikacji.

Odwiedź mój projekt na: Buncha - kontener IoC

Dziękuję Ci!



0

Długo pracowałem z .Net, PHP i Javą, dlatego też chciałem mieć wygodne NendJS. Ludzie mówili, że wbudowane DI w NodeJS jest wystarczające, ponieważ możemy go uzyskać za pomocą Module. Ale nie zadowoliło mnie to dobrze. Chciałem zachować moduł nie więcej niż klasę. Dodatkowo chciałem, aby DI miał pełną obsługę zarządzania cyklem życia modułu (moduł singleton, moduł przejściowy itp.), Ale z modułem Node musiałem bardzo często pisać kod manualny. Wreszcie chciałem ułatwić test jednostkowy. Dlatego stworzyłem dla siebie Zastrzyk Zależności.

Jeśli szukasz DI, spróbuj. Można go znaleźć tutaj: https://github.com/robo-creative/nodejs-robo-container . Jest w pełni udokumentowany. Zajmuje się także niektórymi typowymi problemami związanymi z DI i sposobem ich rozwiązywania w sposób OOP. Mam nadzieję, że to pomoże.


Tak, masz rację, biblioteka DI w twoich projektach jest ważna dla dobrych architektur. Jeśli chcesz zobaczyć przypadek użycia dla DI, zobacz readme dla tego repozytorium również bibliotekę DI dla węzła Jems DI .
Francisco Mercedes

-1

Niedawno utworzyłem bibliotekę o nazwie circuitbox, która umożliwia używanie wstrzykiwania zależności w pliku node.js. Wykonuje prawdziwy zastrzyk zależności w porównaniu do wielu bibliotek opartych na wyszukiwaniu zależności, które widziałem. Circuitbox obsługuje również asynchroniczne procedury tworzenia i inicjowania. Poniżej znajduje się przykład:

Załóżmy, że następujący kod znajduje się w pliku o nazwie consoleMessagePrinter.js

'use strict';

// Our console message printer
// deps is injected by circuitbox with the dependencies
function ConsoleMessagePrinter(deps) {
  return {
    print: function () {
      console.log(deps.messageSource.message());
    }
  };
}

module.exports = ConsoleMessagePrinter;

Załóżmy, że w pliku main.js znajduje się następujący element

'use strict';

// our simple message source
// deps is injected by circuitbox with the dependencies
var simpleMessageSource = function (deps) {
  return {
    message: function () {
      return deps.message;
    }
  };
};

// require circuitbox
var circuitbox = require('../lib');

// create a circuitbox
circuitbox.create({
  modules: [
    function (registry) {
      // the message to be used
      registry.for('message').use('This is the message');

      // define the message source
      registry.for('messageSource').use(simpleMessageSource)
        .dependsOn('message');

      // define the message printer - does a module.require internally
      registry.for('messagePrinter').requires('./consoleMessagePrinter')
        .dependsOn('messageSource');
    }
  ]
}).done(function (cbx) {

  // get the message printer and print a message
  cbx.get('messagePrinter').done(function (printer) {
    printer.print();
  }, function (err) {
    console.log('Could not recieve a printer');
    return;
  });

}, function (err) {
  console.log('Could not create circuitbox');
});

Circuitbox pozwala definiować komponenty i deklarować ich zależności jako moduły. Po zainicjowaniu umożliwia odzyskanie komponentu. Circuitbox automatycznie wstrzykuje wszystkie komponenty, których potrzebuje komponent docelowy, i przekazuje go do użycia.

Projekt jest w wersji alfa. Twoje komentarze, pomysły i opinie są mile widziane.

Mam nadzieję, że to pomoże!


-1

Myślę, że inne posty wykonały świetną robotę w argumentach za użyciem DI. Dla mnie powody są

  1. Wstrzykuj zależności bez znajomości ich ścieżek. Oznacza to, że jeśli zmienisz lokalizację modułu na dysku lub zamienisz go na inny, nie musisz dotykać każdego zależnego od niego pliku.

  2. Ułatwia to kpiny z zależności w celu testowania bez konieczności nadpisywania requirefunkcji globalnej w sposób, który działa bez problemów.

  3. Pomaga uporządkować i uzasadnić twoją aplikację jako luźno powiązane moduły.

Ale bardzo ciężko mi było znaleźć środowisko DI, które mój zespół i ja możemy łatwo zastosować. Niedawno zbudowałem framework o nazwie deppie w oparciu o te funkcje

  • Minimalny interfejs API, którego można się nauczyć w kilka minut
  • Nie wymaga dodatkowego kodu / config / adnotacji
  • Bezpośrednie mapowanie jeden do jednego na requiremoduły
  • Można częściowo dostosować do pracy z istniejącym kodem


-1

Node.js wymaga DI tak samo, jak każdej innej platformy. Jeśli budujesz coś dużego, DI ułatwi wyśmiewanie się z zależności twojego kodu i dokładne przetestowanie kodu.

Na przykład moduły warstwy bazy danych nie powinny być po prostu wymagane w modułach kodu biznesowego, ponieważ podczas testowania tych modułów kodu jednostki daos ładuje się i łączy z bazą danych.

Jednym rozwiązaniem byłoby przekazanie zależności jako parametrów modułu:

module.exports = function (dep1, dep2) {
// private methods

   return {
    // public methods
       test: function(){...}
   }
}

W ten sposób zależności można łatwo i naturalnie wyśmiewać, a Ty możesz skupić się na testowaniu kodu, bez korzystania z trudnej biblioteki innej firmy.

Istnieją inne rozwiązania (broadway, architekt itp.), Które mogą ci w tym pomóc. chociaż mogą robić więcej, niż chcesz lub używać więcej bałaganu.


Niemal dzięki naturalnej ewolucji skończyłem tak samo. Podaję zależność jako parametry i świetnie sprawdza się w testowaniu.
munkee,

-1

Opracowałem bibliotekę, która w prosty sposób obsługuje wstrzykiwanie zależności, co zmniejsza kod płyty wzorcowej. Każdy moduł jest zdefiniowany przez unikalną nazwę i funkcję kontrolera. Parametry kontrolera odzwierciedlają zależności modułu.

Czytaj więcej na KlarkJS

Krótki przykład:

KlarkModule(module, 'myModuleName1', function($nodeModule1, myModuleName2) {
    return {
        log: function() { console.log('Hello from module myModuleName1') }
    };
});
  • myModuleName1 to nazwa modułu.
  • $nodeModule1jest biblioteką zewnętrzną z node_module. Nazwa rozwiązuje się na node-module1. Prefiks $oznacza, że ​​jest to moduł zewnętrzny.
  • myModuleName2 to nazwa modułu wewnętrznego.
  • Wartość zwracana przez sterownik jest wykorzystywana z innych wewnętrznych modułów, gdy definiują parametr myModuleName1.

-1

Odkryłem to pytanie, odpowiadając na problem na moim własnym module DI, pytając, dlaczego kiedykolwiek potrzebny jest system DI do programowania w NodeJS.

Odpowiedź wyraźnie dotyczyła odpowiedzi podanych w tym wątku: to zależy. Istnieją kompromisy dla obu podejść, a czytanie odpowiedzi na to pytanie daje dobry ich kształt.

Tak więc prawdziwa odpowiedź na to pytanie powinna być taka, że ​​w niektórych sytuacjach użyłbyś systemu DI, w innych nie.

To powiedziawszy, to, czego chcesz jako programista, to nie powtarzać się i nie wykorzystywać ponownie swoich usług w różnych aplikacjach.

Oznacza to, że powinniśmy napisać usługi gotowe do użycia w systemie DI, ale niepowiązane z bibliotekami DI. Dla mnie oznacza to, że powinniśmy pisać takie usługi:

module.exports = initDBService;

// Tells any DI lib what it expects to find in it context object
// The $inject prop is the de facto standard for DI imo 
initDBService.$inject = ['ENV'];

// Note the context object, imo, a DI tool should bring
// services in a single context object
function initDBService({ ENV }) {
/// actual service code
}

W ten sposób Twoja usługa nie ma znaczenia, jeśli korzystasz z niej z narzędziem DI lub bez niego.


-1

TypeDI jest najsłodszym ze wszystkich wymienionych tutaj, spójrz na ten kod w TypeDI

import "reflect-metadata";
import {Service, Container} from "typedi";

@Service()
class SomeClass {

    someMethod() {
    }

}

let someClass = Container.get(SomeClass);
someClass.someMethod();

Spójrz też na ten kod:

import {Container, Service, Inject} from "typedi";

// somewhere in your global app parameters
Container.set("authorization-token", "RVT9rVjSVN");

@Service()
class UserRepository {

    @Inject("authorization-token")
    authorizationToken: string;

}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.