💉

Dependency Injectionのパターンをまとめる

2022/01/08に公開約5,400字

まとめるパターン

  • Dependency Injection を使わない書き方 (比較用)
  • Dependency Injection 本体
  • Poor Man's Dependency Injection
  • DIコンテナ
  • Service Locater
  • Illegitimate Injection

Adaptive Code ~ C#実践開発手法 第2版をベースにしつつ、技術ブログなど読んで自分なりに調整しています。
言語はJavaです。不慣れなのでもしかしたらミスがあるかも。もし見つけたら、そっと教えて下さい。

https://amzn.to/3vpMThY

Dependency Injectionを使わない書き方

何かを読み込み、何かをログに書き込むHogeクラスを例に考えます。

class CsvFileReader{...}  //csvファイルから情報を読むクラス
class MySQLLogger{...}    //MySQLにログを記録するクラス
class Hoge{
  public Hoge(){
    mReader = new CsvFileReader();
    mLogger = new MySqlLogger();
  }
  ...
  private CsvFileReader  mReader = nullptr;
  private MySQLLogger    mLogger = nullptr;
}

上のコードでは、読み込み元はcsvファイル、ログの書き込み先はMySqlです。
コンストラクタでCsvFileReader,MySQLLoggerのインスタンスを直接生成しています。

もしシンプルなアプリを作りたいだけなら、これで終わりでも問題ないかもしれません
しかし、もう少し柔軟性が求められる、例えば次のようなときに困りそうです

  • CIでHogeのテストをしたいが、MySQLLoggerのためにいちいちMySQLをセットアップする必要がある
  • 仕様変更でxmlからもデータを読み込めるようにする必要がある
    これを解決するため、Hogeが必要とするクラスをインターフェースによって抽象化し、依存性を実装から切り離す発想が生まれます。

Dependency Injection 本体

外部からインターフェースであるIReader,ILoggerを渡せるようにします。

//インターフェース
interface IReader{...};      //何かから情報を読むインターフェース
interface ILogger{...};      //何かにログを記録するインターフェース

class Hoge{
  public Hoge(IReader reader, ILogger logger)
  {
    mReader = reader;
    mLogger = logger;
  }
  ...
  private IReader mReader = null;
  private ILogger mLogger = null;
}

先の例と同じことを実現する場合、次のようにインターフェースを実装したCsvFileReader,MySQLLoggerを渡してHogeを生成します。

class CsvFileReader implements public IReader{...}
class MySQLLogger implements ILogger{...}

void CreateHoge()
{
  Hoge hoge = new Hoge(new CsvFileReader(), new MySQLLogger());
  ...
}

このように、あるクラスが依存するオブジェクトを内部ではなく、外部から渡す設計を Dependency Injection 略して DI 日本語では「依存性の注入」と呼びます。(直訳すぎてわかりにくいですが、要はオブジェクトを外から注入する、という意味合いです)

Poor Man's Dependency Injection

DIでインスタンスを生成する方法には、いくつかパターンがあります。
まず、一番シンプルな例を出します。とはいえ、これはすぐ上で書いた内容と同じです。

void CreateHoge()
{
  Hoge hoge = new Hoge(new CsvFileReader(), new MySQLLogger());
}

シンプルすぎてパターンの説明として不足するので、ちょっと前提を変更します。
CsvFileReaderが実は別のインターフェースIFileに依存し、 クラスCsvFileがIFileを実装していることにします。
CsvFileReaderはやはりDIを採用しており、外部からIFileを渡す設計とします。

interface IFile{...};
class CsvFile implements IFile{...};

void CreateHoge()
{
  IFile   file   = new CsvFile();
  IReader reader = new CsvFileReader(file); 
  Hoge    hoge   = new Hoge(reader, new MySQLLogger());
}

Hogeオブジェクトを生成するためにはIReader(ここではCsvFileReader)オブジェクトが必要
→ CsvFileReaderオブジェクトを生成するためにはIFile(ここではCsvFileReader)オブジェクトが必要
と、連鎖的に必要とするオブジェクトを全て生成し、引数として渡します。

この素朴な依存解決の方法をPoor Man's Dependency Injectionと呼ぶそうです。

DIコンテナ

Poor Man's Dependency Injectionでも十分に仕事ができていますが、オブジェクトの生成手続きを別クラスに集約し、より柔軟にするのがDIコンテナです。
DIコンテナには様々な実装方法があり、かつ具体的な実装内容を示すのがちょっと難しいクラスなので、色々ごまかしつつ、どんなメソッドがあるかだけ例示します。

