C# や .NET を触っていると、DI(依存性注入)という言葉をどこかで必ず耳にします。
ただ、実際のところはどうでしょう。
- コンストラクタでインターフェースを受け取っているけど、理由はよく分からない
- DI コンテナって便利らしいけど、何が嬉しいのかピンとこない
- ASP.NET Core では勝手に動いてるけど、仕組みを理解してるかと言われると微妙
こんな感覚を持っている人は意外と多いんです。
でも DI は、特定のフレームワークの機能ではありません。
もっと根っこの部分にある、“設計を柔らかくするための考え方” です。
コンソールアプリでも、ライブラリでも、WPF でも、Unity でも、.NET であればどこでも活かせます。
この記事では、
「DI を使うと何が変わるのか」
「どう書けば実務で役立つのか」
を、できるだけ肩の力を抜いて話していきます。
1. DI が解決したい問題は「結びつきが強すぎる」こと
まずは DI が何を解決するのかを押さえておきましょう。
例えば、こんなコード。
public class UserService
{
private readonly FileLogger _logger = new FileLogger();
public void CreateUser(string name)
{
_logger.Log($"User created: {name}");
}
}
一見シンプルですが、問題があります。
-
UserServiceがFileLoggerにガッチリ依存している - ログの出力先を変えたいときに
UserServiceを修正しないといけない - テストで差し替えができない
つまり、変更に弱い設計になってしまうんです。
■ Before:依存を自分で抱え込む(密結合)
2. DI の本質は「依存を外から渡す」だけ
DI と聞くと難しく感じますが、やっていることはとてもシンプルです。
必要なものを自分で new せず、外から渡してもらう
これだけです。
先ほどの例を DI を使って書き直すとこうなります。
public class UserService
{
private readonly ILogger _logger;
public UserService(ILogger logger)
{
_logger = logger;
}
public void CreateUser(string name)
{
_logger.Log($"User created: {name}");
}
}
こうすることで、
- ログの実装を差し替えられる
- テストでモックを渡せる
-
UserServiceが余計な責務を持たなくなる
といったメリットが生まれます。
■ After:依存を外から渡す(疎結合)
3. .NET で DI を使う方法は 2 種類ある
① 自前で注入する(手動 DI)
コンソールアプリや小規模なツールでは、これで十分です。
ILogger logger = new FileLogger();
var service = new UserService(logger);
service.CreateUser("Taro");
手動 DI のメリットは「仕組みがシンプルで理解しやすい」こと。
DI の本質を理解するには、まずここから始めるのが一番です。
② DI コンテナを使う(自動 DI)
規模が大きくなると、依存関係を手でつなぐのが大変になります。
そこで登場するのが DI コンテナです。
.NET には標準 DI コンテナがあり簡単に使えます。
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddSingleton<ILogger, FileLogger>();
services.AddTransient<UserService>();
var provider = services.BuildServiceProvider();
var service = provider.GetRequiredService<UserService>();
service.CreateUser("Taro");
■ DI コンテナによる自動解決の流れ
- 登録:どのインターフェースにどの実装を使うかを DI コンテナに教える
- 依存解決:必要になったときに、依存関係をたどって自動で生成
- 完成:依存が注入された状態のインスタンスが手に入る
4. ライフサイクル(Singleton / Scoped / Transient)の理解は必須
DI コンテナを使うときに避けて通れないのがライフサイクルです。
-
Singleton
アプリ終了まで同じインスタンス
→ 設定、キャッシュ、軽量なサービス向け -
Scoped
“スコープ”ごとにインスタンスを共有
→ Web では「1リクエスト = 1スコープ」
→ コンソールアプリでは自分でスコープを作る -
Transient
注入されるたびに新しいインスタンス
→ ステートレスな処理向け
5. インターフェースを使う理由は「差し替えやすさ」
DI とセットで語られるのが「インターフェースを使いましょう」という話。
理由はシンプルです。
- 実装を差し替えられる
- テストでモックを使える
- クラスの責務が明確になる
ただし、実装が1つしかないのに無理にインターフェースを作る必要はありません。
実務では「差し替えの可能性があるか」で判断するのが自然です。
6. DI を使わないとどう破綻するか
DI の価値が一番わかるのは、使わなかった場合の破綻を知ったときです。
■ ある日、ログ出力先を変えることになった
あなたのプロジェクトでは、ログをファイルに書き出す FileLogger を使っていました。
public class UserService
{
private readonly FileLogger _logger = new FileLogger();
}
ところがある日、上司からこう言われます。
「ログをクラウドに送るようにしてほしい」
あなたは青ざめます。
なぜなら…
-
FileLoggerを 直接 new しているクラスが大量にある - それらを全部探して書き換える必要がある
- テストも全部壊れる
- 変更の影響範囲が読めない
密結合のツケが一気に爆発します。
■ DI を使っていた場合
もし DI を使っていたら、変更はたった 1 行。
// 変更前
services.AddSingleton<ILogger, FileLogger>();
// 変更後
services.AddSingleton<ILogger, CloudLogger>();
- 既存のサービスは ILogger しか知らない
- 実装の差し替えは DI コンテナに任せるだけ
- テストも壊れない
- 影響範囲は最小限
DI の価値はここにあります。
7. 実務でよくある DI の落とし穴
● コンストラクタが太りすぎる
依存が多すぎるクラスは、そもそも責務が多すぎるサインです。
● なんでも Singleton にする
状態を持つクラスを Singleton にすると、予期せぬバグの温床になります。
● DI コンテナに登録しすぎる
「とりあえず全部登録」は逆効果。
本当に必要なものだけ登録するのが鉄則です。
8. まとめ:DI は「設計を柔らかくするための道具」
DI はフレームワークの機能ではなく、設計の考え方です。
- 依存を外から渡す
- 実装を差し替えられる
- テストしやすくなる
- クラスの責務が整理される
.NET では標準 DI コンテナがあるので、どんなアプリでも DI を取り入れられます。
まずは「自分で new しない」ことを意識してみると、DI の良さが自然と見えてきます。