1726072178
2024-09-11 09:18:56
私は何年もの間、依存性注入を使用して依存性を効率的に管理することに熱心でした。この概念に馴染みがなくても心配しないでください。すぐに説明します。
しかし、これは間違いなく、React アプリケーションよりもバックエンド アプリケーションでよく使用されているものです。 この記事ではそれが何なのかを説明します、 フロントエンド React アプリケーションで依存性注入を使用する方法、 そして メリットは何なのか。
まず、DI が一般的に何であるかを説明し、次に React アプリケーションで DI をどのように使用できるかを説明します。
DIに精通している場合はこのセクションをスキップしてください
非常に基本的なレベルでは、 依存性注入(DI)は、コード内で直接ハードコーディングしたりインスタンス化したりするのではなく、変数、オブジェクト、またはサービスをコードに注入できる設計パターンです。。
簡単な例をいくつか挙げて説明します (React の例については後ほど説明します)。
ブログ投稿を保存する関数があれば、次のように記述できます。
import {getDbConnection} from './db' function saveBlogPost(title: string, body: string) { await getDbConnection().save({title, body}) }
しかし、このコードをテストするのは困難です。関数をモック化する必要があります getDbConnection、その実装の詳細を知る必要があります。
また、ブログ投稿を保存する別の方法(たとえば、データベースではなくローカルに保存する)を提供したい場合、柔軟性が低くなります。
DIでは、オブジェクトを渡す(注入する)ことになります、 このような:
// all injected storage services must conform to this shape: interface BlogStorage { save: (post: {title: string, body: string}) => Promisevoid> } function saveBlogPost(blogStorage: BlogStorage, title: string, body: string) { await blogStorage.save({title, body}) } // example implementation class DbBlogStorage implements BlogStorage { constructor() { this.connection = // ... set up db connection } save(title: string, body: string) { await this.connection.save(title, body) } } // tying it all together: const storage: BlogStorage = new DbBlogStorage(); saveBlogPost(storage, "some blog post", "some text here");
当然ですが、この例はまだフロントエンドに固有のものではありません。一見、やり過ぎ、または過剰に設計されているように見えるかもしれませんが、React コンテキストで DI がどのように役立つかを示します。
しかし、今では、 storage それを裏付ける他のものについての議論 BlogStorage インターフェース。これによりテストが容易になります。例:
const mockBlogStorage: BlogStorage = { save: jest.fn() } saveBlogPost(mockBlogStorage, 'title', 'blog') expect(mockBlogStorage).toHaveBeenCalledTimes(1)
Reactのコンテキストでは、依存関係の管理はReactの「制御の反転」を使用して処理できます。 createContext そして useContext。
これを使用すると、プロパティ ドリルダウンなしでコンポーネント ツリーに依存関係を渡すことができ、よりクリーンで保守しやすいコードベースが実現します。
これは、変数やオブジェクトを必要な場所に挿入するための効果的な簡単な方法です。
コンテキストはReactの核となる部分です。すでにご存知かと思いますが、そうでない場合は公式の ドキュメントはこちら
DI を使用した React の例
最初の例は単純な関数で、React に特有のものではありませんでした。ここでは、より現実的で単純な React の例を示します。
「共有」ボタンのあるブログ記事があるとします。共有ボタンをクリックすると、関数が呼び出されます。 sendAnalyticsEvent('share-button-pressed') (そして共有するための URL を表示します)。
import { sendAnalyticsEvent } from './infrastructure/analytics'; function YourBlogPost({ blogPost }: { blogPost: BlogPost }) { const share = () => { sendAnalyticsEvent('share-button-pressed'); // // then show user some url to share alert('Share this URL: http://example.com/' + blogPost.slug); }; return ( div> h1>{blogPost.title}h1> button onClick={share}>Sharebutton> div> ); }
このアプローチの問題は、コンポーネントが分析機能と密接に結合されていることです。
この関数は、分析データをサードパーティの API に送信するために HTTP リクエストを発行する可能性があります。テストでは、この動作によって実際のライブラリ/API 呼び出しがトリガーされることは望ましくありません。
使用する場合 ストーリーブック コンポーネントを個別にプレビューするには、この API 呼び出し動作は必要ありません。現時点では、すべてが関数内に直接ハードコードされているため、操作が非常に面倒です。ただし、このように依存関係を管理することで、Storybook にいくつかのモック分析関数を簡単に挿入できます。
Reactで制御の反転を導入する手順
いくつかの手順を実行したいと思います。
- アダプタインターフェースを作成する必要な機能の契約を定義します(たとえば、次のような文字列を受け入れる関数など)。
share-button-pressed)。 - コンテキストを作成する このアダプタ(依存関係管理コンテナ)を保持します。
- アプリ内でそのコンテキストのプロバイダを使用して、アダプタの具体的な実装を設定します。。
- そしてあなたの
YourBlogPostコンポーネントの依存関係にアクセスする(useContext()ヘルパーフック)を作成して使用します。
少しわかりにくい場合は、次の例を見れば理解できるはずです。
アダプタインターフェースを作成する
注入したいものの形状を記述する型を作成します。
export type SendAnalyticsEvent = (eventType: string) => void;
この形状を受け入れるコンテキストを作成する
次のステップは、依存関係を保持するコンテナを作成し、それを使用するコンポーネントに渡すことです。
// interface for shape of the context interface DIContainerInjectors { sendAnalyticsEvent: SendAnalyticsEvent // references the shape defined above ^ } // create the context (no initial values) const InjectionContainerContext = createContextDIContainerInjectors>() // create a component which can provide the values export const InjectionContainerProvider = (props: {sendAnalyticsEvent: SendAnalyticsEvent}) => { const injectors: DIContainerInjectors = { sendAnalyticsEvent: props.sendAnalyticsEvent // ... and any other services you want to inject in } return InjectionContainerContext.Provider value={injectors}> {props.children} InjectionContainerContext.Provider> } // helper hook to get the injectors export const useInjectedValue = (): DIContainerInjectors => { const ctx = useContext(InjectionContainerContext) if(!ctx) throw new Error('Must use InjectionContainerProvider first') return ctx }
アプリでコンテキストを使用する
実際のアプリでは、コンテナ プロバイダーを使用します。
この例では直接 YourBlogPost 子としてですが、明らかに任意の (サブ) 子コンポーネントで使用できます。
// import whatever dep you want to inject in here. // this is a simplified example, in reality you might // use a more complex DI service container to manage what gets injected in // but also see the test example which shows a test of YourBlogPost with a mock analytics // dependency injected in. import {sendAnalyticsEvent} from './sendAnalyticsEvent' export function App({Component}) { const someBlogPost = { /* ... */ }; return (InjectionContainerProvider sendAnalyticsEvent={sendAnalyticsEvent}> YourBlogPost blogPost={someBlogPost} /> InjectionContainerProvider>); }
コンポーネントに注入されたオブジェクトを使用する
最後に、コンポーネント内で注入されたオブジェクトを使用できるようになります。
import {sendAnalyticsEvent} from './infrastructure/analytics'; function YourBlogPost({blogPost}: {blogPost: BlogPost}) { const {sendAnalyticsEvent} = useInjectedValue() const share = () => { sendAnalyticsEvent('share-button-pressed') // // we didn't import it directly // then show user some url to share alert('Share this url: http://example.com/' + blogPost.slug) } div> h1>{blogPost.title}h1> button onClick={share}>Sharebutton> div> }
コンポーネントをテストする方法
依存関係管理システムを設定したので、同じ InjectionContainerProvider コンポーネントを使用して、いくつかのモック関数とともにモック関数を挿入できます。
it('should send analytics event', async () => { const mockSendAnalyticsEventFn = jest.fn(); render( InjectionContainerProvider sendAnalyticsEvent={mockSendAnalyticsEventFn}> YourBlogPost blogPost={{ title: 'Test Post', slug: 'test-post' }} /> InjectionContainerProvider> ); const button = screen.getByText('Share'); await userEvent.click(button); expect(mockSendAnalyticsEventFn).toHaveBeenCalledTimes(1); });
Reactアプリでは、コンポーネントは一般的に「ビュー」の部分、つまり見た目とイベントハンドラの設定だけを扱うべきです(onClick 等)。
副作用のあるもの(API 呼び出しや localStorage などのブラウザ Web API とのやり取りなど)やビジネス ロジックは、通常、React コンポーネントの一部にすべきではなく、抽象化する必要があります。
これらは多くの場合、依存関係管理の適切な候補となります。
例:
- 分析(上記の簡単な例のように)
- 内部 API 呼び出し (バックエンドへ)
- 外部 API 呼び出し (サードパーティ サービスへ)
- 複雑/計算が遅いビジネス ロジック (ビジネス ロジックの内部をテストする必要のないテストのほとんどに単純なロジックを挿入します)
window.localStorage、window.sessionStorage等- 設定や環境変数。DIを使用して、サービスや関数だけでなく変数に注入することもできます。
- ロギング – データをロギングサービスに送信できるロガーを組み込むと便利です
私の例は非常に小さいですが、基本的な考え方は理解できると思います。これは余分なコードであり、定型文が多く、実際に実行されるコードが少しわかりにくくなっています。
なぜこのような余分な労力をかける価値があるのか疑問に思うかもしれません。主な利点は、 コードのメンテナンスが容易、 もっと 再利用可能なコード、コードのテストがはるかに簡単になります。
同様に テストを容易にする (とても便利です タイムスタンプ)などのツールを使用することができます。 ストーリーブック 個々のコンポーネントをプレビューし、模擬サービス/オブジェクトを簡単に挿入できます (たとえば、実際の API 呼び出しを行わなくても済みます)。
コードの結合度が大幅に低下し、保守が容易になることがよくあります。
また、 明確な関心の分離。
すでに述べたように、私は依存関係を効率的に管理することに賛成です。ただし、依存関係管理手法の使用にはいくつかの欠点があり、アプリケーションに追加する前に考慮する必要があります。
Reactでこれを行う標準的な方法はありません
ほとんどのバックエンドフレームワークでは、それぞれのフレームワーク/ライブラリに対して非常に標準的で一般的な方法があります。Reactでは、手動で記述するだけで済みます。 useContext() / createContext()これは問題なく、うまく機能し、柔軟性も得られますが、これを処理するための本当に一般的なライブラリがあればいいのにと思います。
- 依存性注入が一般的なFEフレームワークを探しているなら、 角度。
- より一般的な DI ライブラリについては、以下を参照してください。
複雑な記述とデバッグ
ハードコードする方がはるかに簡単です new SomeService() 共通インターフェースを定義したり、値を提供したりすることよりも簡単です。ただし、優れたシステム (型チェック付き) が完成すると、それほど複雑さは増しません。
変数をクリックしてインターフェース定義にたどり着くのも、デバッグがはるかに難しくなります。実際に実行されている実装コードを把握するのは、さらに面倒な場合があります。
構成
データ取得ライブラリ(tanstackクエリ、RTKクエリなど)を使用する場合 提供されたオブジェクト/サービスに挿入するのは非常に複雑になる可能性があります。ただし、これは多くの場合、一度だけ設定すれば済みます。
モックを使った過剰なテスト
コード カバレッジ率が非常に高い (すべてがテストされているように見える) システムを思いつくことができます。
しかし、最終的には本質的には、個別にテストする単体テストが多数作成されることになります。
モックを挿入しないと、より現実的な統合テストをテストする機会を逃す可能性があります。
実際には、両方を少しずつ(モック注入サービスを使用した単体テストと完全な統合テスト)用意する必要があります。モックに注入すると、開発が迅速かつ容易になります。ただし、システムの個別の部分が実際にうまく連携しているかどうかを確認するために、完全な統合テストを実行する必要があることを忘れないでください。
コメントを残して、使用したツールやライブラリを教えてください。このことについて何人かの友人に尋ねたところ、私が思っていたよりも多くの人が DI を使用していることがわかりました。ただし、ほとんどの人は DI と呼ばずにセットアップしただけです。バックエンド エンジニアに尋ねれば、バックエンド アプリケーションでは DI の方がはるかに人気があり一般的であるため、DI について一日中絶賛するでしょう。
#React #における依存性注入 #例を交えた完全ガイド