DIコンテナでフロントエンドの依存性を解決する(Nuxt.js)
はじめに
フロントエンドでも、依存性注入(DI)を活用できます。
バックエンドと異なり、フロントエンドは「1アクション / 1トランザクション」ではなく、ユーザー操作(クリック、入力など)に応じて、任意のタイミングでアクションが発生します。
複数のコンポーネントから同じ機能にアクセスする必要があるため、クラスや関数のパラメータによる依存性注入だけでなく、DIコンテナでグローバルに管理し、どこからでも呼び出せるようにする方が適していると考えます。
DIコンテナ(本プロジェクトでは InversifyJS を使用)により、依存関係を宣言的に管理し、柔軟な解決を実現できます。
本記事では、features/{機能} 構成のプロジェクトで、コンテナコンポーネントや部分コンポーネントからもDIコンテナを利用できる仕組みを説明します。
プロジェクト構成
本プロジェクトは、機能ごとに features/{機能} ディレクトリで構成されています。
(この構成は、「 Feature-Based Directory Structure (フィーチャーベースのディレクトリ構成)」や、「 Package by Feature 」として知られています)
実際のプロジェクトでは、
-
features/user(ユーザー管理) -
features/product(商品管理) -
features/auth(認証) -
features/order(注文管理)
といった、機能ごとにディレクトリが分かれています。
features/{機能}/
├── di/ # DIコンテナの設定
│ ├── symbols.ts # 依存関係のシンボル定義
│ └── {機能}.inversify.config.ts # バインド定義
├── application/ # アプリケーション層
├── infra/ # インフラ層
├── presentation/ # プレゼンテーション層
├── gateways/ # ゲートウェイ層
├── containers/ # コンテナコンポーネント
├── components/ # 部分コンポーネント
└── ...
この構成により、各機能を独立して管理し、DIコンテナで依存関係を解決できます。
DIコンテナのグローバルアクセス
DIコンテナは、Nuxtプラグインでアプリケーション全体に提供されます。
import 'reflect-metadata'
import container from '../configs/inversify.config'
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.provide('container', container)
})
これにより、コンテナコンポーネント、部分コンポーネント、その他の任意の場所から useNuxtApp().$container でアクセスできます。
アーキテクチャの全体像
各機能は、次のレイヤーで構成されます。
- Repository層 - データアクセス
- ViewModel層 - 状態管理
- Presenter層 - プレゼンテーションロジック
- Usecase層 - ビジネスロジック
- Gateway層 - 外部APIとの境界
各層はインターフェース経由で結合され、DIコンテナが依存関係を解決します。
依存関係の定義
シンボルの定義
各依存関係は Symbol で識別します。
Symbol は JavaScript/TypeScript のプリミティブ型の一つで、一意の識別子を生成します。
文字列キーと違い、Symbol は名前衝突を防ぎ、型安全性を保ちます。
DIコンテナでは、この特性を活用して依存関係を識別します。
const SearchActionViewModelSymbol = Symbol('User.SearchActionViewModel')
const SearchActionUsecaseSymbol = Symbol('User.SearchActionUsecase')
const SearchActionPresenterSymbol = Symbol('User.ISearchActionPresenter')
const SearchActionRepositorySymbol = Symbol('User.ISearchActionRepository')
const SearchGatewaySymbol = Symbol('User.SearchGateway')
export {
SearchGatewaySymbol,
SearchActionViewModelSymbol,
SearchActionUsecaseSymbol,
SearchActionPresenterSymbol,
SearchActionRepositorySymbol,
}
Symbol により、型安全性を保ちつつ名前衝突を避けられます。
DIコンテナのバインド定義
各機能の features/{機能}/di/{機能}.inversify.config.ts で依存関係を定義します。
1. Repository
const bindDIUser = (container: Container) => {
:
container
.bind<ISearchActionRepository>(SearchActionRepositorySymbol)
.to(SearchActionRepository)
:
}
export { bindDIUser }
- インターフェース
ISearchActionRepositoryをSearchActionRepositoryにマッピングする
2. ViewModel
const bindDIUser = (container: Container) => {
:
container.bind(SearchActionViewModelSymbol).to(SearchActionViewModel)
:
}
export { bindDIUser }
-
ViewModelは状態を持つため、必要に応じてシングルトンにする
3. Presenter
const bindDIUser = (container: Container) => {
:
container
.bind<
ISearchActionPresenter<ISearchForm, SearchActionViewModel>
>(SearchActionPresenterSymbol)
.toDynamicValue(context => {
const viewModel: SearchActionViewModel = context.get(
SearchActionViewModelSymbol,
)
return new SearchActionPresenter(viewModel)
})
:
}
export { bindDIUser }
-
toDynamicValueでViewModelに依存するPresenterを動的生成 - コンテナから
ViewModelを取得し、コンストラクタに注入
4. Usecase
const bindDIUser = (container: Container) => {
:
container
.bind<IActionUsecase>(SearchActionUsecaseSymbol)
.toDynamicValue(context => {
const repository: ISearchActionRepository = context.get(
SearchActionRepositorySymbol,
)
return new SearchActionUsecase(repository)
})
:
}
export { bindDIUser }
-
Repositoryに依存するUsecaseを動的に生成する -
Repositoryを取得して注入する
5. Gateway (最上位の依存性解決)
const bindDIUser = (container: Container) => {
:
container
.bind<
IDataGateway<ISearchForm, SearchActionViewModel>
>(PatentSearchGatewaySymbol)
.toDynamicValue(context => {
const usecase: IActionUsecase = context.get(SearchActionUsecaseSymbol)
const presenter: ISearchActionPresenter<
ISearchForm,
SearchActionViewModel
> = context.get(SearchActionPresenterSymbol)
return new PatentSearchGateway<ISearchForm, SearchActionViewModel>(
usecase,
presenter,
)
})
:
}
export { bindDIUser }
-
UsecaseとPresenterに依存するGatewayを動的に生成する - 外部インターフェースとして機能する
依存関係グラフ
Gateway
├── Usecase
│ └── Repository
└── Presenter
└── ViewModel
この階層により、各層が適切に分離され、DIコンテナが各依存関係を自動解決します。
どこからでも呼び出せる
DIコンテナはグローバルに提供されるため、コンテナコンポーネント、部分コンポーネント、その他の任意の場所から利用できます。
コンテナコンポーネントからの使用例
const performSearchAction = async (
formData: ISearchForm,
): Promise<SearchActionViewModel> => {
const { $container } = useNuxtApp()
showLoading()
try {
const gateway: SearchGateway<ISearchForm, SearchActionViewModel> =
$container.get(SearchGatewaySymbol)
return await gateway.execute(formData)
} finally {
hideLoading()
}
}
コンテナから Gateway を取得し、execute を呼び出します。
コンテナが依存関係を自動解決します。
部分コンポーネントからの使用例
部分コンポーネントからも同様に利用できます。
// 部分コンポーネントからの使用例
<script setup lang="ts">
const { $container } = useNuxtApp()
const fetchUserData = async () => {
// 部分コンポーネントからでもDIコンテナにアクセス可能
const gateway = $container.get(SearchGatewaySymbol)
const viewModel = await gateway.execute(formData)
// 処理を続ける...
}
</script>
このように、コンポーネントの種類を問わず、どこからでもDIコンテナにアクセスし、依存関係を解決できます。
Gatewayの動作フロー
async execute(request: T1): Promise<T2> {
this._presenter.setFormData(request)
await this._usecase.execute(this._presenter)
return this._presenter.viewModel
}
-
Presenterにフォームデータを設定する -
Usecaseを実行する -
ViewModelを返す
各ステップで依存関係が解決され、柔軟な処理が実現されます。
DIコンテナの初期化
DIコンテナは、次の定義で初期化されます。
import { Container } from 'inversify'
import 'reflect-metadata'
import { bindDIUser } from '@features/user/di/user.inversify.config'
// DIコンテナを構築する
const container = new Container()
bindDIUser(container)
export default container
各機能の bindDI{機能} 関数を呼び出し、依存関係を登録します。
新しい機能を追加する場合も、同様に bindDI{新機能} を追加するだけです。
DIコンテナによる柔軟性の実現
1. 実行時依存性解決
toDynamicValue を使うことで、実行時に依存関係を解決できます。
これにより、環境や条件に応じて異なる実装を注入できます。
2. インターフェースベースの設計
インターフェースを経由することで、実装を切り替えても影響を最小化できます。
テスト時はモック、本番では実装を注入するなど、柔軟に対応できます。
3. 階層的な依存関係の管理
複雑な依存関係も、コンテナが自動解決します。各層の責務を明確にしつつ、柔軟な構成を実現します。
4. 機能ごとの独立性
features/{機能} 構成により、各機能を独立して管理できます。
各機能のDI設定を分離することで、機能間の影響を最小化し、保守性を向上させます。
5. どこからでもアクセス可能
DIコンテナがグローバルに提供されるため、コンテナコンポーネント、部分コンポーネント、その他の任意の場所から同じ方法で依存関係を解決できます。
これにより、コードの一貫性と再利用性が向上します。
メリット
- テスタビリティの向上:モックを注入可能で、単体テストが容易
- 疎結合の実現:インターフェース経由の依存により変更に強い
- 保守性の向上:依存関係が明確で、変更時の影響範囲が把握しやすい
- 再利用性の向上:コンポーネントを再利用しやすい
- 柔軟な依存性解決:実行時に依存関係を解決し、環境や条件に応じた実装を選択可能
-
機能ごとの独立性:
features/{機能}構成により、各機能を独立して管理 - グローバルアクセス:コンテナコンポーネント、部分コンポーネント、その他の任意の場所から同じ方法でアクセス可能
まとめ
DIコンテナを活用することで、フロントエンドでも柔軟な依存性解決を実現できます。
features/{機能} 構成により、各機能を独立して管理し、DIコンテナで依存関係を解決できます。
DIコンテナを導入することで、以下のメリットが得られます。
- 宣言的な依存関係管理:依存関係をコード上で明確に定義
- 型安全性の確保:TypeScript と組み合わせて型安全性を維持
- テスト容易性の向上:モック注入により単体テストが容易
- 柔軟な依存性解決:実行時に依存関係を解決し、環境に応じた実装を選択可能
- クリーンアーキテクチャの実現:各層の責務を明確に分離
-
機能ごとの独立性:
features/{機能}構成により、各機能を独立して管理 - グローバルアクセス:コンテナコンポーネント、部分コンポーネント、その他の任意の場所から同じ方法で依存関係を解決可能
この実装例は、フロントエンドでも依存性注入を活用できることを示しています。
DIコンテナを活用することで、柔軟で保守性の高いコードを実現でき、features/{機能} 構成と組み合わせることで、スケーラブルで保守しやすいアーキテクチャを構築できます。
Discussion