前回はこちら
前回からの続き
後輩: 先輩、この前のDIコンテナの話、よく分かりました!Program.csでAddScopedって書きましたけど、他にAddTransientとかAddSingletonっていうのもあるみたいですね。これって、どう使い分けるんですか?
先輩: いい質問だね!DI を使いこなすには、もう少しだけ知っておくと便利な概念があるんだ。いくつか解説するね。
インジェクション(注入)の種類
先輩: まず、一口に「注入」と言っても、やり方がいくつかあるんだ。主に3種類あるから覚えておこう。
1. コンストラクタ インジェクション
public interface IRepository
{
void GetData();
}
public class Repository : IRepository
{
public void GetData()
{
Console.WriteLine("Data Retrieved");
}
}
public class Service
{
private readonly IRepository _repository;
// コンストラクタインジェクション
public Service(IRepository repository)
{
_repository = repository;
}
public void Serve()
{
_repository.GetData();
}
}
public class Program
{
static void Main(string[] args)
{
// DI container セットアップ
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<IRepository, Repository>();
serviceCollection.AddTransient<Service>();
var serviceProvider = serviceCollection.BuildServiceProvider();
// Resolve dependencies
var service = serviceProvider.GetService<Service>();
service.Serve();
}
}
先輩: クラスのインスタンスを作る時(コンストラクタ)に必要な部品をまとめて渡す、最も一般的で推奨される方法だよ。
利点:
- そのクラスが何に依存しているかが一目瞭然になる
- 必要な部品が足りないとコンパイル時点でエラーになるので安全
- テストの時に偽物の部品(モック)を簡単に渡せる
欠点:
先輩: 特に大きな欠点はないけど、依存する部品が多くなりすぎるとコンストラクタが長くなることがあるね。
後輩: なるほど。コンストラクタ インジェクションの利点として、「必要な部品が⾜りないとコンパイル時点でエラーになるので安全」とありましたが、もしこれがコンストラクタ インジェクションではない場合、実⾏時ではどうなるんでしょうか?
先輩: とても鋭い質問だね!まさにそれが、私たちが基本としてコンストラクタ インジェクション(CI)を推奨する最大の理由なんだ。CIが「コンパイル時」の安全性を担保してくれるのに対し、他の注⼊方法や設計によっては、依存関係の不足が「実行時」になって初めて露呈するリスクがあるんだよ。
後輩: 実行時のエラーですか…。
先輩: そう。特に注意が必要なのが、次に説明するプロパティ インジェクションだ。
2. プロパティ インジェクション
public interface IService
{
void Serve();
}
public class Service : IService
{
public void Serve()
{
Console.WriteLine("Service Called");
}
}
public class Client
{
//DI用のプロパティ
public IService Service { get; set; }
public void Start()
{
Service?.Serve();
}
}
public class Program
{
static void Main(string[] args)
{
// DI container セットアップ
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<IService, Service>();
// Resolve dependencies
var serviceProvider = serviceCollection.BuildServiceProvider();
var client = new Client
{
//プロパティ インジェクション
Service = serviceProvider.GetService<IService>()
};
client.Start();
}
}
先輩: クラスのプロパティ({ get; set; }の部分)を通じて、後から部品を注入する方法だ。
利点:
- 必ずしも必要ではない、オプションの部品を注入するのに向いている
欠点:
- 部品の注入が強制されないので、設定し忘れるとエラーの原因になる
- そのせいでテストが少し難しくなることがある
先輩: このプロパティ インジェクションは、必ずしも必要ではない、オプションの部品を注⼊するのには向いている。
後輩: それが「実行時の危険性」にどう繋がるんですか?
先輩: コンストラクタ インジェクションと違って、プロパティ インジェクションでは、部品の注⼊が強制されないんだ。つまり、設定し忘れてもコンパイルは通ってしまう。
後輩: ああ!コンパイル時にはエラーにならないんですね。
先輩: その通り。そして、その設定されていないクラスをアプリケーションが実行時に利用しようとした瞬間、必要な部品(依存関係)が見つからずにエラー(Null参照など)を吐いてしまうんだ。
後輩: なるほど!コンストラクタ インジェクションならコンパイルの時点で赤線が出て開発中に気づけるのに、プロパティ インジェクションだと、ユーザーがその機能を使うまでエラーに気づかない可能性があるんですね。それが 「テストが失敗するリスクがある」 という点にも繋がるわけですね。
先輩: まさにその通りだよ。だからこそ、必須の依存関係に対しては、依存関係が明確になり、コンパイル時の安全性が高いコンストラクタ インジェクションが、最も基本的な(そして推奨される)手法となるんだ。
3. メソッド インジェクション
public interface INotifier
{
void Notify(string message);
}
public class Notifier : INotifier
{
public void Notify(string message)
{
Console.WriteLine("Notification: " + message);
}
}
public class NotificationService
{
//DI用の引数
public void SendNotification(INotifier notifier, string message)
{
notifier.Notify(message);
}
}
public class Program
{
static void Main(string[] args)
{
// DI container セットアップ
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<INotifier, Notifier>();
var serviceProvider = serviceCollection.BuildServiceProvider();
// Resolve dependencies
var notifier = serviceProvider.GetService<INotifier>();
var notificationService = new NotificationService();
//メソッドインジェクション
notificationService.SendNotification(notifier, "Hello World!");
}
}
先輩: 特定のメソッドが呼ばれる時に、そのメソッドの引数として部品を渡す方法だよ。
利点:
- そのメソッドが呼ばれる時だけ部品が必要、という場合に柔軟に対応できる
欠点:
- 依存関係がメソッド内に限定されてしまう
- 多くの部品が必要だと、メソッドの引数がどんどん増えて複雑になる可能性がある
後輩: なるほど!基本は「コンストラクタ インジェクション」で、実行時の安全性を確保しつつ、状況に応じて「プロパティ」や「メソッド」のインジェクションも考える、という感じですね。違いがスッキリしました!
先輩: その安全性がいつ確保されるか、という違いが、CI が推奨される最大の理由なんだ。この違いを、車の組み立てに例えて解説してみよう。
車の組み立てに例えてみよう!
コンストラクタ インジェクション(CI):厳格な工場出荷検査
| 要素 | 役割 | 安全性 |
|---|---|---|
| 部品(エンジン、タイヤ) | 必須の依存関係 | コンパイル時の安全性 |
| コンストラクタ | 車のメインフレームへの取り付け口 | 部品の注入を強制する |
先輩: コンストラクタ インジェクションは、最も安全性が高い。これは、車が工場から 出荷される前、つまりコードをビルドする段階(コンパイル時) に、エンジンやタイヤといった走行に絶対必要な部品が揃っているかを厳しくチェックする仕組みに似ている。
後輩: エンジンやタイヤは、その車(クラス)の動作に必須の部品ですね。
先輩: その通り。もし組み立て担当者(開発者)がエンジン(必須の依存関係)の設定を忘れた場合、車はそもそも完成せずに出荷ラインでエラーとなり停止するんだ。
後輩: なるほど!CI ならコンパイルの時点でエラーが検出され、 公道(実行時) に出てから部品不足で立ち往生するリスクがない。必須の部品はCIで強制する、というのがよく分かりました。
プロパティ インジェクション(PI):オプション装備の取り付け忘れ
| 要素 | 役割 | 安全性 |
|---|---|---|
| 部品(カーナビ、サンルーフ) | オプションの依存関係 | 実行時のリスク |
| プロパティ | 後から取り付けられる追加の接続ポート | 部品の注入が強制されない |
先輩: 次にプロパティ インジェクション(PI)だけど、これは柔軟性が高い反面、実行時のリスクを伴う。
後輩: 柔軟性、というのは?
先輩: これは、車体は完成しているが、必ずしも必要ではない、オプションの部品、例えば最新のカーナビやサンルーフの接続ポートを後から取り付けるようなものだ。
先輩: この部品の注⼊は強制されない。そのため、カーナビを取り付け忘れても(部品が設定されていなくても)、車体は完成し、公道(実行時)に出ること はできる(つまりコンパイルは成功する)。
後輩: ああ!コンパイルは通ってしまうけれど、実行時の安全性が低いんですね。
先輩: まさにそう。ドライバー(ユーザー)が 走行中(実行時) にカーナビのボタンを押した瞬間、必要な部品(依存関係)が見つからずにエラー(Null参照など)が発生し、機能不全に陥る。問題がユーザーがその機能を使うまで露呈しないから、テストが失敗するリスクも高くなるんだ。
メソッドインジェクション(MI):特定のアクションに必要なツールボックス
| 要素 | 役割 | 安全性 |
|---|---|---|
| 部品(ジャッキ、工具) | 特定のメソッド実行時に必要な依存関係 | スコープ限定の実行時チェック |
| メソッドの引数 | 特定の作業を行う際の受け渡し口 | 部品の注入をその場で強制する |
先輩: 最後にメソッドインジェクションは、特定のタスクを実行する時だけ必要な部品を渡す方法だったね。
後輩: メソッドの引数として渡すやり方ですね。
先輩: これは、車本体の部品ではなく、特定の作業を行うために必要な工具箱(依存関係)を、その作業を始める直前(メソッド呼び出し時)に渡すことに似ている。
先輩: 例えば、「タイヤ交換」というメソッドを呼ぶ際にジャッキ(必要な部品)を渡さなければ、その場で作業は失敗する。しかし、その失敗は 他の走行機能(他のメソッド)には影響を与えない。 影響が局所的であるという特徴を持つんだ。
後輩: 車の例えで、安全性の違いがすごくクリアになりました!必須の部品(依存関係)はコンストラクタインジェクションでコンパイル時にチェックし、実行時の事故を防ぐのが基本、という設計思想がよく理解できました!
依存関係の分離
先輩: DIの根本的な目的は、クラス同士のべったりな関係を切り離すこと、つまり 「依存関係の分離」 にあるんだ。これによって、素晴らしいメリットが生まれる。
- テストしやすくなる: 部品を個別にテストできるから、品質が上がる
- 再利用しやすくなる: 部品が独立しているので、他の場所でも使い回せる
- メンテナンスしやすくなる: コードの繋がりが明確になり、修正が楽になる
ライフタイムスコープ
後輩: AddTransient、AddScoped、AddSingleton…この3つの使い分けがよく分からないです。
先輩: それが サービスの「ライフタイムスコープ」 と言って、「DIコンテナがオブジェクトをいつ作って、いつまで使い回すか」を決める重要な設定なんだ。それぞれ解説するね。
1. Transient (一時的)
先輩: Transient は、DIコンテナに「ちょうだい!」と要求されるたびに、毎回新しいインスタンスを生成して渡す仕組みだよ。軽くて、状態を持たない(ステートレスな)サービスや、一時的な計算処理なんかに向いている。
services.AddTransient<IService, Service>();
2. Scoped (スコープ限定)
先輩: Scoped は、「特定の期間内では、ずっと同じもの」を使い回す仕組みだ。Webアプリケーションの場合、1 回のリクエスト(ユーザーがページを開いてから表示されるまで)の間は同じインスタンスが使われる。リクエストが終わると、そのインスタンスは破棄されるんだ。データベースの接続情報など、リクエスト内で共有したい情報を扱うのに適しているよ。
services.AddScoped<IService, Service>();
3. Singleton (単一)
先輩: Singleton は、「アプリケーション全体で、たった一つ」しか存在しないオブジェクト。一度作られたら、アプリケーションが終了するまでずっと同じインスタンスが使い回されるんだ。
アプリケーション全体で共有する設定情報や、生成にコストがかかる重いサービスなどに使われることが多いね。ただし、使い方を間違えるとメモリを圧迫し続けたり、意図しない問題を引き起こしたりするから、慎重に使う必要があるんだ。
services.AddSingleton<IService, Service>();
DIコンテナのパフォーマンス最適化
後輩: 先輩、DI コンテナってすごく便利ですけど、サービスをたくさん登録していくと、アプリケーションの起動が遅くなったり、動作が重くなったりしませんか?パフォーマンスが少し気になります。
先輩: 鋭い指摘だね。DI コンテナは強力な反面、使い方を考えないとパフォーマンスの低下を招くこともあるんだ。パフォーマンスを最適化するためのポイントがいくつかあるから、それを押さえておこう。
-
ライフタイムの適切な選択
先輩: 一番大事なのはこれだね。頻繁に使うけど状態を持たないステートレスなサービスやスレッドセーフなサービスは、 Singleton にする。そうすれば、インスタンスの生成がアプリケーションのライフサイクルで一度きりになるから、毎回生成するコストを削減できる。逆に、リクエストごとに状態が変わるようなものは Scoped を選ぶ。この使い分けが基本だよ。
services.AddSingleton<ISingletonService, SingletonService>();
services.AddScoped<IScopedService, ScopedService>();
services.AddTransient<ITransientService, TransientService>();
-
サービスの初期化を遅らせる
先輩: アプリケーションの起動時に、全てのサービスを一斉にインスタンス化すると、その分起動が遅くなってしまう。DI コンテナの設定を工夫すれば、そのサービスが本当に必要になった時に初めてインスタンスを生成する 「遅延初期化」 ができるんだ。これで起動パフォーマンスを改善できることがある。
services.AddSingleton<ISingletonService>(provider =>
{
return new SingletonService();
});
-
依存関係をシンプルに保つ
先輩: 一つのクラスが、あれもこれもと多くのサービスに依存していると、それだけインスタンスを生成する際の連鎖が長くなって複雑になる。本当に必要なサービスだけを注入するように、クラスの責務を適切に分割して、依存関係をシンプルに保つよう設計を心がけることが大切だよ。
public interface ISimpleService1
{
public void Operation();
}
public interface ISimpleService2
{
public void Operation();
}
public class SimpleService1 : ISimpleService1
{
public void Operation()
{
Console.WriteLine("SimpleService1");
}
}
public class SimpleService2 : ISimpleService2
{
public void Operation()
{
Console.WriteLine("SimpleService2");
}
}
public class ComplexService(ISimpleService1 service1, ISimpleService2 service2)
{
private readonly ISimpleService1 _service1 = service1;
private readonly ISimpleService2 _service2 = service2;
public void PerformOperation()
{
_service1.Operation();
_service2.Operation();
}
}
public class Program
{
static void Main(string[] args)
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<ISimpleService1, SimpleService1>();
serviceCollection.AddTransient<ISimpleService2, SimpleService2>();
serviceCollection.AddTransient<ComplexService>();
var serviceProvider = serviceCollection.BuildServiceProvider();
var complexService = serviceProvider.GetService<ComplexService>();
complexService.PerformOperation();
}
}
後輩: この例では、ComplexServiceが必要とする依存関係が明確に分離されているんですね。
-
パフォーマンスをモニタリングする
先輩: もしパフォーマンスに問題を感じたら、やみくもに修正するんじゃなくて、まずは計測することが重要だ。
Application Insights や PerfView のような監視ツールを使えば、どのサービスの生成に時間がかかっているのか、ボトルネックを特定できるからね。
後輩: なるほど…!ただ登録するだけじゃなくて、ライフタイムを賢く選んで、初期化のタイミングを考えて、依存関係はシンプルに、ですね。パフォーマンスを意識した設計が重要なんだと分かりました!