Dependency Injection解説 と C#における実装例
概要
Dependency Injection解説 と C#における実装例を紹介しようと思います。
C#を使っていますが考え方は他のオブジェクト指向型プログラミング言語でも応用できます。
次回の記事はUnityEditor向けDependency Injection講座の予定です!
DIの本質
DIの本質は以下のように言えます。
抽象オブジェクトに依存させ、その依存オブジェクトを注入によって柔軟に切り替え可能にする
上記のことを意識しながら以下の解説を読んでください。
DIに関連の深いキーワード
- 依存注入
- 疎結合
- 実体ではなく抽象と結合していること
- 抽象への依存
Dependency Injection の例
Dependency Injectionは直訳すると「依存注入」となります。
どうやって依存注入を実現するのか簡単な例を紹介します。
DIを使用しないパターン
以下のMessenger
クラスはRecv
メソッドを経由してメッセージを受け取る構成です。
using System; public class Messenger { public void Recv(string message) { Console.WriteLine(message); } }
このMessenger
クラスはConsole.WriteLIne
を直接コーディング(ハードコーディング)しており、そのメソッドが属する名前空間System
が必要となります。
これはSystem.Console.WriteLine
と蜜結合している状態と言えます。
DIを使用するパターン
上記Messenger
のDI対応をしましょう。
本質で述べたように抽象オブジェクトに依存させます。
DIを実装する場合はよくinterface
が使用されます。 edrfvb
今回は「stringを受け取って何かをする」interface ILogger
を用意します。
public interface ILogger { void Log(string message); }
そして、Messenger
にはSystem.Console.WriteLine
ではなくILogger
に依存させましょう。
public class Messenger { private ILogger _logger; // コンストラクタでILoggerの実体を受け取る public Messenger(ILogger logger) { _logger = logger; } public void Recv(string message) { // ILogger.Logをコール _logger.Log(message); } }
Messenger
クラスのコードは多くなりましたが、以下の特徴を持つようになりました。
Recv
の動きはILogger.Log
に依存System.Console.WriteLine
への蜜結合が解消
DI対応したMessenger
は依存するILogger
をコンストラクタ経由で注入されるようになりました。
これが「依存注入(Dependency Injection」と呼ばれる理由です。
実体ではなく抽象オブジェクトに依存させるため、Loggerとは疎結合している状態と言えます。
Loggerを実装して注入
Console.WriteLineを実施するLogger
これはいわゆる「DIを使用しないパターン」と同じ挙動となるLogger
using System; public class ConsoleLogger : ILogger { public void Log(string message) { Console.WriteLine(message); } }
ちなみに以下のように使用します。
var injectedLogger = new ConsoleLogger(); var messenger = new Messenger(injectedLogger); messenger.Recv("Hello, Messenger!");
コンソールに"Hello, Messenger!"
と出力されます。
ファイルにメッセージを追記していくLogger
指定されたパスのファイルにmessageを追記しているLogger
public class FileLogger : ILogger { private string _filePath; public FileLogger(string filePath) { _filePath = filePath; } public void Log(string message) { File.AppendAllText(_filePath, message); } }
var injectedLogger = new FileLogger(@"C:\test.txt"); var messenger = new Messenger(injectedLogger); messenger.Recv("Hello, Messenger!");
C:\test.txt
に"Hello, Messenger!"
と追記されていきます。
メッセージを無視するLogger
あえて何もしないLogger
public class DummyLogger : ILogger { public void Log(string message) {} }
var injectedLogger = new DummyLogger(); var messenger = new Messenger(injectedLogger); messenger.Recv("Hello, Messenger!");
DummyLogger.Log
は何もしないため、messenger.Recv
をコールしても何も起きません。
何に使うかというと、Messenger.Recv
で何もさせたくないとき、もしくはさせることがないときに使います。
Messenger
のコンストラクタにnullを渡すとRecv
時にNullReferenceException
が発生してしまいます。
何もさせることがないけどnullを渡すとまずいときにDummyLogger
のような何もしないLoggerを渡してやれば問題は起きません。
依存注入でnullが渡された場合の対策
今回作成したMessenger
ですが、nullを渡されたときに問題が起きないようにしておいた方が安全です。
_loggerがnullの時は処理を無視する方法ももちろん可能ですが。。。
public void Recv(string message) { _logger?.Log(message); }
今回はせっかくDummyLogger
を作ったので、個人的にはコンストラクタを以下のように修正するのがオススメです!
loggerにnullが渡された場合はDummyLoggerを使用させます。
public Messenger(ILogger logger) { _logger = (logger != null) ? logger : new DummyLogger(); }
C#にはコンパイラレベルでNull非許容型などがありませんので、現状はルール化、もしくは上記のような対応が必要になります。
KotlinなどのようなNull Safetyの概念が入れてば不要になると思います。
DIの利点
DIを導入することの利点をいくつか紹介します
SOLID原則における「open/closed principle」を満たせる
平たく言うと柔軟にクラスの挙動をクラス自体を修正することなく変更できるようになります。
- Messengerの挙動の追加はLoggerを作成して注入することで対応(拡張に対してOpen)
- Messenger自体の修正は不要(修正に対してClose)
テスタビリティ(Testability)を満たせる
UnitTestなどのテストコード作成への対応が可能になります。
(逆にDIを知らない場合はUnitTestやテスタビリティのあるクラス設計が難しい気がします)。
例えば以下のようにMessenger.Recv
にnullが渡されたときは動作が無視されるとしましょう。
public class Messenger { public void Recv(string message) { if (message == null) { return; } Console.WriteLine("" + message); } }
if文による条件分岐のテストはコードを実行してわざわざ人力でコンソールを見ないといけません。
今時そんな事やってられないので、UnitTestに対応させてテストを自動化させます。
そんなときもDIに対応させておくことで、UnitTestにも柔軟に対応できます!
public class Messenger { private ILogger _logger; // コンストラクタでILoggerの実体を受け取る public Messenger(ILogger logger) { _logger = logger; } public void Recv(string message) { if (message == null) { return; } _logger.Log(message); } }
DIの種類
DIの実装方法はいくつかあります。
都合の良い方法で実装してあげてください。
Constructor Dependency Injection
コンストラクタでDIを行う方法です。
上記で説明したDIはこちらになります。
インスタンス生成後は依存の切り替えはなしということを明示します。
※コードは省略
Method/Property Dependency Injection
メソッドやプロパティ経由でDIする方法です。
public class Messenger { private ILogger Logger { get; set; } public void Recv(string message) { Logger.Log(message); } }
インスタンス生成後でも依存の切り替え可能ということを明示します。
雑感
今回はDIの解説を入れてみました。
本当はUnityにおけるDIを解説したかったんですが、DIの話が長くなったので分割することにしました。
そもそもDIに関する記事って普通に多いのでいらないんじゃね?とも思ったんですが、自分なりの解説記事も出してみたくなったので作ってみました。
皆さんのエンジニアリングの参考になれば幸いです。
本題は次回の「UnityEditor向けDependency Injection講座」です!
お楽しみに~