LoginSignup
109
110

More than 3 years have passed since last update.

あなたも使ってるDIから理解するDIパターン

Last updated at Posted at 2020-07-13

こう言うと「私は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.jpg

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.jpg

ここでも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ライフを!

109
110
16

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
109
110