Sådan løses grimme cirkulære afhængighedsproblemer en gang for alle i JavaScript & TypeScript
-
index.js
kræverAbstractNode.js
- modulindlæseren begynder at indlæse
AbstractNode.js
og køre modulkoden. Den ting, den først møder, er en krav (import) erklæring tilLeaf
- så modullæseren begynder at indlæse
Leaf.js
filen. Hvilket igen starter med at kræveAbstractnode.js
. -
AbstractNode.js
indlæses allerede og returneres straks fra modulcachen. Da dette modul endnu ikke løb ud over den første linje (kravet omLeaf
), er udsagnene, der introducererAbstractNode
– klassen, endnu ikke udført! - så klassen
Leaf
forsøger at strække sig fra værdienundefined
snarere end en gyldig klasse. Hvilket kaster runtime-undtagelsen vist ovenfor. BOOM!
Rettelsesforsøg 1
så det viser sig, at vores cirkulære afhængighed forårsager et grimt problem. Men hvis vi ser tæt på, er det ret nemt at bestemme, hvad lastningsordren skal være:
- Indlæs
AbstractNode
klassen først - Indlæs
Node
ogLeaf
klassen efter det.
med andre ord, lad os definere AbstractNode
klassen først, og så har den brug for Leaf
og Node
. Det burde virke, fordi Leaf
og Node
ikke behøver at være kendt endnu, når man definerer AbstractNode
klassen. Så længe de er defineret før AbstractNode.from
kaldes for første gang, skal vi have det godt. Så lad os prøve følgende ændring:
viser sig, der er et par problemer med denne løsning:
for det første er dette grimt og skaleres ikke. I en stor kodebase vil dette resultere i at flytte importen tilfældigt rundt, indtil ting bare sker for at arbejde. Hvilket ofte kun er midlertidigt, da en lille refactoring eller ændring i importopgørelser i fremtiden subtilt kan justere modulindlæsningsrækkefølgen og genindføre problemet.
for det andet, om dette virker er meget afhængig af modulet bundler. Når du f.eks. pakker vores app sammen med pakke (eller pakke eller Rollup), fungerer denne løsning ikke. Men når du kører dette lokalt med Node.JS og commonJS moduler denne løsning kan fungere fint.
undgå problemet
så tilsyneladende kan dette problem ikke løses let. Kunne det have været undgået? Svaret er ja, der er flere måder at undgå problemet på. Først og fremmest kunne vi have opbevaret koden i en enkelt fil. Som vist i vores første eksempel kan vi på den måde løse problemet, da det giver fuld kontrol over den rækkefølge, hvor modulinitialiseringskoden kører.
for det andet vil nogle mennesker bruge ovenstående problem som argument for at fremsætte udsagn som “man bør ikke bruge klasser” eller “brug ikke arv”. Men det er en overforenkling af problemet. Selvom jeg er enig i, at programmører ofte ty til arv for hurtigt, for nogle problemer er det bare perfekt og kan give store fordele med hensyn til kodestruktur, genbrug eller ydeevne. Men vigtigst af alt er dette problem ikke begrænset til klassearv. Præcis det samme problem kan introduceres, når der er cirkulære afhængigheder mellem modulvariabler og funktioner, der kører under modulinitialisering!
vi kunne omorganisere vores kode på en sådan måde, at vi opdeler AbstractNode
-klassen i mindre stykker, så AbstractNode
ikke har nogen afhængigheder på Node
eller Leaf
. I denne sandkasse er from
– metoden trukket AbstractNode
– klassen ud og sat i en separat fil. Dette løser problemet, men nu er vores projekt og API struktureret forskelligt. I store projekter kan det være meget svært at bestemme, hvordan man trækker dette trick ud, eller endda umuligt! Forestil dig for eksempel, hvad der ville ske, hvis print
– metoden var afhængig af Node
eller Leaf
i den næste iteration af vores app…
Bonus: et ekstra grimt trick, jeg brugte før: return basisklasser fra funktioner og gearingsfunktion hejsning for at få tingene indlæst i den rigtige rækkefølge. Jeg er ikke engang sikker på, hvordan jeg skal forklare det ordentligt.
det interne modulmønster til redning!
jeg har kæmpet med dette problem ved flere lejligheder på tværs af mange projekter. På et tidspunkt for nogle år siden skrev jeg endda et script til at sammenkæde alle kildefiler og slette alle importopgørelser. En poor-mans modul bundler bare for at få fat i modulet lastning rækkefølge.
men efter at have løst dette problem et par gange, dukkede et mønster op. En, der giver fuld kontrol over modulindlæsningsrækkefølgen uden at skulle omstrukturere projektet eller trække underlige hacks! Dette mønster fungerer perfekt med alle de værktøjskæder, jeg har prøvet det på (Rollup, Pakke, Pakke, Node).
kernen i dette mønster er at indføre en index.js
og internal.js
fil. Reglerne i spillet er som følger:
-
internal.js
modulet både importerer og eksporterer alt fra hvert lokalt modul i projektet - hvert andet modul i projektet importerer kun fra
internal.js
filen og aldrig direkte fra andre filer i projektet. -
index.js
filen er det vigtigste indgangspunkt og importerer og eksporterer alt frainternal.js
, som du vil udsætte for omverdenen. Bemærk, at dette trin kun er relevant, hvis du udgiver et bibliotek, der forbruges af andre. Så vi sprang over dette trin i vores eksempel.
Bemærk, at ovenstående regler kun gælder for vores lokale afhængigheder. Eksterne modul import er tilbage som er. De er trods alt ikke involveret i vores cirkulære afhængighedsproblemer. Hvis vi anvender denne strategi på vores demo-applikation, vil vores kode se sådan ud:
når du anvender dette mønster for første gang, kan det føles meget konstrueret. Men det har et par meget vigtige fordele!
- først og fremmest løste vi vores problem! Som demonstreret her kører vores app heldigvis igen.
- årsagen til, at dette løser vores problem, er: vi har nu fuld kontrol over modulindlæsningsordren. Uanset importordren i
internal.js
er, bliver vores modulindlæsningsordre. (Du vil måske tjekke billedet nedenfor eller genlæse modulordrenforklaringen ovenfor for at se, hvorfor dette er tilfældet) - vi behøver ikke at anvende refactorings, vi ikke ønsker. Vi er heller ikke tvunget til at bruge grimme tricks, som at flytte kræver udsagn til bunden af filen. Vi behøver ikke at gå på kompromis med vores kodebas arkitektur, API eller semantiske struktur.
- Bonus: importopgørelser bliver meget mindre, da vi importerer ting fra færre filer. For eksempel
AbstractNode.js
har kun på import erklæring nu, hvor det havde to før. - Bonus: med
index.js
har vi en enkelt kilde til sandhed, der giver finkornet kontrol over, hvad vi udsætter for omverdenen.