Wie man fiese zirkuläre Abhängigkeitsprobleme ein für alle Mal in JavaScript & TypeScript behebt

  1. index.js benötigt AbstractNode.js
  2. Der Modullader beginnt mit dem Laden von AbstractNode.js und dem Ausführen des Modulcodes. Das, worauf es zuerst stößt, ist eine require (import) Anweisung an Leaf
  3. , sodass der Modullader beginnt, die Leaf.js Datei zu laden. Was wiederum mit der Anforderung von Abstractnode.js beginnt.
  4. AbstractNode.js wird bereits geladen und sofort aus dem Modulcache zurückgegeben. Da dieses Modul jedoch noch nicht über die erste Zeile hinausgelaufen ist (die Anforderung von Leaf), wurden die Anweisungen zur Einführung der Klasse AbstractNode noch nicht ausgeführt!
  5. Die Klasse Leaf versucht also, vom Wert undefined zu erweitern, anstatt von einer gültigen Klasse. Was die oben gezeigte Laufzeitausnahme auslöst. BUMM!

Fix Versuch 1

Es stellt sich also heraus, dass unsere zirkuläre Abhängigkeit ein unangenehmes Problem verursacht. Wenn wir jedoch genau hinschauen, ist es ziemlich einfach zu bestimmen, wie die Ladereihenfolge aussehen soll:

  1. Laden Sie zuerst die AbstractNode Klasse
  2. Laden Sie danach die Node und Leaf Klasse.

Mit anderen Worten, definieren wir zuerst die AbstractNode -Klasse und lassen Sie sie dann Leaf und Node benötigen. Das sollte funktionieren, da Leaf und Node bei der Definition der Klasse AbstractNode noch nicht bekannt sein müssen. Solange sie definiert sind, bevor AbstractNode.from zum ersten Mal aufgerufen wird, sollten wir in Ordnung sein. Versuchen wir also die folgende Änderung:

Es stellt sich heraus, dass es einige Probleme mit dieser Lösung gibt:

Erstens ist dies hässlich und skaliert nicht. In einer großen Codebasis führt dies dazu, dass Importe zufällig verschoben werden, bis Dinge einfach funktionieren. Was oft nur vorübergehend ist, da ein kleines Refactoring oder eine Änderung der Importanweisungen in der Zukunft die Reihenfolge des Modulladens subtil anpassen und das Problem wieder einführen kann.

Zweitens hängt es stark vom Modulbündler ab, ob dies funktioniert. In Codesandbox funktioniert diese Lösung beispielsweise nicht, wenn Sie unsere App mit Parcel (oder Webpack oder Rollup) bündeln. Wenn Sie dies jedoch lokal mit Node ausführen.js- und CommonJS-Module Diese Problemumgehung funktioniert möglicherweise einwandfrei.

Vermeidung des Problems

Daher kann dieses Problem anscheinend nicht einfach behoben werden. Hätte es vermieden werden können? Die Antwort ist ja, es gibt mehrere Möglichkeiten, das Problem zu vermeiden. Zunächst hätten wir den Code in einer einzigen Datei aufbewahren können. Wie in unserem ersten Beispiel gezeigt, können wir das Problem auf diese Weise lösen, da es die vollständige Kontrolle über die Reihenfolge gibt, in der der Modulinitialisierungscode ausgeführt wird.

Zweitens werden einige Leute das obige Problem als Argument verwenden, um Aussagen wie “Man sollte keine Klassen verwenden” oder “Keine Vererbung verwenden” zu machen. Aber das ist eine übermäßige Vereinfachung des Problems. Obwohl ich zustimme, dass Programmierer oft zu schnell auf Vererbung zurückgreifen, ist es für einige Probleme einfach perfekt und kann große Vorteile in Bezug auf Codestruktur, Wiederverwendung oder Leistung bringen. Am wichtigsten ist jedoch, dass dieses Problem nicht auf die Klassenvererbung beschränkt ist. Genau das gleiche Problem kann auftreten, wenn zirkuläre Abhängigkeiten zwischen Modulvariablen und Funktionen auftreten, die während der Modulinitialisierung ausgeführt werden!

