目的
Dependency Injection Principles, Practices, and Patternsの1章 「The basics of Dependency Injection: What, why, and how」のメモ。何気なく使っているDependency Injectionについて理解を深めていきたい。
よくある勘違いと間違いの理由
遅延バインディングによるメリットだけ
DataBaseを利用するアプリケーションがあるとする。あるタイミングで、利用するDBがSQL ServerからPostgreSQLになったとする。再コンパイルなしでDBアクセスのインターフェースを変えるためには、設定ファイルなどで接続方法に関する設定を保持し、それを利用して切り替えるようにすることができる。DIを使うことで、このようなユースケースに対応できるようになる。しかし、これは、DIの持つ一つのメリットであって、これがすべてということではなく、そこを取り違えて考えていることがある。ということ。
Unit Testがしやすくなるというメリットだけ
DIを利用することで、サービス間の呼び出しが疎結合になるため、UnitTestが実行しやすくなるというメリットがある。これについても、DIはUnit Testをサポートするためだけの方式と勘違いしているケースがある。Unit Testの効率化についてもDIが提供する数あるメリットのうちの一つであることを認識する必要がある。
DI=Abstract Factoryの強化版
Abstract Factory => Service Locator。Service locator は、DIの考え方の真逆に位置する考え方。ここよくわからなかった。後ろの章で詳しい説明が出てくるらしい。
DIにはDIContainerが必須
DIは、概念、方針である。一方、DI Containreは、開発を助ける補助的な機能である。DIを利用するうえで、DI Containerは必須ではない。
DIのメリット
Late binding:遅延バインディング
利用するサービスを変えることで挙動を変えることができる。例えば、メッセージを出力するためのインターフェースと、それを実装したクラスが2つあるとする。
public class IMessageWriter{ void Write(string message); } public class ConsoleMessageWriter:IMessageWriter{ void Write(string message){ Console.Write(message); } } public class FileMessageWriter:IMessageWriter{ void Write(string message){ File.WriteAllText(@"D:\FilePath.txt", message); } }
はじめは、Console出力に対応した実装を利用していたが、あるとき、出力先がファイルに変更された。
ConsoleMessageWriterを直接Newする(early binding)していた場合、コードを修正してリビルドする必要がある。
public void Main(args[]){ var writer = new ConsoleMessageWriter(); writer.Write("Hi"); } //↓に書き換える public void Main(args[]){ var writer = new FileMessageWriter(); writer.Write("Hi"); }
一方、DIを採用した場合、参照先のクラス設定を変更するだけで対応が可能となる。
//呼び出し元の例 public void Main(args[]){ IConfigurationRoot configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .Build(); string typeName = configuration["messageWriter"]; Type type = Type.GetType(typeName, throwOnError: true); }
この例の場合、appsettings.jsonに記載しているIMessageWriterの実装済みクラスの情報を変更するイメージ。
{ "messageWriter": "<Name Space>.<Class Name>, <Project Name>" }
このように、実装したサービスの付け替えができることがLate Bindingのメリットである。今回の例の場合、リビルドなしに付け替えができるが、そもそも今の時点で実装されていないもの(例えば、データベースへのメッセージ保存)が必要となったときは、そのサービスの実装が必要になるため、当然ビルドも必要となる。ここでは、どのサービスが使われるかの設定のみを変更することで、同じインターフェースを利用しているすべての箇所に変更が反映される点がメリットということだと思う。上記例では1か所の変更だけであるため、Early の場合とLateの場合での違いが分かりにくいが、修正箇所が多くなったらその違いが明確になるはず。
Extensibility:拡張性
上記の例の続き。Consoleにメッセージを出力する仕様に追加で、認証されたユーザーのみがメッセージを出力する仕様が追加されたとする。
疎結合な実装をするためには、1つのサービスが担う役割を1つにするように心がけるとよい。そのため、コンソールにメッセージを出力するための”ConsoleMessageWriterに”ではなく、認証をつかさどる新しいSecureMessageWriterに認証機能を実装し、問題がなければ、ConsoleMessageWriterを呼び出すというように実装する。
こうすることで、IMessageWriterインターフェースを利用している個所と、すでにテスト済みのConsoleMessageWriterの実装を変更することなく新しい機能(認証)を追加することができる。
open for extensibility, but closed for modification.
Parallel development:並行開発
引き続き、上記の例を利用する。
ConsoleMessageWriterやSecureMessageWriterのように、疎結合を保つことができると、複数の開発者での並行開発が効率よくできる。
例えば、上記2つのクラスをAさん、Bさんで開発した場合のことを考える。これらのクラスが密結合であった場合、ConsoleMessageWriterクラスを担当するAさんが実装につまった場合、それを呼び出したいBさんのSecureMessageWriterクラスの実装も詰まってしまう。一方、疎結合であれば、Aさんの実装を待たずに、BさんはMockを利用して自分の責任範囲である認証部分のテストを開始できるようになる。大規模開発になれば、より効果が発揮されるようなシナリオ。
Maintainability:メンテナンス性
1つのサービスが担う役割を1つにするように心がけることで、メンテナンス性が向上する。
機能の拡張のしやすさについては、上記で言及済み。
さらに、トラブルシューティングの場面においても、どのサービスが原因かを特定しやすくなる。
Testability:テスト容易性
サービスが担う役割に対してUnit Testを実装していけるため、メンテナンス性の高いUnit Testの構成をとることができる。 サービスごとは疎結合となるため、他のサービスが未実装であっても、同じインターフェースを利用するMockやStubを作成することで、テスト作業を進めることができる。
Michael Feathers even defines the term legacy application as any application that isn’t covered by unit tests. (Prentice Hall. 2004)
DIすべきもの、すべきでないもの
Dependencyには、Stable dependencyとVolatile dependencyの概念がある。
Stable dependency
BCLなどのようなものがこれに該当する。BCLは再利用可能であり、どんな環境(.NET Core Runtimeがある環境)で実行することができる。Stable Dependencyであるかを判定するチェックポイントは以下の4点。 - 自分の開発が始まる時点ですでに利用可能になっているもの。 - 破壊的な変更がないことが想定できるもの。 - 決定論的な結果が取得できるもの。 - クラスまたはモジュールを別のものに置き換えたり、ラップしたり、装飾したり、インターセプトしたりする必要がないもの。
BCLのほかにも、サードパーティー製のライブラリや、社内の秘伝のライブラリなどもこれにあたる(可能性が高い)。
Volatile dependency
Volatile dependencyは、開発中のライブラリであったり、決定論的な結果が取得できない処理を持っているものが該当する。”決定論的な結果が返ってこない”の代表例としては、現在時刻を返したり、ランダムな値を返したりするような処理があげられる。こういったものは、Unit Testのタイミングで問題が顕著化する。複数回実行しても結果が同じことを検証したいのに反し、結果が予想できないためである。
Volatile Dependencyと判定するためのチェックポイントは以下の通り。
- 実行環境の影響を受けるもの。例えば、DBアクセス処理など。
- 開発中で、まだ利用可能となっていないもの。
- 開発環境すべてで利用可能なものではないもの。例えば、サードパーティー製のライブラリで、ライセンス料金が高額なため、特定の開発者しか利用していないものなど。
- 決定論的な結果が返ってこないもの。
Unit Testの容易性を保つためにも、これらのサービス(実装)を疎結合に保つことに焦点を充てるのがDIにとって大切なポイント。
まとめ
- DI自体は、ソフトウェア開発における設計原則、方式の集合体である。
- DIの目的は、クラス間の依存を疎結合にたもち、コード群のメンテナンス性を高めるものである。
- リスコフの置換原則によって、DIが成り立っている。この原則により、サービスの置換が可能となる。
- Dependencyには、Stable dependencyとVolatile dependencyの概念がある。DIにおいて注目すべきはVolatile dependencyのほう。
- 既存のコード(DIを導入していないもの)に対してDIを導入するのはハードルが高い。
Refernce
Seemann, Mark; van Deursen, Steven. Dependency Injection Principles, Practices, and Patterns (p.22). Manning Publications. Kindle 版.