Open5

なぜ依存を注入するのか DIの原理・原則とパターンを読む

ナナセナナセ

なぜ依存を注入するのか DIの原理・原則とパターンを読んだ内容のアウトプット。

本書を手に取ったのは、周囲のつよつよのエンジニアが好評されていたのもあるが、当時の自分がプログラムは書けるけど

  • あとから読んだときに自分でも内容がわからない
  • テストがしづらい(簡単に壊れる)
  • 自分でアーキテクチャを考えたことがない

といった課題に直面していた状況で、アバウトに設計に関する知見を求めていたため。

普段書籍を読んでもインプットで満足してしまうことが多いので、アウトプットしてみての効果を確かめたい目的を大いに含む。

ナナセナナセ

1.1 保守のしやすいコードを書くためには

DIの目的

  • 副題に対する回答となり、つまりDIは保守容易性を実現するための要素の1つ。
  • DIは疎結合なコードを実現するためのテクニック。
  • 上記を言語化した下記の表現はすんなり頭に浸透した。

    インターフェースに対してプログラミングするのであって、実装に対してプログラミングするのではない。

1.1.1 DIに関するよくある誤解

  • DIに関する誤解とそれらに対するアンサーが記載されている。
    • 4つほど列挙されているが、浅はかな自分はDIコンテナが必要というものしか該当しなかった。(誤解にほとんど当てはまらなかったという点について喜ぶべきなのか、はたまたDIに対する知見のなさが現れているだけなのか)
  • DIコンテナはDIを行ううえで必ずしも必要ではなく、DIコンテナを使用しないDIをPure DIと呼ぶ。

1.1.2 DIの目的

  • DIと関連する4つの設計パターンについて家電製品を例に解説している。
    • インターフェース:壁についているコンセント
    • 実装:ドライヤーのコードについているプラグ
  • コンセントというインターフェースを変えることなく、さまざまな家電製品(実装)を扱えるようにする考え方はリスコフの置換原則に相当する。
    • リスコフの置換原則では、ソフトウェアに対して将来起こるであろう新たな要求に対して対応できるようにする。
  • 既存のコードを変更することなく、機能を拡張できるようにするという概念は、開放/閉鎖の原則*と呼ばれる。
  • インターフェースに対してプログラミングするというのは、疎結合な実装を実現するための考え方ではあるが、実際に動作させるための中身(実装)をどこで生成するかという疑問に対する回答がDIである。

Decoratorパターン

  • インターフェースの実装クラスに対して、同じインターフェースを持った別のクラスを介入させる設計パターン。
  • コンセントとコンピュータの間に無停電電源装置を介入させるイメージ。
    • 無停電電源装置は、コンセントと同じインターフェースを持っているので、インターフェースを変えることなく間に入ることができる。

Compositeパターン

  • 複数の機能を1つに集約する設計パターン。
  • 電源タップのイメージ。
    • コンセントのインターフェースを増やさずに、複数の家電(機能)を利用することができる。
    • コンポジッターが複数機能を提供する役割を担うことで、コンセントと家電はインターフェースを変える必要がなくなり、昨日の追加や除去が行いやすくなる。

Adapterパターン

  • 異なるインターフェースを接続する設計パターン。
  • 海外のコンセントと日本の家電を接続する変換アダプターのイメージ。

Nullオブジェクト

  • 対象のインターフェースに対して、何も行わない実装クラスのこと。
    • コンセントの電源カバー
  • テストや、開発途中でヌルポが発生しないようにするための概念と理解した。
ナナセナナセ

1.2 サンプル・アプリケーション:『Hello DI!』

1.2.1 サンプル・アプリケーションのコード

  • 下記のコードにおけるSalutationクラスはコンストラクタ経由で、IMessageWriterインターフェースを注入されるようになっている。
    • Salutationクラスはその実装に関心を持つ必要はなく、IMessageWriterインターフェースさえ意識するだけで済む。
    • 生成するクラスが必要とする依存をコンストラクタの引数に指定することで、必要な依存を静的に定義できる(コンストラクタインジェクション)。