class DIContainer{
  //インターフェースと、そのインターフェースに対応するクラスの生成方法を登録する
  public <T> void registerType(Class<T> t, Function<DIContainer, T>){...}
   
  //インターフェースに対応するクラスのオブジェクトを返す
  public <T> T resolve(Class<T> t){...}
  ...
};

先と同じ処理をこのDIコンテナを使って書いてみます。

void RegisterTypes(DIContainer container)
{
  //ILoggerを必要とするときはMySQLLoggerを返すよう登録
  container.registerType(ILogger.class, (c)->new MySQLLogger());
  //IFileを必要とするときはCsvFileのオブジェクトを返すよう登録
  container.registerType(IFile.class, (c)->new CsvFile());
  //IReaderを必要とするときはCsvFileReaderを返すよう登録
  container.registerType(IReader.class,
                         (c)->new CsvFileReader(c.resolve(IFile.class)));
  //Hogeを必要とするときはそのままHogeを返すよう登録
  container.registerType(Hoge.class,
                         (c)->new Hoge(c->resolve(IReader.class), c->resolve(ILogger.class)));
  
}

void CreateHoge(DIContainer container)
{
  Hoge hoge = container.resolve<Hoge>();
}

DIコンテナは登録と解決の2段階で使います
今回の例では、RegisterTypes関数中で各インターフェースと対応するオブジェクトの生成手段を登録し、CreateHoge関数でHogeオブジェクトを生成しています。
CsvFileReaderの生成手段を登録する際、IFileのオブジェクトをDIContainerに要求しているのもポイントです。

RegisterTypes関数に依存関係が集約されており、以下がメリットとされています。

  • 依存関係の一覧性が高まる
  • 設定ファイルや何らかの規約を使い、さらに柔軟に依存関係を設定できる余地がある

Service Locater

書籍ではアンチパターンとして紹介されています。
やはり様々な実装方法が考えられますが、その一例で説明します。

class Hoge{
  public Hoge()
  {
    mReader = ServiceLocater.GetIntance(IReader.class);
    mLogger = ServiceLocater.GetIntance(ILogger.class);
  }
  ...
  private IReader mReader = null;
  private ILogger mLogger = null;
}

ServiceLocaterのGetInstanceメソッドは、指定したインターフェースに対応するクラスのインスタンスを返すstaticメソッドです。DIコンテナと同様、事前に何らかの方法で生成手段を登録し、求めたらインスタンスを返すクラスです。
DIコンテナとのパターン的な違いは、Hogeクラスにコンストラクタの引数がなく、代わりにServiceLocaterに依存している点です。
アンチパターンと書きましたが、ServiceLocaterはDIの実現方法の一種として採用しているライブラリもあり、全面的に間違ったパターンとは言い切れません。
Hogeが各クラスではなく各インターフェースに依存する点は実現できていますし、シンプルという意味ではメリットもあります。
しかし、Hogeの依存にServiceLocaterが増えている点、ServiceLocaterが一種の神クラスとして扱われる点をデメリットとし、敬遠されることも多いようです。

Illegitimate Injection

こちらも書籍ではアンチパターンとして紹介されています。

class Hoge{
  public Hoge(IReader reader, ILogger logger)
  {
    mReader = reader;
    mLogger = logger;
  }
  public Hoge()
  {
    mReader = new CsvFileReader(new CsvFile());
    mLogger = new MySQLLogger();
  }
  ...
  private IReader mReader = null;
  private ILogger mLogger = null;
}

すでに説明しているDI的なコンストラクタに加え、具体的なクラスを初期化する引数なしのコンストラクタが増えました。
この構成は「普通は引数なしのコンストラクタを使ってHogeを生成するが、テストのときだけ外からダミーのオブジェクトを注入する」という目的で作られがちです。
書籍では、結局Hogeが具体的なクラスに依存する点がよくないとし、アンチパターンとしていました。

まとめ

  • Poor Man's Dependency Injection
    • 一番シンプルなDIの構成
  • DIコンテナ
    • 少し複雑な代わりにより柔軟なDIの構成
  • Service Locater
    • DI的ではあるが、デメリットがあって敬遠されることがある構成
  • Illegitimate Injection
    • DI的でもあるが、結局具体的なクラスに依存するので避けるのが推奨される構成

以上です

Discussion

ログインするとコメントできます