はじめに
実務でDI改善
等のPRを見かけ、DI?、InversifyJS?、@Injectable()
?と不明なワードが多く、イメージを掴むのが難しかったですが調べた内容を記載しています。
DI効果、DIによるコード改修・ユニットテストのしやすさを知りたい方に読んでいただければと思います🥺
目次
- Dependency Injection (依存性の注入) とは?
- TypeScriptにおけるDIの実装
- DIコンテナとよく使われるメソッド
- DIコンテナの各メソッド解説:「もしカフェを開いたら」...
- サンプルコードで解説
- そしてテストがめちゃくちゃラクになる!
- まとめ
- 参考文献
🔌 Dependency Injection (依存性の注入) とは?
まずは概念から...
DIは、ソフトウェアのデザインパターンの一つで、
「あるコンポーネント(クラス)が必要とする別のコンポーネント(依存オブジェクト)を、外部から与える(注入する)」
という設計原則です。
🧷 なぜDIを使うのか?
疎結合 (Loose Coupling)
クラスが自身で依存オブジェクトを生成するのではなく、外部から受け取るため、特定の具象クラスへの依存度が下がります。
これにより、依存オブジェクトの実装が変わっても、利用側のクラスへの影響を最小限に抑えられます。
テスト容易性 (Testability)
依存オブジェクトを外部から注入できるため、テスト時には本物の代わりにモックオブジェクトやスタブを簡単に注入できます。
これにより、ユニットテストが容易になります。
再利用性と保守性 (Reusability & Maintainability)
コンポーネント間の依存関係が明確になり、コードの見通しが良くなります。
疎結合な設計は、コンポーネントの再利用性を高め、変更や機能追加時の影響範囲を特定しやすくします。
関心の分離 (Separation of Concerns)
オブジェクトの生成と利用の責任を分離できます。
オブジェクトの生成やライフサイクル管理はDIコンテナに任せ、クラスは自身のビジネスロジックに集中できます。
🧷 基本的な考え方:制御の反転 (Inversion of Control - IoC)
DIは、より大きな概念であるIoC(制御の反転)を実現する具体的な手法の一つ
通常、クラスAがクラスBを使う場合、クラスAの内部で new B() のようにBのインスタンスを生成(制御)するが、
DIを使うと、クラスAは「Bが必要だ」と宣言するだけで、Bのインスタンス生成やAへの提供(注入)は外部の仕組み(DIコンテナ)が行います。
IoCと呼ばれる理由は、オブジェクトの生成・管理(制御)を、クラス自身でなく外部(コンテナ)で行う(反転)ためです。
🔮 TypeScriptにおけるDIの実装
TypeScriptでDIを実装する場合、一般的に以下の要素が使われます。
1. デコレータ (Decorators)
クラスやプロパティ、コンストラクタ引数にメタデータを付与し、DIコンテナが依存関係を認識・解決するために使われます(例: @Injectable()
, @Inject()
)。
2. DIコンテナ (DI Container)
依存関係の「登録」と「解決」を担当する中心的な役割を果たします。アプリケーション全体でどのインターフェースにどの具象クラスを使うか、オブジェクトのライフサイクル(シングルトンか、都度生成かなど)を管理します。
3. インターフェース (Interfaces) / 抽象クラス (Abstract Classes)
依存関係を具象クラスではなく、抽象(インターフェースや抽象クラス)に対して定義することで、疎結合を促進します。
4. reflect-metadata
TypeScriptの型情報を実行時に取得するために、多くのDIライブラリがこのライブラリに依存しています。
tsconfig.json
で emitDecoratorMetadata: true
と experimentalDecorators: true
を有効にする必要があります。
🧙♂️ 少し掘り上げ解説
-
reflect-metadata
は、JavaScript(やTypeScript)に「型情報などのメタデータ」を埋め込んで、あとから取り出せるようにするライブラリです。 - 簡単にいうと、
「変数や関数にこっそりメモを貼っておいて、あとでそのメモを見る」
みたいなことができる道具です。
📦 どんなときに使うの?
たとえば…
- TypeScriptの型情報を、実行時に使いたいとき
- DIコンテナ(依存性注入) を作るとき
-
デコレーター(
@Something
) で、何か特別な情報を持たせたいとき
に使います。
🧪 超簡単な例
import "reflect-metadata";
class User {
@Reflect.metadata("design:type", String)
name: string = "";
}
// メタデータを取得する
const type = Reflect.getMetadata("design:type", User.prototype, "name");
console.log(type); // [Function: String]
👉「nameというプロパティの型はStringだよ」というメモ(メタデータ)を貼っておいて、あとでそれを取り出しています。
🔧 なぜTypeScriptで必要?
TypeScriptの型はコンパイル時に消えるから、JavaScriptには残りません。
でも、
「実行時にも型情報が欲しい!」
というときにreflect-metadata
を使えば、型のメタデータを保存・取得できるようになります。
用語 | 意味 |
---|---|
メタデータ | データに関するデータ(型、名前、属性など) |
reflect | メタデータを扱うためのAPI(Reflect API) |
用途 | デコレーター、DI、バリデーションなどに便利 |
⚡ DIコンテナとよく使われるメソッド
💡 DIコンテナの目的
- 依存関係の管理を自動化
- 疎結合(loose coupling)な設計
- テストが容易になる
💡 よく使われるメソッド(代表的なDIコンテナに共通する例)
メソッド名 | 概要 |
---|---|
register() / bind()
|
クラスやインターフェースをコンテナに登録する |
singleton() |
インスタンスを1つだけ作成し、全体で共有(キャッシュ)する |
transient() |
呼び出すたびに新しいインスタンスを生成する(デフォルトのことも多い) |
resolve() / get()
|
登録した依存を取り出す(依存を注入してインスタンスを返す) |
inject() |
手動で依存関係を注入したいときに使う(フレームワークによって異なる) |
🫖 DIコンテナの各メソッド解説:「もしカフェを開いたら」...
あなたはカフェオーナー。
でも「コーヒー豆を焙煎して、ミルクを用意して、バリスタも自分でやって…」なんてやってたら大変。
そこで 「DIコンテナ」= 秘書さん を雇います。
あなたの代わりに材料やスタッフを揃えてくれます!
🧩 1. register() / bind()
「こういうスタッフや材料があるよ」と秘書に教える
container.register('Milk', Milk)
👉「うちのカフェではミルクっていう材料はこれを使うよ」と事前に伝える。
🔁 2. singleton()
「これは毎回同じ人/ものを使って」
container.singleton('EspressoMachine', EspressoMachine)
👉 エスプレッソマシンは1台あればOK。何杯作っても同じマシンを使うから、1つだけ作って共有する。
🧽 3. transient()
「これは毎回新しく用意して」
container.transient('Cup', Cup)
👉 カップはお客さんごとに新しいのを出すでしょ?だからその都度新しいのを用意!
🎁 4. resolve() / get()
「これちょうだい」って秘書にお願いする
const milk = container.resolve('Milk')
👉「今ミルクが必要なんだけど」と頼めば、秘書が(登録済みのものを)出してくれる。
🚀 5. inject()
「この人に材料をあらかじめ渡しておいて」
@Inject('Milk') class LatteMaker { ... }
👉 バリスタ(LatteMaker)に、「事前にミルク渡しといてね」と秘書にお願いしておく。
💡 まとめ(めちゃカンタンに)
メソッド | たとえ | 意味 |
---|---|---|
register() |
材料を登録 | こういうの使うよと教える |
singleton() |
同じマシンを使い続ける | 1つだけ作って使い回す |
transient() |
毎回新しいカップを出す | 呼び出すたびに新しく用意する |
resolve() |
ミルクちょうだい | 登録済みのものをもらう |
inject() |
バリスタに材料を渡す | 自動で依存を注入してあげる |
📖 サンプルコードで解説
👋 はじめに
TypeScriptでこんなコードに出会うとします。
class UserService {
private userRepository: UserRepository;
constructor() {
this.userRepository = new UserRepository(); // ← ここで自分で new
}
getUser(id: string) {
return this.userRepository.findById(id);
}
}
👉 一見シンプルで良さそうに見えますが、実はこれ、変更やテストにすごく弱いです。
🤔 何が問題なの?
このコードでは、UserService
が UserRepository
を 自分で new して固定しています。
つまり、「ユーザー情報の取得方法を変えたい」となったとき(例えば、外部APIから取得したくなったとき)でも…
this.userRepository = new UserRepository(); // ← これを変えないといけない!
と、UserServiceの中身まで書き換えが必要になります。
💡 解決策:依存性注入(DI)
これを解決するのが 「依存性注入(Dependency Injection)」 という考え方です。
DIを使うと、こうなります。
class UserService {
constructor(private userRepository: UserRepository) {}
getUser(id: string) {
return this.userRepository.findById(id);
}
}
ポイントはここ👇
UserServiceはUserRepositoryを自分で作らない! 代わりに、外から渡してもらう(注入される) だけ。
🔁 差し替えが自由自在に!
たとえば、以下のように使い分けができます。
const repo = new DbUserRepository(); // DB用リポジトリ
const service = new UserService(repo);
あるいは、
const repo = new ApiUserRepository(); // 外部API用リポジトリ
const service = new UserService(repo);
UserServiceの中身は一切変えずに、注入するリポジトリだけを差し替えることができるんです。
✅ そしてテストがめちゃくちゃラクになる!
DIの最大の恩恵のひとつが ユニットテストのしやすさ。
実際にやってみましょう!
🧪 JestでUserServiceをテストする例
まず、UserRepositoryのモック(ニセもの)を作ります。
class MockUserRepository implements UserRepository {
findById(id: string) {
return { id, name: 'テスト太郎' };
}
}
テストコードはこんな感じ
describe('UserService', () => {
it('ユーザーが取得できること', () => {
const mockRepo = new MockUserRepository();
const service = new UserService(mockRepo);
const result = service.getUser('abc123');
expect(result).toEqual({ id: 'abc123', name: 'テスト太郎' });
});
});
💡 さらに柔軟に:jest.fn()で関数をモック化
const mockFindById = jest.fn().mockReturnValue({ id: 'xyz', name: 'モック花子' });
const mockRepo: UserRepository = {
findById: mockFindById,
};
const service = new UserService(mockRepo);
const result = service.getUser('xyz');
expect(result.name).toBe('モック花子');
expect(mockFindById).toHaveBeenCalledWith('xyz');
これで
- 本物のDBなし
- ネットワークなし
- 超高速&安定
なテストが可能になります!
🎉 まとめ
- DI は、あるコンポーネント(クラス)が必要とする別のコンポーネント(依存オブジェクト)を、外部から与える(注入する)設計原則
項目 | DIなし | DIあり |
---|---|---|
柔軟性 | ❌ 固定されてる | ✅ 差し替え自由 |
テストしやすさ | 😢 難しい(DBやAPIが必要) | 😊 モックで高速テスト可能 |
保守性 | 💣 高い結合度 | 💪 疎結合で変更に強い |
- DIの特徴
- 疎結合
- テスト容易性
- 再利用性と保守性
- 関心の分離
- DIコンテナ 代表的なメソッド
メソッド名 | 概要 |
---|---|
register() / bind()
|
クラスやインターフェースをコンテナへ登録 |
singleton() |
インスタンスを1つだけ作成し、全体で共有 |
transient() |
呼び出すたびに新しいインスタンスを生成 |
resolve() / get()
|
登録した依存を取り出す |
inject() |
手動で依存関係を注入したいときに使う |