vervelende circulaire afhankelijkheidsproblemen voor eens en altijd oplossen in JavaScript & TypeScript
-
index.js
vereistAbstractNode.js
- de module Lader begint met het laden van
AbstractNode.js
en het uitvoeren van de module code. Wat het eerst tegenkomt is een require (import) statement naarLeaf
- zodat de module loader het
Leaf.js
bestand begint te laden. Die op zijn beurt begint metAbstractnode.js
. -
AbstractNode.js
wordt al geladen en wordt onmiddellijk teruggestuurd uit de module-cache. Echter, omdat die module nog niet voorbij de eerste regel liep (de eis vanLeaf
), zijn de statements die deAbstractNode
klasse introduceerden nog niet uitgevoerd! - dus probeert de klasse
Leaf
uit te breiden van de waardeundefined
in plaats van een geldige klasse. Die gooit de runtime uitzondering hierboven weergegeven. Boem!
Fix poging 1
dus, het blijkt dat onze circulaire afhankelijkheid een vervelend probleem veroorzaakt. Echter, als we goed kijken is het vrij gemakkelijk om te bepalen wat de ladingsvolgorde moet zijn:
- Laad de klasse
AbstractNode
eerste - Laad daarna de klasse
Node
enLeaf
.
met andere woorden, Laten we eerst de AbstractNode
klasse definiëren en dan Leaf
en Node
vereisen. Dat zou moeten werken, omdat Leaf
en Node
nog niet bekend hoeven te zijn bij het definiëren van de klasse AbstractNode
. Zolang ze gedefinieerd zijn voordat AbstractNode.from
voor het eerst wordt aangeroepen, zouden we in orde moeten zijn. Dus laten we de volgende verandering proberen:
blijkt dat er een paar problemen zijn met deze oplossing:
eerst is dit lelijk en schaalt het niet. In een grote code base, dit zal resulteren in het verplaatsen van de invoer willekeurig rond totdat dingen gewoon gebeurt te werken. Dat is vaak slechts tijdelijk, als een kleine refactoring of verandering in import verklaringen in de toekomst kan subtiel aanpassen van de module laadvolgorde, opnieuw het probleem.
ten tweede, of dit werkt in hoge mate afhankelijk is van de module bundler. Bijvoorbeeld, in codesandbox, wanneer het bundelen van onze app met pakket (of Webpack of Rollup), deze oplossing werkt niet. Echter, bij het uitvoeren van dit lokaal met Node.JS en commonJS modules deze oplossing zou kunnen werken prima.
vermijden van het probleem
dus, blijkbaar, dit probleem kan niet gemakkelijk worden opgelost. Had het vermeden kunnen worden? Het antwoord is ja, er zijn verschillende manieren om het probleem te voorkomen. Ten eerste hadden we de code in één bestand kunnen houden. Zoals getoond in ons eerste voorbeeld, kunnen we op die manier het probleem oplossen omdat het volledige controle geeft over de volgorde waarin de module initialisatiecode draait.
ten tweede zullen sommige mensen het bovenstaande probleem gebruiken als argument om uitspraken te doen als “men zou geen klassen moeten gebruiken”, of “gebruik geen overerving”. Maar dat is een te grote vereenvoudiging van het probleem. Hoewel ik het ermee eens ben dat programmeurs vaak te snel hun toevlucht nemen tot overerving, is het voor sommige problemen gewoon perfect en kan het grote voordelen opleveren in termen van code structuur, hergebruik of prestaties. Maar het belangrijkste is dat dit probleem niet beperkt is tot klassenovererving. Precies hetzelfde probleem kan worden geà ntroduceerd wanneer het hebben van circulaire afhankelijkheden tussen module variabelen en functies die draaien tijdens module initialisatie!
we zouden onze code zo kunnen herorganiseren dat we de AbstractNode
klasse in kleinere stukken verdelen, zodat AbstractNode
geen afhankelijkheden heeft op Node
of Leaf
. In deze sandbox is de from
methode uit de AbstractNode
klasse getrokken en in een apart bestand gezet. Dit lost het probleem wel op, maar nu is ons project en API anders gestructureerd. In grote projecten kan het heel moeilijk zijn om te bepalen hoe je deze truc uit te voeren, of zelfs onmogelijk! Stel je bijvoorbeeld voor wat er zou gebeuren als de print
methode afhing van Node
of Leaf
in de volgende iteratie van onze app…
Bonus: een extra lelijke truc die ik eerder gebruikte: retourneer basisklassen van functies en hefboomfunctie hijsen om dingen in de juiste volgorde te laden. Ik weet niet eens hoe ik het goed moet uitleggen.
de interne module patroon te redden!
ik heb meerdere malen met dit probleem gevochten in vele projecten een paar voorbeelden zijn mijn werk bij Mendix, MobX, MobX-state-tree en verschillende persoonlijke projecten. Op een gegeven moment, een paar jaar geleden schreef ik zelfs een script om alle bronbestanden samen te voegen en alle importverklaringen te wissen. Een poor-mans module bundler alleen maar om grip te krijgen op de module laden bestelling.
echter, na het oplossen van dit probleem een paar keer, verscheen een patroon. Een die volledige controle geeft over de module laadvolgorde, zonder het project te herstructureren of rare hacks te trekken! Dit patroon werkt perfect met alle tool-ketens die ik heb geprobeerd het op (Rollup, Webpack, Parcel, Node).
de kern van dit patroon is om een index.js
en internal.js
bestand in te voeren. De regels van het spel zijn als volgt:
- de module
internal.js
importeert en exporteert alles van elke lokale module in het project - elke andere module in het project importeert alleen uit het bestand
internal.js
en nooit rechtstreeks uit andere bestanden in het project. - het bestand
index.js
is het belangrijkste invoerpunt en importeert en exporteert alles vaninternal.js
dat u aan de buitenwereld wilt blootstellen. Merk op dat deze stap alleen relevant is als je een bibliotheek publiceert die door anderen wordt gebruikt. Dus hebben we deze stap in ons voorbeeld overgeslagen.
merk op dat de bovenstaande regels alleen van toepassing zijn op onze lokale afhankelijkheden. Externe module invoer worden gelaten zoals is. Ze zijn immers niet betrokken bij onze circulaire afhankelijkheidsproblemen. Als we deze strategie toepassen op onze demo applicatie, zal onze code er zo uitzien:
wanneer u dit patroon voor de eerste keer toe te passen, kan het erg gekunsteld voelen. Maar het heeft een paar zeer belangrijke voordelen!
- Allereerst hebben we ons probleem opgelost! Zoals hier aangetoond onze app is gelukkig weer actief.
- de reden dat dit ons probleem oplost is: we hebben nu volledige controle over de module laadvolgorde. Wat de importvolgorde in
internal.js
ook is, zal onze module laadvolgorde zijn. (U kunt de afbeelding hieronder bekijken, of de uitleg van de module bestelling hierboven opnieuw lezen om te zien waarom dit het geval is) - we hoeven geen refactorings toe te passen die we niet willen. Ook worden we niet gedwongen om lelijke trucs te gebruiken, zoals het verplaatsen van require statements naar de onderkant van het bestand. We hoeven de architectuur, API of semantische structuur van onze codebasis niet in gevaar te brengen.
- Bonus: import statements zullen veel kleiner worden, omdat we materiaal zullen importeren uit Minder bestanden. Bijvoorbeeld
AbstractNode.js
heeft nu alleen op import statement, waar het er eerder twee had. - Bonus: met
index.js
hebben we één enkele bron van waarheid, die fijnkorrelige controle geeft over wat we aan de buitenwereld blootstellen.