Wir könnten unseren Code so neu organisieren, dass wir die AbstractNode -Klasse in kleinere Teile aufteilen, so dass AbstractNode keine Abhängigkeiten von Node oder Leaf hat. In dieser Sandbox wurde die from -Methode aus der AbstractNode -Klasse gezogen und in eine separate Datei eingefügt. Dies löst das Problem, aber jetzt sind unser Projekt und unsere API anders strukturiert. In großen Projekten kann es sehr schwierig sein zu bestimmen, wie dieser Trick ausgeführt werden soll, oder sogar unmöglich! Stellen Sie sich zum Beispiel vor, was passieren würde, wenn die print -Methode in der nächsten Iteration unserer App von Node oder Leaf abhängen würde …

Bonus: Ein zusätzlicher hässlicher Trick, den ich zuvor verwendet habe: Basisklassen von Funktionen zurückgeben und Funktionshebevorgänge nutzen, um die Dinge in der richtigen Reihenfolge zu laden. Ich bin mir nicht einmal sicher, wie ich es richtig erklären soll.

Das interne Modulmuster zur Rettung!

Ich habe mehrfach mit diesem Problem in vielen Projekten gekämpft, einige Beispiele sind meine Arbeit bei Mendix, MobX, MobX-state-tree und mehrere persönliche Projekte. Irgendwann habe ich vor ein paar Jahren sogar ein Skript geschrieben, um alle Quelldateien zu verketten und alle Importanweisungen zu löschen. Ein schlechter Modulbündler, nur um die Modulladereihenfolge in den Griff zu bekommen.

Nachdem dieses Problem einige Male gelöst wurde, trat jedoch ein Muster auf. Eine, die die volle Kontrolle über die Modulladereihenfolge gibt, ohne das Projekt umstrukturieren oder seltsame Hacks ziehen zu müssen! Dieses Muster funktioniert perfekt mit allen Toolketten, an denen ich es ausprobiert habe (Rollup, Webpack, Parcel, Node).

Der Kern dieses Musters besteht darin, eine index.js und internal.js Datei einzuführen. Die Spielregeln lauten wie folgt:

  1. Das internal.js -Modul importiert und exportiert alles aus jedem lokalen Modul im Projekt
  2. Jedes andere Modul im Projekt importiert nur aus der internal.js -Datei und niemals direkt aus anderen Dateien im Projekt.
  3. Die index.js -Datei ist der Haupteinstiegspunkt und importiert und exportiert alles von internal.js, was Sie der Außenwelt zugänglich machen möchten. Beachten Sie, dass dieser Schritt nur relevant ist, wenn Sie eine Bibliothek veröffentlichen, die von anderen verwendet wird. Also haben wir diesen Schritt in unserem Beispiel übersprungen.

Beachten Sie, dass die obigen Regeln nur für unsere lokalen Abhängigkeiten gelten. Externe Modulimporte bleiben unverändert. Sie sind schließlich nicht an unseren zirkulären Abhängigkeitsproblemen beteiligt. Wenn wir diese Strategie auf unsere Demo-Anwendung anwenden, sieht unser Code folgendermaßen aus:

Wenn Sie dieses Muster zum ersten Mal anwenden, kann es sich sehr künstlich anfühlen. Aber es hat ein paar sehr wichtige Vorteile!

  1. Zunächst haben wir unser Problem gelöst! Wie hier gezeigt, läuft unsere App wieder glücklich.
  2. Der Grund, warum dies unser Problem löst, ist: Wir haben jetzt die volle Kontrolle über die Reihenfolge des Modulladens. Was auch immer die Importreihenfolge in internal.js ist, wird unsere Modulladereihenfolge sein. (Vielleicht möchten Sie das Bild unten überprüfen oder die obige Erklärung zur Modulreihenfolge erneut lesen, um zu sehen, warum dies der Fall ist.)
  3. Wir müssen keine Refactorings anwenden, die wir nicht möchten. Wir sind auch nicht gezwungen, hässliche Tricks anzuwenden, wie das Verschieben von require-Anweisungen an den unteren Rand der Datei. Wir müssen keine Kompromisse bei der Architektur, API oder semantischen Struktur unserer Codebasis eingehen.
  4. Bonus: Importanweisungen werden viel kleiner, da wir Dinge aus less-Dateien importieren. Zum Beispiel hat AbstractNode.js jetzt nur noch eine Importanweisung, wo es zuvor zwei hatte.
  5. Prämie: mitindex.js haben wir eine einzige Quelle der Wahrheit, die eine feinkörnige Kontrolle darüber gibt, was wir der Außenwelt aussetzen.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.