cum să remediați problemele de dependență circulară urâtă o dată pentru totdeauna în JavaScript și TypeScript
-
index.js
necesităAbstractNode.js
- încărcătorul modulului începe încărcarea
AbstractNode.js
și rulează codul modulului. Lucrul pe care îl întâlnește mai întâi este o declarație require (import) laLeaf
- astfel încât încărcătorul modulului începe să încarce fișierul
Leaf.js
. Care, la rândul său, începe prin a solicitaAbstractnode.js
. -
AbstractNode.js
este deja încărcat și este returnat imediat din memoria cache a modulului. Cu toate acestea, deoarece acel modul nu a trecut încă dincolo de prima linie (cerințaLeaf
), declarațiile care introduc clasaAbstractNode
nu au fost încă executate! - deci, clasa
Leaf
încearcă să se extindă de la valoareaundefined
, mai degrabă decât o clasă validă. Care aruncă excepția de rulare prezentată mai sus. Bum!
Fix încercare 1
deci, se pare că dependența noastră circulară provoacă o problemă urât. Cu toate acestea, dacă ne uităm atent, este destul de ușor să determinăm care ar trebui să fie ordinea de încărcare:
- încărcați clasa
AbstractNode
mai întâi - încărcați clasa
Node
șiLeaf
după aceea.
cu alte cuvinte, să definim mai întâi clasa AbstractNode
și apoi să o solicităm Leaf
și Node
. Acest lucru ar trebui să funcționeze, deoarece Leaf
și Node
nu trebuie să fie cunoscute încă la definirea clasei AbstractNode
. Atâta timp cât sunt definite înainte AbstractNode.from
este chemat pentru prima dată, ar trebui să fim bine. Deci, să încercăm următoarea schimbare:
se pare că există câteva probleme cu această soluție:
în primul rând, acest lucru este urât și nu scară. Într-o bază de cod mare, acest lucru va duce la mutarea importurilor la întâmplare până când lucrurile se întâmplă să funcționeze. Ceea ce este adesea doar temporar, deoarece o mică refactorizare sau modificare a declarațiilor de import în viitor poate ajusta subtil ordinea de încărcare a modulului, reintroducând problema.
în al doilea rând, dacă acest lucru funcționează este foarte dependentă de modulul bundler. De exemplu, în codesandbox, atunci când grupați aplicația noastră cu colet (sau Webpack sau Rollup), această soluție nu funcționează. Cu toate acestea, atunci când rulați acest lucru local cu nod.modulele js și commonJS această soluție ar putea funcționa foarte bine.
evitarea problemei
deci, aparent, această problemă nu poate fi rezolvată cu ușurință. Ar fi putut fi evitată? Răspunsul este da, există mai multe modalități de a evita problema. În primul rând, am fi putut păstra codul într-un singur fișier. Așa cum se arată în exemplul nostru inițial, în acest fel putem rezolva problema, deoarece oferă control deplin asupra ordinii în care rulează codul de inițializare a modulului.
în al doilea rând, unii oameni vor folosi problema de mai sus ca argument pentru a face afirmații precum “nu trebuie să folosiți clase” sau “nu folosiți moștenirea”. Dar aceasta este o supra-simplificare a problemei. Deși sunt de acord că programatorii recurg adesea la moștenire prea repede, pentru unele probleme este perfect și ar putea aduce beneficii mari în ceea ce privește structura codului, reutilizarea sau performanța. Dar cel mai important, această problemă nu se limitează la moștenirea clasei. Exact aceeași problemă poate fi introdusă atunci când aveți dependențe circulare între variabilele modulului și funcțiile care rulează în timpul inițializării modulului!
am putea reorganiza codul nostru în așa fel încât să împărțim clasa AbstractNode
în bucăți mai mici, astfel încât AbstractNode
să nu aibă dependențe de Node
sau Leaf
. În acest sandbox from
metoda a fost scos AbstractNode
clasa și a pus într-un fișier separat. Acest lucru rezolvă problema, dar acum proiectul și API-ul nostru sunt structurate diferit. În proiecte mari ar putea fi foarte greu pentru a determina cum de a trage acest truc off, sau chiar imposibil! Imaginați-vă, de exemplu, ce s-ar întâmpla dacă metoda print
depindea de Node
sau Leaf
în următoarea iterație a aplicației noastre…
Bonus: un truc urât suplimentar pe care l-am folosit înainte: returnați clasele de bază din funcții și ridicați funcția de pârghie pentru a încărca lucrurile în ordinea corectă. Nici măcar nu știu cum să explic corect.
modelul modulului intern la salvare!
am luptat cu această problemă în mai multe rânduri în multe proiecte câteva exemple includ munca mea la Mendix, MobX, MobX-State-tree și mai multe proiecte personale. La un moment dat, acum câțiva ani am scris chiar și un script pentru a concatena toate fișierele sursă și a șterge toate declarațiile de import. Un modul poor-mans bundler doar pentru a obține o prindere pe ordinea de încărcare modul.
cu toate acestea, după rezolvarea acestei probleme de câteva ori, a apărut un model. Unul care oferă control deplin asupra comenzii de încărcare a modulului, fără a fi nevoie să restructurați proiectul sau să trageți hack-uri ciudate! Acest model funcționează perfect cu toate lanțurile de instrumente pe care le-am încercat (Rollup, Webpack, Parcel, Node).
esența acestui model este introducerea unui fișier index.js
și internal.js
. Regulile jocului sunt următoarele:
- modulul
internal.js
importă și exportă totul de la fiecare modul local din proiect - orice alt modul din proiect importă numai din fișierul
internal.js
și niciodată direct din alte fișiere din proiect. - fișierul
index.js
este principalul punct de intrare și importă și exportă totul de lainternal.js
pe care doriți să îl expuneți lumii exterioare. Rețineți că acest pas este relevant numai dacă publicați o bibliotecă consumată de alții. Așa că am sărit peste acest pas în exemplul nostru.
rețineți că regulile de mai sus se aplică numai dependențelor noastre locale. Importurile de module externe sunt lăsate așa cum este. Până la urmă, ei nu sunt implicați în problemele noastre circulare de dependență. Dacă aplicăm această strategie aplicației noastre demo, codul nostru va arăta astfel:
când aplicați acest model pentru prima dată, s-ar putea să vă simțiți foarte contrived. Dar are câteva beneficii foarte importante!
- în primul rând, ne-am rezolvat problema! După cum sa demonstrat aici aplicația noastră este fericit rulează din nou.
- motivul pentru care acest lucru rezolvă problema noastră este: acum avem control deplin asupra comenzii de încărcare a modulului. Oricare ar fi ordinea de import în
internal.js
este, va fi comanda noastră de încărcare modul. (S-ar putea să doriți să verificați imaginea de mai jos sau să citiți din nou explicația comenzii modulului de mai sus pentru a vedea de ce este cazul) - nu trebuie să aplicăm refactorări pe care nu le dorim. Nici nu suntem obligați să folosim trucuri urâte, cum ar fi mutarea necesită declarații în partea de jos a fișierului. Nu trebuie să compromitem arhitectura, API-ul sau structura semantică a bazei noastre de cod.
- Bonus: declarațiile de import vor deveni mult mai mici, deoarece vom importa lucruri din mai puține fișiere. De exemplu,
AbstractNode.js
are doar pe declarația de import acum, în cazul în care a avut două înainte. - Bonus: cu
index.js
, avem o singură sursă de adevăr, oferind un control cu granulație fină asupra a ceea ce expunem lumii exterioare.