Come risolvere i problemi di dipendenza circolare una volta per tutte in JavaScript e TypeScript

  1. index.js richiede AbstractNode.js
  2. Il caricatore del modulo inizia a caricare AbstractNode.js e ad eseguire il codice del modulo. La cosa che incontra per la prima volta è un’istruzione require (import) su Leaf
  3. Quindi il caricatore del modulo inizia a caricare il file Leaf.js. Che, a sua volta, inizia richiedendo Abstractnode.js.
  4. AbstractNode.js è già in fase di caricamento e viene immediatamente restituito dalla cache del modulo. Tuttavia, poiché quel modulo non è ancora stato eseguito oltre la prima riga (il require di Leaf), le istruzioni che introducono la classe AbstractNode non sono ancora state eseguite!
  5. Quindi, la classe Leaf tenta di estendersi dal valore undefined, piuttosto che da una classe valida. Che genera l’eccezione di runtime mostrata sopra. BUM!

Fix tentativo 1

Quindi, si scopre che la nostra dipendenza circolare causa un brutto problema. Tuttavia, se guardiamo da vicino è abbastanza facile determinare quale dovrebbe essere l’ordine di caricamento:

  1. Carica la classe AbstractNode prima
  2. Carica la classe Node e Leaf dopo.

In altre parole, definiamo prima la classe AbstractNode, quindi richiediamo Leafe Node. Dovrebbe funzionare, perché Leaf e Node non devono ancora essere noti quando si definisce la classe AbstractNode. Finché sono definiti prima che AbstractNode.from venga chiamato per la prima volta, dovremmo andare bene. Quindi proviamo il seguente cambiamento:

Risulta, ci sono alcuni problemi con questa soluzione:

Innanzitutto, questo è brutto e non scala. In una grande base di codice,ciò comporterà lo spostamento delle importazioni in modo casuale fino a quando le cose non funzioneranno. Che è spesso solo temporaneo, poiché un piccolo refactoring o modifica delle istruzioni di importazione in futuro può regolare sottilmente l’ordine di caricamento del modulo, reintroducendo il problema.

In secondo luogo, se funziona dipende fortemente dal bundler del modulo. Ad esempio, in codesandbox, quando si raggruppa la nostra app con Parcel (o Webpack o Rollup), questa soluzione non funziona. Tuttavia, quando si esegue questo localmente con il Nodo.moduli js e commonJS questa soluzione potrebbe funzionare bene.

Evitando il problema

Quindi, a quanto pare, questo problema non può essere risolto facilmente. Poteva essere evitato? La risposta è sì, ci sono diversi modi per evitare il problema. Prima di tutto, avremmo potuto mantenere il codice in un unico file. Come mostrato nel nostro esempio iniziale, in questo modo possiamo risolvere il problema in quanto fornisce il pieno controllo sull’ordine in cui viene eseguito il codice di inizializzazione del modulo.

In secondo luogo, alcune persone useranno il problema sopra come argomento per fare affermazioni come “Non si dovrebbero usare le classi” o “Non usare l’ereditarietà”. Ma questa è una semplificazione eccessiva del problema. Anche se sono d’accordo sul fatto che i programmatori ricorrono spesso all’ereditarietà troppo rapidamente, per alcuni problemi è semplicemente perfetto e potrebbe produrre grandi benefici in termini di struttura del codice, riutilizzo o prestazioni. Ma soprattutto, questo problema non è limitato all’ereditarietà della classe. Esattamente lo stesso problema può essere introdotto quando si hanno dipendenze circolari tra le variabili del modulo e le funzioni che vengono eseguite durante l’inizializzazione del modulo!

Potremmo riorganizzare il nostro codice in modo tale da suddividere la classe AbstractNode in pezzi più piccoli, in modo che AbstractNode non abbia dipendenze da Node o Leaf. In questa sandbox il metodo from è stato estratto dalla classe AbstractNode e inserito in un file separato. Questo risolve il problema, ma ora il nostro progetto e l’API sono strutturati in modo diverso. Nei grandi progetti potrebbe essere molto difficile determinare come tirare fuori questo trucco, o addirittura impossibile! Immagina ad esempio cosa accadrebbe se il metodo print dipendesse da Node o Leaf nella prossima iterazione della nostra app Bonus

Bonus: un brutto trucco aggiuntivo che ho usato prima: restituisce classi base dalle funzioni e leva la funzione di sollevamento per caricare le cose nel giusto ordine. Non sono nemmeno sicuro di come spiegarlo correttamente.

Il modello di modulo interno per il salvataggio!

Ho combattuto con questo problema in più occasioni in molti progetti Alcuni esempi includono il mio lavoro a Mendix, MobX, MobX-state-tree e diversi progetti personali. Ad un certo punto, alcuni anni fa ho persino scritto uno script per concatenare tutti i file sorgente e cancellare tutte le istruzioni di importazione. Un bundler di moduli poveri solo per ottenere una presa sull’ordine di caricamento del modulo.

Tuttavia, dopo aver risolto questo problema alcune volte, è apparso un modello. Uno che dà il pieno controllo sull’ordine di caricamento del modulo, senza dover ristrutturare il progetto o tirare strani hack! Questo modello funziona perfettamente con tutte le catene di strumenti su cui l’ho provato (Rollup, Webpack, Parcel, Node).

Il punto cruciale di questo modello è introdurre un file index.js e internal.js. Le regole del gioco sono le seguenti:

  1. Il modulo internal.js importa ed esporta tutto da ogni modulo locale nel progetto
  2. Ogni altro modulo nel progetto importa solo dal file internal.js e mai direttamente da altri file nel progetto.
  3. Il file index.js è il punto di ingresso principale e importa ed esporta tutto da internal.js che si desidera esporre al mondo esterno. Si noti che questo passaggio è rilevante solo se si sta pubblicando una libreria utilizzata da altri. Quindi abbiamo saltato questo passaggio nel nostro esempio.

Si noti che le regole di cui sopra si applicano solo alle nostre dipendenze locali. Le importazioni di moduli esterni vengono lasciate così come sono. Non sono coinvolti nei nostri problemi di dipendenza circolare, dopo tutto. Se applichiamo questa strategia alla nostra applicazione demo, il nostro codice sarà simile a questo:

Quando si applica questo modello per la prima volta, potrebbe sembrare molto artificioso. Ma ha alcuni vantaggi molto importanti!

  1. Prima di tutto, abbiamo risolto il nostro problema! Come dimostrato qui la nostra applicazione è felicemente in esecuzione di nuovo.
  2. La ragione per cui questo risolve il nostro problema è: ora abbiamo il pieno controllo sull’ordine di caricamento del modulo. Qualunque sia l’ordine di importazione in internal.js, sarà il nostro ordine di caricamento del modulo. (Potresti voler controllare l’immagine qui sotto, o rileggere la spiegazione dell’ordine del modulo sopra per vedere perché questo è il caso)
  3. Non abbiamo bisogno di applicare refactoring che non vogliamo. Né siamo costretti a usare brutti trucchi, come spostare le istruzioni in fondo al file. Non dobbiamo compromettere l’architettura, l’API o la struttura semantica della nostra base di codice.
  4. Bonus: le istruzioni di importazione diventeranno molto più piccole, poiché importeremo materiale da meno file. Ad esempio AbstractNode.js ha solo l’istruzione import ora, dove ne aveva due prima.
  5. Bonus: conindex.js, abbiamo un’unica fonte di verità, dando un controllo a grana fine su ciò che esponiamo al mondo esterno.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.