JavaScriptとTypeScriptで厄介な循環依存関係の問題を一度に修正するにはどうすればよいですか
-
index.js
AbstractNode.js
- が必要ですモジュールローダーは
AbstractNode.js
のロードとモジュールコードの実行を開始します。 最初に遭遇するのは、Leaf
- へのrequire(import)文であるため、モジュールローダーは
Leaf.js
ファイルのロードを開始します。 これは、次にAbstractnode.js
を要求することから始まります。 AbstractNode.js
はすでにロードされており、モジュールキャッシュからすぐに返されています。 しかし、そのモジュールはまだ最初の行(Leaf
のrequire)を超えて実行されていないため、AbstractNode
クラスを導入するステートメントはまだ実行されていません!したがって、Leaf
クラスは有効なクラスではなくundefined
値から拡張しようとします。 これは、上記の実行時例外をスローします。 ブーム!
fix attempt1
だから、循環依存関係が厄介な問題を引き起こすことが判明しました。 しかし、よく見ると、読み込み順序がどうあるべきかを判断するのは非常に簡単です:
- 最初に
AbstractNode
クラスをロードします - その後に
Node
とLeaf
クラスをロードします。つまり、最初にAbstractNode
クラスを定義してから、Leaf
とNode
を必要としましょう。 これは、Leaf
とNode
がAbstractNode
クラスを定義するときにまだ知られていなければならないため、うまくいくはずです。 それらがAbstractNode.from
が初めて呼び出される前に定義されている限り、私たちはうまくいくはずです。 それでは、次の変更を試してみましょう:
結局のところ、この解決策にはいくつかの問題があります:
まず、これは醜いですし、スケールしません。 大規模なコードベースでは、これは物事がちょうど動作するように起こるまで、ランダムにインポートを移動することになります。 これは、将来のimport文の小さなリファクタリングや変更が微妙に問題を再導入し、モジュールの読み込み順序を調整することができるので、多くの場合、一
第二に、これが動作するかどうかはモジュールバンドラに大きく依存しています。 たとえば、codesandboxでは、アプリをParcel(またはWebpackまたはRollup)にバンドルすると、このソリューションは機能しません。 ただし、これをNodeでローカルに実行する場合。jsおよびcommonJSモジュールこの回避策は正常に動作する可能性があります。
問題を回避する
だから、明らかに、この問題は簡単に修正することはできません。 それは回避されている可能性がありますか? 答えはイエスです、問題を回避するにはいくつかの方法があります。 まず第一に、コードを単一のファイルに保持することができました。 最初の例に示されているように、モジュール初期化コードが実行される順序を完全に制御できるため、問題を解決できます。第二に、上記の問題を引数として使用して、「クラスを使用すべきではない」、「継承を使用しない」などの文を作成する人もいます。 しかし、それは問題の過度の単純化です。 私はプログラマがしばしばあまりにも早く継承に頼ることに同意しますが、いくつかの問題については完璧であり、コード構造、再利用、またはパフォーマ しかし、最も重要なのは、この問題はクラス継承に限定されるものではありません。 モジュールの初期化中に実行されるモジュール変数と関数の間に循環依存関係がある場合、まったく同じ問題が発生する可能性があります!
AbstractNode
はNode
またはLeaf
に依存しないように、AbstractNode
クラスを小さな部分に分割するようにコードを再編成することができます。 このサンドボックスでは、from
メソッドがAbstractNode
クラスから取り出され、別のファイルに配置されています。 これで問題は解決しますが、現在はプロジェクトとAPIの構造が異なります。 大規模なプロジェクトでは、このトリックをやってのける方法を決定することは非常に難しいかもしれない、あるいは不可能! たとえば、アプリの次の反復でprint
メソッドがNode
またはLeaf
に依存した場合に何が起こるかを想像してみてください…ボーナス:私が前に使用した追加の醜い 私はそれを適切に説明する方法さえわかりません。
内部モジュールパターンを救助に!いくつかの例には、Mendix、MobX、MobX-state-tree、およびいくつかの個人的なプロジェクトでの私の仕事が含まれます。 ある時点で、数年前、私はすべてのソースファイルを連結し、すべてのimport文を消去するスクリプトを書いていました。 モジュールの読み込み順序を把握するための貧しい人のモジュールのバンドルラー。
しかし、この問題を数回解決した後、パターンが現れました。 プロジェクトを再構築したり、奇妙なハックを引っ張ったりすることなく、モジュールのロード順序を完全に制御できるもの! このパターンは、私が試したすべてのツールチェーン(Rollup、Webpack、Parcel、Node)で完璧に動作します。
このパターンの要点は、
index.js
とinternal.js
ファイルを導入することです。 ゲームのルールは次のとおりです:internal.js
モジュールは、プロジェクト内のすべてのローカルモジュールからすべてをインポートおよびエクスポートします- プロジェクト内の他のすべてのモジュールは、
internal.js
ファイルからのみインポートし、プロジェクト内の他のファイルから直接インポートすることはありません。 index.js
ファイルはメインエントリポイントであり、internal.js
から外の世界に公開したいすべてをインポートおよびエクスポートします。 この手順は、他のユーザーが使用しているライブラリを公開している場合にのみ関連することに注意してください。 そのため、この例ではこの手順をスキップしました。
上記のルールはローカルの依存関係にのみ適用されることに注意してください。 外部モジュールのインポートはそのままにしておきます。 結局のところ、それらは私たちの循環依存関係の問題には関与していません。 この戦略をデモアプリケーションに適用すると、コードは次のようになります:
このパターンを初めて適用すると、非常に不自然に感じるかもしれません。 しかし、それはいくつかの非常に重要な利点があります!
- まず、私たちは問題を解決しました! ここで示したように、我々のアプリは喜んで再び実行されています。
- これが私たちの問題を解決する理由は、モジュールの読み込み順序を完全に制御できるようになったことです。
internal.js
のインポート順序が何であれ、モジュールのロード順序になります。 (下の図を確認するか、上記のモジュール順序の説明を再読して、これが当てはまる理由を確認することができます) - 不要なリファクタリングを適用す また、require文をファイルの一番下に移動するような醜いトリックを使用することも強制されません。 私たちは、コードベースのアーキテクチャ、API、または意味構造を妥協する必要はありません。
- ボーナス:インポートステートメントは、より少ないファイルからものをインポートするため、はるかに小さくなります。 たとえば、
AbstractNode.js
にはon import文のみがあり、以前は2つありました。 - ボーナス:
index.js
では、私たちには単一の真実の源があり、私たちが外の世界に公開するものをきめ細かく制御します。