依存性の注入 (DI) の基礎
CoddyのC#ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 46/70。
依存性の注入 (Dependency Injection: DI) は、クラスが自身で依存関係を作成するのではなく、外部からそれらを受け取る手法です。これにより、コードはより柔軟で、テスト可能になり、疎結合になります。
DIなしでは、クラスは独自の依存関係を作成するため、変更やテストが困難になります:
public class OrderService
{
private EmailSender sender = new EmailSender(); // 密結合
public void PlaceOrder() => sender.Send("Order placed");
}DIを使用すると、依存関係はコンストラクタを介して渡されます:
public interface IMessageSender
{
void Send(string message);
}
public class EmailSender : IMessageSender
{
public void Send(string message) => Console.WriteLine("Email: " + message);
}
public class OrderService
{
private IMessageSender sender;
public OrderService(IMessageSender sender)
{
this.sender = sender;
}
public void PlaceOrder() => sender.Send("Order placed");
}これで、OrderService は電子メール、SMS、またはテスト用のモック送信者のどれを使用しているかを関知しません。依存関係は、オブジェクトの作成時に「注入」されます:
IMessageSender emailSender = new EmailSender();
OrderService service = new OrderService(emailSender);
service.PlaceOrder(); // 電子メール: 注文が完了しましたこのパターンは、具象的な実装ではなく抽象(インターフェース)に依存するという原則に従います。これにより、それらを使用するクラスを変更することなく実装を入れ替えることが可能になり、ユニットテストや柔軟なアーキテクチャの維持に不可欠です。
チャレンジ
簡単依存関係の注入(Dependency Injection)の威力を示すロギングシステムを構築しましょう。ログの書き込み方法をハードコーディングする代わりに、コンソール、ファイルシミュレーション、あるいはその他の出力先であっても、その詳細を知ることなく動作する柔軟な Logger クラスを作成します。
コードを以下の3つのファイルに整理します:
ILogWriter.cs:Logging名前空間にILogWriterというインターフェースを定義します。このインターフェースは、あらゆるログ出力先のコントラクト(契約)を表し、メッセージがどこに書き込まれたかを説明する文字列を返す単一のメソッドWrite(string message)を持つ必要があります。LogWriters.cs:Logging名前空間にILogWriterの2つの具体的な実装を作成します:ConsoleLogWriter- そのWriteメソッドは"Console: " + messageを返します。FileLogWriter- コンストラクタを通じてファイル名を受け取り、そのWriteメソッドは"File[" + filename + "]: " + messageを返します。
Logger.cs: コンストラクタ注入(Constructor Injection)を通じて依存関係を受け取るLoggerクラスをLogging名前空間に作成します。Loggerは以下の条件を満たす必要があります:- コンストラクタを通じて
ILogWriterを受け取り、それをプライベートフィールドに保存する。 - 注入されたライターに処理を委譲し、その結果を返す
Log(string message)メソッドを持つ。
- コンストラクタを通じて
Program.cs:ConsoleLogWriterを持つロガーとFileLogWriterを持つロガーの2つを作成することで、依存関係の注入を実演します。注入される依存関係によって、同じLoggerクラスがどのように異なる動作をするかを示してください。
2つの入力を受け取ります:
- ファイルロガー用のファイル名
- ログに記録するメッセージ
ConsoleLogWriter を使用して Logger を作成し、メッセージをログに記録します。次に、FileLogWriter(入力されたファイル名を使用)を使用して別の Logger を作成し、同じメッセージをログに記録します。それぞれの結果を個別の行に出力してください。
例えば、入力が app.log と System started の場合、出力は以下のようになります:
Console: System started
File[app.log]: System startedLogger クラスが一切変更されていないことに注目してください。これはあらゆる ILogWriter の実装で動作します。これが依存関係の注入の本質です。Logger は抽象(インターフェース)に依存しており、具体的な実装は外部から「注入」されます。後で Logger クラスを全く修正することなく、DatabaseLogWriter や CloudLogWriter を簡単に追加することができます!
チートシート
依存性の注入(DI)は、クラスが自身で依存関係を作成するのではなく、外部からそれらを受け取る手法であり、コードをより柔軟でテスト可能にし、疎結合にします。
DIなし(密結合):
public class OrderService
{
private EmailSender sender = new EmailSender(); // Tightly coupled
public void PlaceOrder() => sender.Send("Order placed");
}DIあり(疎結合):
public interface IMessageSender
{
void Send(string message);
}
public class EmailSender : IMessageSender
{
public void Send(string message) => Console.WriteLine("Email: " + message);
}
public class OrderService
{
private IMessageSender sender;
public OrderService(IMessageSender sender)
{
this.sender = sender;
}
public void PlaceOrder() => sender.Send("Order placed");
}依存関係の注入:
IMessageSender emailSender = new EmailSender();
OrderService service = new OrderService(emailSender);
service.PlaceOrder(); // Email: Order placedDIは、具体的な実装ではなく抽象(インターフェース)に依存するという原則に従っており、それを利用するクラスを修正することなく実装を入れ替えることを可能にします。
自分で試してみよう
using System;
using Logging;
class Program
{
public static void Main(string[] args)
{
// 入力を読み込む
string filename = Console.ReadLine();
string message = Console.ReadLine();
// TODO: ConsoleLogWriter を使用して Logger を作成し、メッセージをログに記録する
// 結果を出力する
// TODO: FileLogWriter を使用して(filename を使用)別の Logger を作成し、同じメッセージをログに記録する
// 結果を出力する
}
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
5ポリモーフィズムとインターフェース
コンパイル時 vs 実行時ポリモーフィズムインターフェース vs 抽象クラス複数のインターフェース明示的なインターフェースアップキャストとダウンキャストまとめ - 図形計算機8オブジェクト指向の高度な概念
継承よりコンポジションジェネリクス (クラスとメソッド)デリゲートとイベント属性とリフレクションIDisposable と using ステートメント依存性の注入 (DI) の基礎