private static void Main()
{
    IMessageWriter writer = new ConsoleMessageWriter();
    var salutation = new Salutation(writer);
    salutation.Exclaim();
}

1.2.2 DIを用いるメリット

  • DIを用いた疎結合な実装は、簡単な例だとそのメリットを実感しづらいが、コード量が増えるにつれてその恩恵は大きくなる。
  • 疎結合にすることで得られるメリット
    • 遅延バイディング
      • 使用する実装をコンパイル時ではなく、使用時に決められるようになる。(遅延評価)
      • 構成ファイルを書き換えることで、再コンパイルなしで振る舞いを変えることができる。

      たとえば

      IMessageWriter writer = new ConsoleMessageWriter();
      

      // 構成ファイル読み込み
      IConfigurationRoot configuration = new ConfigurationBuilder()
          .SetBasePath(Directory.GetCurrentDirectory())
          .AddJsonFile("appsettings.json")
          .Build();
      
      // 構成ファイルに定義されているクラス名を取得
      string typeName = configuration["messageWriter"];
      Type type = Type.GetType(typeName)
      
      // クラス名からオブジェクトを生成
      IMessageWriter writer = (IMessageWriter)Activator.CreateInstance(type);
      
      appsettings.json
          "messageWriter": "HelloDI.Console.ConsoleMessageWriter, HelloDI.Console"
      

      のように変更することで、遅延バインディングを実現できる。

    • 拡張容易性
      • 言葉そのままで、拡張性や再利用性を高められる。
      • 保守容易性とも被る部分がありそう。
    • 並列開発
      • 異なる機能の開発を並列に行える。
        • インターフェースさえ決めておけば中身はそれぞれ独立して開発できる。
      • 複数人で開発している場合にとても大事。
    • 保守容易性
      • クラスごとの責務を分けるため、保守が行いやすくなる。
      • このクラス何してんじゃいが把握しやすくなると嬉しいよね。
    • テスト容易性
      • 単体テストがしやすくなる。
      • インタフェースさえ合っていれば動かせるからね。
ナナセナナセ

1.3 何を注入し、何を注入しないのか?

  • インターフェースを間に挟んでモジュールを構成する場合、そのインターフェースは結合部を持つことを意味する。

1.3.2 揮発性依存

  • 結合部を設置することは、直接具象クラスを利用する場合と比べて手間がかかるため、必要なときのみ設置するべきである。
  • 依存の中には、メリットを損なわせるものも存在し、それを揮発性依存と呼ぶ。
    • 実行環境に対する設定や調整が必要となるもの
      • RDBへの操作を行う型(他のRDBに置き換えられない)
    • 非決定的な振る舞いをするもの
      • 乱数や現在日時を取得する処理など
  • 抽象を介さずに揮発性依存を使ってしまうと、遅延バインディングや拡張、テストができなくなってしまう。
ナナセナナセ

1.4 依存注入の特性

  • 依存オブジェクトを生成し、その依存を必要とするクラスに注入し生成することをオブジェクト合成と呼ぶ
  • 依存を注入される側は、その生存期間を制御する責務からも解放される
  • DIにおける重要な特性として3点挙げられる
    • オブジェクト合成
    • 介入
    • 生存管理

1.4.2 オブジェクトの生存期間

  • 同じインターフェースの依存を必要とするインスタンスを生成する場合、異なる依存を使用するのか、依存を共有するのか、2通り存在する。
    • 異なる依存を使用する場合
      IMessageWriter writer1 = new ConsoleMessageWriter();
      IMessageWriter writer2 = new ConsoleMessageWriter();
      
      var salutation = new Salutation(writer1);
      var valediction = new Valediction(writer2);
      
    • 依存を共有する場合
      IMessageWriter writer = new ConsoleMessageWriter();
      
      var salutation = new Salutation(writer);
      var valediction = new Valediction(writer);
      
  • 依存を共有する場合、特にIDisposalインターフェースを実装している場合、依存を使用する側で生存期間を意識する必要が出てくる。
  • 依存の制御をアプリケーションのエントリーポイントに移すことで、横断的関心事への適用が行いやすくなり、依存の生存管理を効率的に行えるようになる。