オブジェクト指向プログラミングに精通している場合、またはそれを検討し始めたばかりの場合は、おそらく SOLID という頭字語に遭遇したことがあるでしょう。 SOLID は、開発者がクリーンで保守可能、スケーラブルなコードを作成できるように設計された一連の原則を表します。この記事では、依存関係反転原則を表す SOLID の "D" に焦点を当てます。
しかし、詳細に入る前に、まずこれらの原則の背後にある「理由」を理解しましょう。
オブジェクト指向プログラミングでは、通常、アプリケーションをクラスに分割し、それぞれが特定のビジネス ロジックをカプセル化し、他のクラスと対話します。たとえば、ユーザーがショッピング カートに商品を追加できる単純なオンライン ストアを想像してください。このシナリオは、店舗の運営を管理するために連携する複数のクラスでモデル化できます。この例を、依存関係逆転の原則 がシステムの設計をどのように改善できるかを検討するための基礎として考えてみましょう。
class ProductService { getProducts() { return ['product 1', 'product 2', 'product 3']; } } class OrderService { constructor() { this.productService = new ProductService(); } getOrdersForUser() { return this.productService.getProducts(); } } class UserService { constructor() { this.orderService = new OrderService(); } getUserOrders() { return this.orderService.getOrdersForUser(); } }
ご覧のとおり、OrderService や ProductService などの依存関係は、クラス コンストラクター内で緊密に結合されています。この直接的な依存関係により、これらのコンポーネントの置き換えやモック化が困難になり、実装のテストや交換の際に課題が生じます。
Dependency Injection (DI) パターンは、この問題の解決策を提供します。 DI パターンに従うことで、これらの依存関係を分離し、コードをより柔軟でテストしやすくすることができます。 DI を実装するためにコードをリファクタリングする方法は次のとおりです:
class ProductService { getProducts() { return ['product 1', 'product 2', 'product 3']; } } class OrderService { constructor(private productService: ProductService) {} getOrdersForUser() { return this.productService.getProducts(); } } class UserService { constructor(private orderService: OrderService) {} getUserOrders() { return this.orderService.getOrdersForUser(); } } new UserService(new OrderService(new ProductService()));
各サービスのコンストラクターに依存関係を明示的に渡しています。これは正しい方向へのステップではありますが、依然として密結合クラスが得られます。このアプローチにより、柔軟性はわずかに向上しますが、コードをよりモジュール化し、テストしやすくするという根本的な問題には完全には対処できません。
依存関係反転原則 (DiP) は、これをさらに一歩進めて、「何を渡すべきか?」という重要な質問に答えます。この原則は、具体的な実装を渡す代わりに、必要な抽象化、特に、期待されるインターフェイスに一致する依存関係のみを渡す必要があることを示唆しています。
たとえば、製品の配列を返す getProducts メソッドを備えた ProductService クラスを考えてみましょう。 ProductService を特定の実装 (データベースからデータをフェッチするなど) に直接結合する代わりに、さまざまな方法で実装できます。ある実装ではデータベースから製品をフェッチし、別の実装ではテスト用にハードコードされた JSON オブジェクトを返す場合があります。重要なのは、両方の実装が同じインターフェイスを共有し、柔軟性と互換性を確保していることです。
この原則を実践するには、制御の反転 (IoC) と呼ばれるパターンに頼ることがよくあります。 IoC は、依存関係の作成と管理の制御をクラス自体から外部コンポーネントに移す技術です。これは通常、Dependency Injection コンテナーまたは Service Locator を通じて実装されます。Service Locator は、必要な依存関係をリクエストできるレジストリとして機能します。 IoC を使用すると、クラス コンストラクターにハードコーディングすることなく、適切な依存関係を動的に注入できるため、システムがよりモジュール化され、保守が容易になります。
class ProductService { getProducts() { return ['product 1', 'product 2', 'product 3']; } } class OrderService { constructor() { this.productService = new ProductService(); } getOrdersForUser() { return this.productService.getProducts(); } } class UserService { constructor() { this.orderService = new OrderService(); } getUserOrders() { return this.orderService.getOrdersForUser(); } }
ご覧のとおり、依存関係はコンテナー内に登録されているため、必要に応じて依存関係を置換または交換できます。この柔軟性は、コンポーネント間の疎結合を促進するため、重要な利点です。
ただし、このアプローチにはいくつかの欠点があります。依存関係は実行時に解決されるため、何か問題が発生した場合 (依存関係が欠落しているか互換性がない場合など)、実行時エラーが発生する可能性があります。さらに、登録された依存関係が予想されるインターフェイスに厳密に準拠するという保証はなく、微妙な問題が発生する可能性があります。この依存関係解決方法はサービス ロケーター パターンと呼ばれることが多く、実行時解決への依存と依存関係を曖昧にする可能性があるため、多くの場合アンチパターンとみなされます。
制御の反転 (IoC) パターンを実装するための JavaScript で最も人気のあるライブラリの 1 つは、InversifyJS です。これは、依存関係をクリーンなモジュール方式で管理するための堅牢で柔軟なフレームワークを提供します。ただし、InversifyJS にはいくつかの欠点があります。大きな制限の 1 つは、依存関係の設定と管理に必要な定型コードの量です。さらに、多くの場合、特定の方法でアプリケーションを構造化する必要がありますが、これはすべてのプロジェクトに適しているとは限りません。
InversifyJS の代替となるのは、JavaScript および TypeScript アプリケーションの依存関係を管理するための軽量で合理化されたアプローチである Friendly-DI です。これは、Angular や NestJS などのフレームワークの DI システムからインスピレーションを得ていますが、より最小限で冗長ではないように設計されています。
Friendly-DI の主な利点は次のとおりです。
ただし、Friendly-DI は TypeScript 専用に設計されており、使用を開始する前にその依存関係をインストールする必要があることに注意することが重要です。
class ProductService { getProducts() { return ['product 1', 'product 2', 'product 3']; } } class OrderService { constructor() { this.productService = new ProductService(); } getOrdersForUser() { return this.productService.getProducts(); } } class UserService { constructor() { this.orderService = new OrderService(); } getUserOrders() { return this.orderService.getOrdersForUser(); } }
また、tsconfig.json:
も拡張します。
class ProductService { getProducts() { return ['product 1', 'product 2', 'product 3']; } } class OrderService { constructor(private productService: ProductService) {} getOrdersForUser() { return this.productService.getProducts(); } } class UserService { constructor(private orderService: OrderService) {} getUserOrders() { return this.orderService.getOrdersForUser(); } } new UserService(new OrderService(new ProductService()));
上記の例は、Friendly-DI を使用して変更できます。
class ServiceLocator { static #modules = new Map(); static get(moduleName: string) { return ServiceLocator.#modules.get(moduleName); } static set(moduleName: string, exp: never) { ServiceLocator.#modules.set(moduleName, exp); } } class ProductService { getProducts() { return ['product 1', 'product 2', 'product 3']; } } class OrderService { constructor() { const ProductService = ServiceLocator.get('ProductService'); this.productService = new ProductService(); } getOrdersForUser() { return this.productService.getProducts(); } } class UserService { constructor() { const OrderService = ServiceLocator.get('OrderService'); this.orderService = new OrderService(); } getUserOrders() { return this.orderService.getOrdersForUser(); } } ServiceLocator.set('ProductService', ProductService); ServiceLocator.set('OrderService', OrderService); new UserService();
ご覧のとおり、@Injectable() デコレーターを追加しました。これはクラスを注入可能としてマークし、依存関係注入システムの一部であることを示します。このデコレータにより、DI コンテナは、これらのクラスが必要な場所にインスタンス化および注入できることを認識できるようになります。
コンストラクターでクラスを依存関係として宣言する場合、具体的なクラス自体に直接バインドしません。代わりに、インターフェイスの観点から依存関係を定義します。これにより、コードが特定の実装から切り離され、柔軟性が向上し、必要に応じて依存関係を交換したりモックしたりすることが容易になります。
この例では、UserService を App クラスに配置しました。このパターンは合成ルートとして知られています。 構成ルート は、すべての依存関係がアセンブルおよび注入されるアプリケーションの中心的な場所であり、本質的にはアプリケーションの依存関係グラフの「ルート」です。このロジックを 1 か所に保持することで、依存関係がどのように解決され、アプリ全体に挿入されるかをより適切に制御できます。
最後のステップは、App クラスを DI コンテナに登録することです。これにより、コンテナはアプリケーションの起動時にすべての依存関係のライフサイクルと注入を管理できるようになります。
npm i friendly-di reflect-metadata
アプリケーション内のクラスを置き換える必要がある場合は、元のインターフェイスに従ってモッククラスを作成するだけです。
class ProductService { getProducts() { return ['product 1', 'product 2', 'product 3']; } } class OrderService { constructor() { this.productService = new ProductService(); } getOrdersForUser() { return this.productService.getProducts(); } } class UserService { constructor() { this.orderService = new OrderService(); } getUserOrders() { return this.orderService.getOrdersForUser(); } }
次に、replace メソッドを使用して、置換可能なクラスを宣言してクラスをモックします。
class ProductService { getProducts() { return ['product 1', 'product 2', 'product 3']; } } class OrderService { constructor(private productService: ProductService) {} getOrdersForUser() { return this.productService.getProducts(); } } class UserService { constructor(private orderService: OrderService) {} getUserOrders() { return this.orderService.getOrdersForUser(); } } new UserService(new OrderService(new ProductService()));
何度も置き換えることができるフレンドリーな DI:
class ServiceLocator { static #modules = new Map(); static get(moduleName: string) { return ServiceLocator.#modules.get(moduleName); } static set(moduleName: string, exp: never) { ServiceLocator.#modules.set(moduleName, exp); } } class ProductService { getProducts() { return ['product 1', 'product 2', 'product 3']; } } class OrderService { constructor() { const ProductService = ServiceLocator.get('ProductService'); this.productService = new ProductService(); } getOrdersForUser() { return this.productService.getProducts(); } } class UserService { constructor() { const OrderService = ServiceLocator.get('OrderService'); this.orderService = new OrderService(); } getUserOrders() { return this.orderService.getOrdersForUser(); } } ServiceLocator.set('ProductService', ProductService); ServiceLocator.set('OrderService', OrderService); new UserService();
以上です。このトピックに関してコメントや説明がある場合は、コメント欄に感想を書き込んでください。
以上が依存関係反転の原則をマスターする: DI を使用したクリーンなコードのベスト プラクティスの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。