Viele Node.js-Entwickler verwenden Modul-Abhängigkeiten (ausschließlich) mit require (), um Module zu binden. Es gibt jedoch auch andere Ansätze mit ihren Vor- und Nachteilen. Ich werde in diesem Artikel darüber sprechen. Es werden vier Ansätze betrachtet:
- Harte Abhängigkeiten (require ())
- Abhängigkeitsinjektion
- Service Locator
- Eingebettete Abhängigkeitscontainer (DI-Container)
Ein bisschen über Module
Module und modulare Architektur bilden die Grundlage von Node.js. Module bieten Kapselung (Ausblenden von Implementierungsdetails und Öffnen nur der Schnittstelle mit module.exports), Wiederverwendung von Code und Aufteilen von logischem Code in Dateien. Fast alle Node.js-Anwendungen bestehen aus vielen Modulen, die irgendwie interagieren müssen. Wenn Sie die Module falsch binden oder sogar die Interaktion der Module abdriften lassen, können Sie sehr schnell feststellen, dass die Anwendung „auseinanderfällt“: Änderungen im Code an einer Stelle führen zu einer Panne an einer anderen Stelle, und Unit-Tests werden einfach unmöglich. Idealerweise sollten die Module eine hohe
Konnektivität , aber eine geringe
Kopplung aufweisen .
Harte Sucht
Eine starke Abhängigkeit eines Moduls von einem anderen tritt auf, wenn require () verwendet wird. Dies ist ein effektiver, einfacher und allgemeiner Ansatz. Zum Beispiel möchten wir nur das Modul verbinden, das für die Interaktion mit der Datenbank verantwortlich ist:
Vorteile:
- Einfachheit
- Visuelle Organisation von Modulen
- Einfaches Debuggen
Nachteile:
- Schwierigkeiten bei der Wiederverwendung des Moduls (z. B. wenn wir unser Modul wiederholt verwenden möchten, jedoch mit einer anderen Instanz der Datenbank)
- Schwierigkeit beim Testen von Einheiten (Sie müssen eine Dummy-Datenbankinstanz erstellen und sie irgendwie an das Modul übergeben)
Zusammenfassung:
Der Ansatz eignet sich sowohl für kleine Anwendungen oder Prototypen als auch für den Anschluss zustandsloser Module: Fabriken, Designer und Funktionssätze.
Abhängigkeitsinjektion
Die Hauptidee der Abhängigkeitsinjektion besteht darin, Abhängigkeiten von einer externen Komponente auf das Modul zu übertragen. Dadurch wird die harte Abhängigkeit im Modul beseitigt und es wird möglich, es in verschiedenen Kontexten (z. B. mit verschiedenen Datenbankinstanzen) wiederzuverwenden.
Die Abhängigkeitsinjektion kann durch Übergeben der Abhängigkeit im Konstruktorargument oder durch Festlegen von Moduleigenschaften implementiert werden. In der Praxis ist es jedoch besser, die erste Methode zu verwenden. Wenden wir die Implementierung von Abhängigkeiten in der Praxis an, indem wir eine Instanz der Datenbank mithilfe der Factory erstellen und an unser Modul übergeben:
Externes Modul:
const dbFactory = require('db'); const OurModule = require('./ourModule.js'); const dbInstance = dbFactory.createInstance('instance1'); const ourModule = OurModule(dbInstance);
Jetzt können wir unser Modul nicht nur wiederverwenden, sondern auch einfach einen Komponententest dafür schreiben: Erstellen Sie einfach ein Scheinobjekt für die Datenbankinstanz und übergeben Sie es an das Modul.
Vorteile:
- Einfache Schreibeinheitentests
- Erhöhen Sie die Wiederverwendbarkeit von Modulen
- Vermindertes Engagement, erhöhte Konnektivität
- Verlagerung der Verantwortung für das Erstellen von Abhängigkeiten auf eine höhere Ebene - dies verbessert häufig die Lesbarkeit des Programms, da wichtige Abhängigkeiten an einem Ort gesammelt und nicht nach Modulen verteilt werden
Nachteile:
- Die Notwendigkeit eines gründlicheren Abhängigkeitsentwurfs: Beispielsweise muss eine bestimmte Reihenfolge der Modulinitialisierung eingehalten werden
- Die Komplexität des Abhängigkeitsmanagements, insbesondere wenn es viele gibt
- Verschlechterung der Verständlichkeit des Modulcodes: Das Schreiben von Modulcode, wenn eine Abhängigkeit von außen kommt, ist schwieriger, da wir diese Abhängigkeit nicht direkt betrachten können.
Zusammenfassung:
Die Abhängigkeitsinjektion erhöht die Komplexität und Größe der Anwendung, ermöglicht jedoch im Gegenzug die Wiederverwendung und erleichtert das Testen. Der Entwickler sollte entscheiden, was in einem bestimmten Fall für ihn wichtiger ist - die Einfachheit einer harten Abhängigkeit oder die breiteren Möglichkeiten, eine Abhängigkeit einzuführen.
Service Locator
Die Idee ist, eine Abhängigkeitsregistrierung zu haben, die als Vermittler beim Laden einer Abhängigkeit mit einem beliebigen Modul fungiert. Anstelle einer festen Bindung werden vom Modul Abhängigkeiten vom Service Locator angefordert. Offensichtlich haben die Module eine neue Abhängigkeit - den Service Locator selbst. Ein Beispiel für einen Service Locator ist das Node.js-Modulsystem: Module fordern mit require () eine Abhängigkeit an. Im folgenden Beispiel erstellen wir einen Service Locator, registrieren Datenbankinstanzen und unser Modul darin.
Externes Modul:
const serviceLocator = require('./serviceLocator.js')(); serviceLocator.register('someParameter', 'someValue'); serviceLocator.factory('db', require('db')); serviceLocator.factory('ourModule', require('ourModule')); const ourModule = serviceLocator.get('ourModule');
Unser Modul:
Es sollte beachtet werden, dass der Service Locator Servicefabriken anstelle von Instanzen speichert, und das ist sinnvoll. Wir haben die Vorteile einer verzögerten Initialisierung und müssen uns jetzt nicht mehr um die Initialisierungsreihenfolge der Module kümmern - alle Module werden bei Bedarf initialisiert. Außerdem hatten wir die Möglichkeit, Parameter im Service Locator zu speichern (siehe "someParameter").
Vorteile:
- Einfache Schreibeinheitentests
- Die Wiederverwendung eines Moduls ist einfacher als bei einer harten Sucht
- Reduziertes Engagement, erhöhte Konnektivität im Vergleich zu harter Sucht
- Verlagerung der Verantwortung für die Erstellung von Abhängigkeiten auf eine höhere Ebene
- Die Reihenfolge der Modulinitialisierung muss nicht eingehalten werden
Nachteile:
- Die Wiederverwendung eines Moduls ist schwieriger als die Implementierung einer Abhängigkeit (aufgrund der zusätzlichen Abhängigkeit des Service Locator).
- Lesbarkeit: Es ist noch schwieriger zu verstehen, was die vom Service Locator geforderte Abhängigkeit bewirkt
- Erhöhtes Engagement im Vergleich zur Abhängigkeitsinjektion
Zusammenfassung
Im Allgemeinen ähnelt ein Service Locator der Abhängigkeitsinjektion. In mancher Hinsicht ist dies einfacher (es gibt keine Initialisierungsreihenfolge), in einigen Fällen ist es schwieriger (weniger als die Möglichkeit, Code wiederzuverwenden).
Eingebettete Abhängigkeitscontainer (DI-Container)
Der Service Locator hat einen Nachteil, aufgrund dessen er in der Praxis selten angewendet wird - die Abhängigkeit der Module vom Locator selbst. Eingebettete Abhängigkeitscontainer (DI-Container) haben diesen Nachteil nicht. Tatsächlich ist dies derselbe Service Locator mit einer zusätzlichen Funktion, die die Abhängigkeiten des Moduls vor dem Erstellen seiner Instanz ermittelt. Sie können Modulabhängigkeiten ermitteln, indem Sie Argumente aus dem Modulkonstruktor analysieren und extrahieren (in JavaScript können Sie mit toString () einen Link zu einer Funktion in eine Zeichenfolge umwandeln). Diese Methode eignet sich, wenn die Entwicklung ausschließlich für den Server erfolgt. Wenn Client-Code geschrieben wird, wird er häufig minimiert und es ist sinnlos, die Namen der Argumente zu extrahieren. In diesem Fall kann die Liste der Abhängigkeiten als Array von Zeichenfolgen übergeben werden (in Angular.js wird dieser Ansatz basierend auf der Verwendung von DI-Containern verwendet). Wir implementieren den DI-Container mithilfe der Analyse von Konstruktorargumenten:
const fnArgs = require('parse-fn-args'); module.exports = function() { const dependencies = {}; const factories = {}; const diContainer = {}; diContainer.factory = (name, factory) => { factories[name] = factory; }; diContainer.register = (name, dep) => { dependencies[name] = dep; }; diContainer.get = (name) => { if(!dependencies[name]) { const factory = factories[name]; dependencies[name] = factory && diContainer.inject(factory); if(!dependencies[name]) { throw new Error('Cannot find module: ' + name); } } diContainer.inject = (factory) => { const args = fnArgs(factory) .map(dependency => diContainer.get(dependency)); return factory.apply(null, args); } return dependencies[name]; };
Im Vergleich zum Service Locator wurde die Inject-Methode hinzugefügt, mit der die Abhängigkeiten des Moduls vor dem Erstellen seiner Instanz ermittelt werden. Der externe Modulcode hat sich nicht wesentlich geändert:
const diContainer = require('./diContainer.js')(); diContainer.register('someParameter', 'someValue'); diContainer.factory('db', require('db')); diContainer.factory('ourModule', require('ourModule')); const ourModule = diContainer.get('ourModule');
Unser Modul sieht genauso aus wie bei einer einfachen Abhängigkeitsinjektion:
Jetzt kann unser Modul sowohl mit Hilfe eines DI-Containers aufgerufen werden als auch die erforderlichen Instanzen von Abhängigkeiten mithilfe einer einfachen Abhängigkeitsinjektion direkt übergeben werden.
Vorteile:
- Einfache Schreibeinheitentests
- Einfache Wiederverwendung von Modulen
- Reduziertes Engagement, erhöhte Konnektivität der Module (insbesondere im Vergleich zu einem Service Locator)
- Verlagerung der Verantwortung für die Erstellung von Abhängigkeiten auf eine höhere Ebene
- Sie müssen die Modulinitialisierung nicht verfolgen
Das größte Minus:
- Signifikante Komplikation der Modulbindungslogik
Zusammenfassung
Dieser Ansatz ist schwieriger zu verstehen und enthält etwas mehr Code, aber aufgrund seiner Kraft und Eleganz lohnt sich die dafür aufgewendete Zeit. In kleinen Projekten kann dieser Ansatz redundant sein, sollte jedoch berücksichtigt werden, wenn eine große Anwendung entworfen wird.
Fazit
Die grundlegenden Ansätze zur Modulbindung in Node.js. wurden berücksichtigt. Wie es normalerweise der Fall ist, gibt es keine „Silberkugel“, aber der Entwickler sollte sich der möglichen Alternativen bewusst sein und für jeden Einzelfall die am besten geeignete Lösung auswählen.
Der Artikel basiert auf einem Kapitel aus dem 2017 veröffentlichten Buch
Node.js Design Patterns . Leider sind viele Dinge in dem Buch bereits veraltet, sodass ich es nicht zu 100% empfehlen kann, es zu lesen, aber einige Dinge sind heute noch relevant.