こんにちは!
筆者はLaravelをメインで扱うWebエンジニアでしたが、FastAPIに触れてDIの実装の違いに驚きました。
DIとは何かを簡単に振り返り、DIに対するアプローチの違いを4つ例を挙げ(Wire, Spring, Laravel, FastAPI)、DIのイメージを深めていこうと思います。
IoC と DI
IoC(制御の反転: Inversion of Control)
お決まりではありますが、DIを語る前段としてIoCという考え方を紹介します。
IoCは、プログラムの処理フローやオブジェクト生成の「制御権」を、個別のコードからフレームワークや外部の仕組みへ逆転させる設計思想・大原則のことです。
わかりやすい例は関数を登録するタイプのIoCです。
| IoCの例 | 内容 |
|---|---|
| フック | 特定のタイミングに処理を登録できるようにする。 典型的にはbeforeXXXやafterXXXといった特定の処理前後に関数を登録する。 |
| ミドルウェア | 特定の処理の行き帰りを複数の処理(関数)でマトリョーシカのように入れ子状に重ね合わせる。 |
| コールバック | 主となる処理に「終わったらこれをして」と関数を預け、非同期処理の完了時などに連鎖的に処理を実行させる。 |
| テンプレートメソッド | 全体の処理の流れ(骨組み)はフレームワーク側で定義しておき、開発者は一部の「穴埋め(具象処理)」となる関数やメソッドだけを登録する。 |
| ストラテジー | 状況(設定や入力)に応じて、実行する「アルゴリズムの中身(ロジック)」を丸ごとオブジェクトや関数として登録し、動的に切り替える。 |
| イベント・リスナー | 特定のイベントをトリガーとして、あらかじめ登録しておいた複数の関連する処理を一斉に連動させて実行する。 |
| スケジューラー | 「時間」や「特定条件」の監視と制御をシステム側に任せ、指定したタイミングで登録されたタスク(関数)を定期実行する。 |
| ルーター・ハンドラー | このURLにアクセスが来たらこの関数を動かすという、入力と処理の対応マップを登録してフレームワークに交通整理させる。 |
上表の例では、ユーザーは処理の登録のみを行なっており、どのようなタイミング・条件・流れで処理を実行するかはフレームワークに委ねられている点でIoCとなります。
DIは、このIoCという広い概念を実現するための具体的な手段・パターンのひとつです。
IoCは、特定の技術というよりフレームワーク化する際の心構えであり、スローガンです。
クラウドサービスでよく耳にする”Undifferentiated heavy lifting”(差別化につながらない重労働)と発想は似ています。
ドメインロジックに集中するための、技術的な発想の転換を意識付ける概念です。
DI(依存性の注入: Dependency Injection)
DIとは、オブジェクトが依存する別のオブジェクトを内部で生成(new)せずに、外部から注入する設計パターンです。
# DIなし(直接生成・密結合)
class UserServiceWithoutDI:
def __init__(self):
self.repository = PostgreSQLUserRepository()
# DIあり(外部から注入・疎結合)
class UserServiceWithDI:
def __init__(self, repository: UserRepository):
self.repository = repository
これだけだと呼び出し側にnewが移動するだけに見えますが、DIの実装ではnewを隠蔽する何かしらの機能を提供します(後述)。
DIの目的とメリットとして以下が挙げられます。
- 結合度の低減: インターフェースや型に依存することで、具体的実装への依存を切り離せる。
- テスト容易性の向上: 単体・結合テストで依存オブジェクトをモックやスタブに切り替えることが容易になる。
- コードの再利用性と保守性の向上: オブジェクトの生成ロジックと利用ロジックが分離されるため、変更に強い。
また依存関係をどこから注入するかによって、大きく3つのスタイルに分類されます。
| 方式 | 説明 |
|---|---|
| コンストラクタ・インジェクション | オブジェクトの生成時(コンストラクタ)に依存を渡す |
| メソッド・インジェクション | 特定のメソッドの呼び出し時に引数として依存を渡す |
| セッター / プロパティ・インジェクション | オブジェクト生成後にセッターメソッドやパブリック属性経由で渡す |
主要なDI実現パターン・アプローチ
言語やフレームワークによって、DIを実現するアプローチは異なります。例として4つのフレームワーク(言語)を解説します。
- Wire(Go): 静的DI
- Spring(Java): IoCコンテナ、アノテーション
- Laravel(PHP): IoCコンテナ、オートワイヤー
- FastAPI(Python): 関数型
Wire(Go): 静的DI
WireでのDIの実現は非常にシンプルで、ファクトリーパターンでnewを隠蔽します。
// main.go (エントリーポイント)
package main
import "log"
func main() {
// newせずファクトリー(DIの文脈ではインジェクター)を使用する
app, err := InitializeApp()
if err != nil {
log.Fatal("アプリの初期化に失敗しました: ", err)
}
// 起動する
app.Start()
}
インジェクターはwire.goにまとめて書きます。
依存関係解決部分について機能があり、wireコマンド(go generate)を実行すると依存関係順にコンストラクタを並べ直して実行する関数が生成されます。
// wire.go
//go:build wireinject
package main
import (
"github.com/google/wire"
"my-app/app"
"my-app/config"
"my-app/db"
"my-app/repository"
)
func InitializeApp() (*app.App, error) {
wire.Build(
// 生成に使用するコンストラクタを列挙
config.NewConfig,
db.NewPostgres,
repository.NewUserRepository,
app.NewApp,
// インターフェースによるDIの場合、バインドを記述
wire.Bind(
new(db.Database), // 要求されるインターフェース
new(*db.Postgres), // 実際に流し込む具象構造体
),
)
return nil, nil
}
テストコードではイニシャライザーを使わずコンストラクタを呼び出し、モックをDIします。
依存注入を全て明記する必要がある点が特徴です。
Spring(Java): IoCコンテナ、アノテーション
Javaの代表的なフレームワークであるSpringは、IoCコンテナ(ApplicationContext)を用いてオブジェクト生成を管理します。
Wireのようにコードで明示的にBuildを書く代わりに、管理させたいクラスにアノテーションを付与することで「このクラスをコンテナに登録してほしい」という意思表示をする方式です。
// インターフェースの定義
public interface UserRepository {
User findById(String id);
}
// 具象クラスに @Repository (Bean登録のアノテーション) を付与
@Repository
public class PostgreSQLUserRepository implements UserRepository {
public User findById(String id) { ... }
}
// 利用側に @Service を付与し、コンストラクタで依存を要求する
@Service
public class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
}
動作の仕組みは以下のようになります。
- ユーザーは管理させたいクラスに
@Component,@Service,@Repositoryなどのアノテーションを付与する。 - アプリケーション起動時、Springが指定されたパッケージ内をスキャン(コンポーネントスキャン)し、アノテーションが付いたクラスを見つけてIoCコンテナに登録する。
- クラスをインスタンス化する際、コンストラクタの引数の型(ここでは
UserRepository)を見て、コンテナ内に適合するBeanがあれば自動的に注入(オートワイヤー)する。
テスト時には、Spring Bootの@MockitoBeanなどを利用することで、コンテナ内の特定のBeanをモックに差し替えることができます。
起動時のコンポーネントスキャンによるオーバーヘッドはありますが、開発者はアノテーションを付けるだけでよく、設定ファイルやインジェクターを意識せずに済むのが大きな特徴です。
Laravel(PHP): IoCコンテナ、オートワイヤー
LaravelのIoCコンテナ(またはDIコンテナ)も、Springと同じくオブジェクト生成をコンテナ経由で一元管理する方式です。
ただしSpringが起動時の明示的な登録(アノテーション)を必須としていたのに対し、Laravelのコンテナは要求された時点でそのクラス名を都度リフレクションするため、コンテナが事前にクラスの存在を知っておく必要がありません。
- ユーザーは具象クラスを定義する。
// ユーザーはDI形式でクラスを定義
class UserController {
public function __construct(UserService $service) { ... }
// アクションメソッドの引数でもメソッドインジェクションが可能
public function show(Request $request, string $id) { ... }
}
class UserService {
public function __construct(UserRepository $repo) { ... }
}
class UserRepository {
public function __construct(UserDatabase $db) { ... }
}
class UserDatabase {
public function __construct() { ... }
}
- ユーザーは
routes/web.phpなどにルーティングを定義して、URLと[UserController::class, 'show']のようにコントローラの特定のアクションメソッドを紐づける。 - リクエストを受け取ると、フレームワークはURLをルーティング設定に従って解決し、対応するUserControllerの生成をIoCコンテナに要求する。
- コンテナはリフレクションによりUserControllerの生成にはUserServiceが必要なことを検知し、UserServiceを生成しようとするとUserRepositoryが必要なことを検知し、、、と自身で依存関係を辿りながら再帰的に必要なオブジェクトを生成する(オートワイヤー)。
showメソッドのRequest $requestのようにメソッド・インジェクションも頻繁に使われます。
テストでは、IoCコンテナ上でUserRepositoryをモックに紐づけて具象クラスを置き換えることができます。
IoCコンテナとリフレクションを用いた動的な解決のため、他の方式と比較してオーバーヘッドは大きくなりますが、インジェクターやバインドをほぼ気にせずに済み、テストでの置き換えも非常に簡単です。
実務上、開発者が明示的にIoCコンテナを操作する場面は多くなく、サービスプロバイダでインターフェースのバインドを設定するときなどに限られます。
オートワイヤーが大変便利なため、コンテナの存在を意識せずにDIの恩恵を受けられるのがLaravelの魅力の1つです。
FastAPI(Python): 関数型
LaravelやSpringのようなIoCコンテナがインスタンスを管理するアプローチとは異なり、FastAPIは関数をベースにした局所的なDIを提供します。
Depends()という関数を用いて、ルート(エンドポイント)の引数として依存関係を宣言します。
from fastapi import FastAPI, Depends
app = FastAPI()
# 1. 依存関係(プロバイダ)となる関数を定義
def get_db():
db = PostgreSQL()
try:
yield db
finally:
db.close()
# 2. 依存関係となるクラス(関数のように呼び出せる)を定義
class UserRepository:
# get_db関数の戻り値に依存する
def __init__(self, db: PostgreSQL = Depends(get_db)):
self.db = db
# 3. ルーティングの引数で Depends() を使って注入を要求する
@app.get("/users/{user_id}")
def read_user(user_id: str, repo: UserRepository = Depends()):
return repo.find_by_id(user_id)
動作の流れは非常にシンプルかつ明示的です。
- リクエストが
/users/123に到達する。 - FastAPIは
read_user関数の引数repoがUserRepository = Depends()となっているのを見る。 UserRepositoryの初期化にはdb = Depends(get_db)が必要だと解決し、先にget_db()を実行する。- 取得した
dbを使ってUserRepositoryを生成し、最終的にread_userに渡す。 - レスポンスを返した後、
get_db()のfinallyブロックが実行され、クリーンアップが行われる。
なお、同一リクエスト内で同じ依存関数(例えばget_db)が複数箇所から参照された場合、FastAPIはデフォルトで最初の1回だけ実行し、以降は結果をキャッシュして使い回します(Depends(get_db, use_cache=False)と明示すればこのキャッシュを無効化できます)。
Depends()が一度使われると、依存元全てにDepends()を書かないと依存解決の連鎖が繋がりません。
テスト時には、FastAPIが用意しているdependency_overridesという辞書を使って、特定の関数をモック関数にすり替えることができます。
# テスト時のモックへの差し替え app.dependency_overrides[get_db] = override_get_db
FastAPIのDIは、LaravelやSpringのようなアプリ全体を管理するIoCコンテナを持たず、関数の引数と型ヒントを活用する設計です。
どこから何が注入されているかがエディタ上で追いやすく、IDEの入力補完とも非常に相性が良いという強みがあります。
おわりに
この記事では、IoCとDIの基本概念から始まり、4つのフレームワーク(Wire, Spring, Laravel, FastAPI)におけるDIの実装アプローチの違いを見てきました。
- Wire(Go): 静的解析によるコード生成で、依存注入を全て明示するパフォーマンス重視のDI。
- Spring(Java): アノテーションと起動時のスキャンを活用した、IoCコンテナによるDI。
- Laravel(PHP): 同じIoCコンテナでも、登録なしのリフレクションで解決する動的DI。
- FastAPI(Python): 関数と型ヒントを組み合わせた、リクエストスコープ主体の明示的なDI。
「オブジェクトの生成と依存関係の解決を外部に委ねる」というDIの根本的な目的はどれも同じですが、言語のパラダイムやフレームワークの思想によって、そのアプローチは全く異なります。
複数のアプローチを知ることは、新しいフレームワークを学ぶ際の理解を深めるだけでなく、普段使っているツールの設計思想を再認識する良いきっかけになります。
本記事が、皆さんのDIへの理解を深める一助となれば幸いです。