こう言うと「私はDIコンテナーなんて使っていない!アンチDIだ!」とおっしゃる人もいるかもしれません。
まぁ落ち着いてください。今回は、DIコンテナーは登場せず、Dependency Injection(以後DI)パターンのお話です。
DIパターンとは、つぎのようなものだと私は考えています。
「依存性を外部から注入することで、ふるまいを変更する設計パターン」
詳細はコードを見つつ解説します。コードはC#で記載していますが、だれでも読めるレベルです。たぶん言語が違えど似たようなコードは誰もが書いたことがあるはずです。
ということで、さっそく見ていきましょう!
あなたも使っているDIパターン
お題
「何らかのリソースから文字列を読み取り、コンソールに出力する」
もうネタはバレたかもしれません。
ひとつめのDI
さて、まずはローカルストレージ上のテキストファイルを読み込んでコンソールに出力してみましょう。以下のコードをご覧ください。
class Program
{
static void Main(string[] args)
{
// ローカルの「README.txt」ファイルを読み取り専用で開く
using var stream = new FileStream("README.txt", FileMode.Open);
WriteConsole(stream);
}
static void WriteConsole(Stream stream)
{
// ストリームから文字列を読みだすため、StreamReaderを生成する
using var reader = new StreamReader(stream);
Console.WriteLine(reader.ReadToEnd());
}
}
シンプルなコードですが、明らかにDIパターンが適用された設計になっています。詳しく見ていきましょう。
ここでは代表的なクラスとして、Program、FileStream、Stream、StreamReaderの4つのクラスが登場します。それらの関係は、つぎのようになっています。
FileStreamはファイルへの入出力を提供するStreamの実装クラスです。
Streamは何らかのリソースへの入出力を提供する「ストリーム」を表す抽象クラスです。Streamは必ずしもテキストリソースだけを扱う訳ではなく、画像などのバイナリソースも扱うため、バイト列をもちいます。
StreamReaderクラスは、Streamからバイト列を取得し、デコードして利用者に文字列を提供します。StreamReaderクラスはバイト列をどのリソースからどのように取得するのか、一切関与しません。そのため抽象的なStreamのみに依存し、実装クラスであるFileStreamには依存しません。
Programクラスはこれらを組み合わせて、ローカルファイルを読み取ってコンソールへ出力しています。
StreamReaderに、抽象的な依存性(Stream)を注入しており、紛れもなくDIパターンが採用されています。
ふたつめのDI
さて、ある時ローカルファイルではなく、Web上のリソースをコンソール出力したくなったとします。
そこで、あなたはつぎのようにコードを書き換えました。
class Program
{
static async Task Main(string[] args)
{
//using var stream = new FileStream("README.txt", FileMode.Open);
// HttpClientを利用してURL「https://www.google.com/」上のリソースを開く
using var httpClient = new HttpClient();
await using var stream = await httpClient.GetStreamAsync("https://www.google.com/");
WriteConsole(stream);
}
static void WriteConsole(Stream stream)
{
// ストリームから文字列を読みだすため、StreamReaderを生成する
using var reader = new StreamReader(stream);
Console.WriteLine(reader.ReadToEnd());
}
}
FileStreamの生成をコメントアウトし、HttpClientのGetStreamAsyncメソッドを利用して、指定アドレスからバイト列を読み取るためのStreamを非同期に取得します。
クラス間の関係はつぎのようになっています。
ここでもHttpClientから取得された抽象的なStreamを、StreamReaderに注入しており、DIパターンが踏襲されていることが見て取れます。
あなたも使っているDI
こんなパターンのDIであれば、あなたも一度は利用したことがあるのではないでしょうか?
実際、こういったパターンの設計は標準ライブラリにもよく見られます。オブジェクト指向言語をつかっている方であれば、どこかでDIをつかっているはずです。
別に構えるほど特別なものではないことに、共感いただけるのではないでしょうか。
ところで、本エントリーではDIコンテナーは登場しません。こんな言葉はありませんが「手組みDIパターン」です。DIコンテナーはDIパターンを利用するための道具であって、DIパターンを構成する必須要素ではありません。
あらためてDIパターンとは何か?
DIパターンの特徴
DIパターンとは、抽象的な依存性を、外部から注入することで、ふるまいを変える設計パターンです。
DIパターンの目的
DIパターンで、ふるまいを変える「目的」はつぎのものを得るためです。
- 再利用性
- 拡張性
- 保守性(レイヤー間の疎結合など)
- テスト容易性
など、ほかにもあります。英語のWikiが良くまとまっているので見てみるのも良いでしょう。
これらの目的を実現するための代表的なひとつの「手段」がDependency Injection Patternです。
DIパターンと同じ目的を実現する他の手段
もちろん手段はひとつではありません。
DIパターンの対抗となる代表的なパターンはService Locatorパターンです。これはDIが誕生した当初から議論されていることです。FactoryなどもService Locatorと大きな違いはありません。
これらを比較したときのメリット・デメリットは簡単には語り切れませんが、ここでは代表的なケースについて簡単に記載します。
DIのデメリット
Service Locatorと比較したとき、「難しい」ことだと私は思っています。習熟するとその難しさから遠ざかってしまいがちですが、そこから目をそらすべきではないでしょう。
Service Locatorパターンは依存先のオブジェクトを利用する箇所で、依存先のオブジェクトを構築(もしくは取得)します。
対してDIパターンでは、依存先オブジェクトを利用する個所と、依存先オブジェクトを構築する個所が分離しています。
これがDIパターンを難しくしている本質です。
Service Locatorパターンでは普通にオブジェクトをnewして利用する代わりに、Service Locatorから取得して利用するだけで、そこに大きなパラダイムの変化はありません。これはDIと比較して「簡単な」解決策です。
DIのメリット
逆にService Locatorでは解決できないケースもあります。そして今回のケースはこれに該当します。
DIやService Locatorの目的は「抽象的な依存性を切り替えることにより、ふるまいを変えること」です。
しかしService Locatorの場合、ふるまいを変えられる「幅」に、DIよりも制限があります。
「ファイルとWeb上のリソースを読み取るためのリーダークラス」はService Locatorパターンでも作れるかもしれません。ファイルのアドレスもURLも文字列ですしね。
しかし「開発対象のシステム専用のBLOBストレージに格納された、バイナリオブジェクトを読み取れるよう拡張できるStreamReaderクラス」を作ることはService Locatorパターン単独で解決することは難しいでしょう。DIよりトリッキーなコードか、Service Locator(依存性)をInjectionするか、いずれか必要になりそうです。もちろんこの例のStreamReaderであれば、専用のStreamさえ作れば簡単に実現できます。
Service Locatorの難しさ
DIは難しいと書きましたが、逆にService Locatorの方が難しくなるケースもあります。
とくにユニットテストでは顕著です。
Service LocatorでMockを解決しようとした場合、依存オブジェクトの利用箇所から分離された箇所で、Mockに差し替える必要があります。これは先に書いたDIの難しさとまったく同じものです。とはいえ、DIよりはそれらの個所は近いです。
またテストケースをマルチスレッドで実行したいといった場合、Service Locatorをマルチスレッド対応する必要があります。Thread-Specific Storageパターンを利用して解決できるでしょうけど、「難しい」話しです。
まとめ
- DIパターンとは、依存性を外部から注入することで、ふるまいを変えるパターンです
- DIパターンの目的は、以下を得ることです
- 再利用性
- 拡張性
- 保守性(レイヤー間の疎結合など)
- テスト容易性
- などなど
- 多くはService Locatorパターンなどで代替が可能ですが、代替できないケースもあります
- 依存性の利用箇所だけ見ると、Service Locatorパターンの方が簡単です
- テストを考慮するとDIの方が簡単なこともよくあります
- DIコンテナーはDIパターンをサポートするツールで、DIパターンそのものではありません
結局は使い分けなんですが、個人的にはService Locatorじゃないといけない場合を除き、DIパターンを利用することが多いです。「慣れれば」そんなに難しいものではないですし、過去のXML Hellみたいなことは現代のDIにはありませんしね。
ということで以上です。よいDIライフを!