DI 入門者向けの話(DI が生まれる背景)
[日記][Tips]DI 入門者向けの話(DI が生まれる背景)
Dependency Injection 日本語訳で、外部依存注入の話をする。
前提知識
オブジェクト指向には多態性なるものがある。
これは、Java でいうところのインターフェースに依存する作りをすることで、その実装を変更できるという論理だ。
割と具体的にコードに落とすと
class AppendHello { public String wrap(String name) { return "Hello, " + name + " name count are " + this.countValue(name); } public int countValue(String name) { return name == null || name.equals("") ? 0 : name.size(); } } public class Main { AppendHello wrapper = new AppendHello(); public void main() { // static でないのはサンプルだから String result = wrapper.wrap("Azalea"); System.out.println(result); // ここは変更なし return result; } }
ってしてしまうと、Main はあまりに AppendHello にべったりしすぎてしまい、AppendHello でなにか修正を受けると影響がモロに出てしまう。
そこで、本当に必要なインターフェースを用意し、Main から依存するのをインターフェースにしてしまえば、何かの理由で AppendHello に手が入っても、Main 処理に影響を及ぼさないし、何よりインターフェースを継承した別のもので代用できる。
これが多態性だ。
interface StringWrapper { String wrap(String value); } class HelloWrapper implements StringWrapper { @Override public String wrap(String name) { return "Hello, " + name + " name count are " + this.countValue(name); } public int countValue(String name) { return name == null || name.equals("") ? 0 : name.size(); } } public class Main { StringWrapper wrapper = new HelloWrapper(); public void main() { String result = wrapper.wrap("Azalea"); System.out.println(result); return result; } }
例えば、HTML でラップするものに差し替えるとしても
class HtmlWrapper implements StringWrapper { @Override public String wrap(String name) { return String.format("<p>%s</p>", name); } } public class Main { StringWrapper wrapper = new HtmlWrapper(); // ここだけ修正 public String main() { String result = wrapper.wrap("Azalea"); // ここは変更なし System.out.println(result); return result; } }
つまり、メソッドの呼び出し方と、その応答データの種類に事前に制約をかけて、そのルールでクラスを実装する限り、使用している Main に修正の手が伸びない。
置き換えて色々な事に使用できる。
そういうことだ。
問題点と当初考えられた解決法
しかし、これには半端に2つ問題が発生する。
1: コンパイル時点で固定になる
それは new した時点で固定となり、処理の入れ替えが効かない事である。
public class Main { // ↓ここで new するやつ固定じゃん StringWrapper wrapper = new HtmlWrapper(); public void main() { System.out.println(wrapper.wrap("Azalea")); } }
つまり、入れ替えしたければ再コンパイルしなければならないという事だ。
2: テストしにくい
- Main.main の挙動が、HtmlWrapper の実装に依存しているため、Main.main の応答結果をテストしたければ、最初に HtmlWrapper の動作を考慮しなければならないという問題がある(仮にこれがデータベースを使うものだと考えてみればいい…表面的に関連性のわからないレコードを大量にテスト準備で用意するのはあまりに大変だ)。
- HtmlWrapper は別クラスなので、何らかの理由で修正が入るかもしれない、つまりこのテストは HtmlWrapper が修正されるたびに書き直さなければならないのだ。
- HelloWrapper 等に置き換える旅にテストを変更しなければならない。これもアフォかと。
オブジェクト指向的に頑張って解決(古典的手段)
この問題を解決するために、オブジェクト指向界ではデザインパターンという名前のあるアルゴリズムのごとく、「こんな時にはこういうデザイン(設計)パターン(型)を適用する」といった設計パターンを作り上げた。
上記であれば、FactoryMethod パターンだ。
interface StringWrapper { String wrap(String value); } class HelloWrapper implements StringWrapper { @Override public String wrap(String name) { return "Hello, " + name + " name count are " + this.countValue(name); } public int countValue(String name) { return name == null || name.equals("") ? 0 : name.size(); } } abstract class Main { abstract protected StringWrapper getWrapper(); // 依存部分だけ切り出す public void main() { String result = this.getWrapper().wrap("Azalea"); System.out.println(result); return result; } } public class HelloMain extends Main { @Wrapper protected StringWrapper getWrapper() { return new HelloWrapper(); } }
このようにすることで、
- Main だけみれば StringWrapper 部分は切り離されているため、使うべき所で継承して用意すればよい。
- Main.main は StringWrapper の実装には依存していないので、テスト時には都合のいい偽物にでも置き換えできる。
他にも、 "AbstractFactory":https://ja.wikipedia.org/wiki/Abstract_Factory_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3 なんかもあるので、見てみるといい。
オブジェクト指向だけでの限界
こうする事で、そこそこ動的に入れ替えが効くようにはなるがいくつか問題が起きる。
- クラス爆発
Main に対し、各実装を含む HelloMain とか HtmlMain とか、ともかくクラス数が増える。
それは保守性や可読性に対してマイナスだ。 - 仕組みのためのコード量が多い
実際に業務上使用したいコードという訳ではないのに、汎用性を求めるための追加コードがやたらと増える。
そのコードが混ざると、「本来要件的に解決したいコード」が、「仕組みのために必要なコード(非業務要件)」に埋もれてしまい、保守性が下がる。
(こうした、仕組み上書かなければならない、業務的に意味のない定型コードは「ボイラープレート」と呼ばれる)
インジェクション
そこで、そうした依存は外から設定注入(インジェクション)できるようにすればいいのではないかと考える。
つまり、必要なオブジェクトは使う時に外から指定してやれば良いのではないかと考える。
その案のもっとも原始的方法は、「依存するオブジェクトをコンストラクタで受け取ってしまう」である。
要するにクラスの中で new しなけりゃいいんだろというなんとも簡単な話。
public class Main { StringWrapper wrapper; public Main(StringWrapper wrapper) { this.wrapper = wrapper; } public void main() { System.out.println(wrapper.wrap("Azalea")); } }
このクラスだけ見れば、これで問題ない様に見える。
しかし使う側で固定化問題が起こる訳だ。
public UseMain { // 使う側でこうなってしまう // 結局フレキシブルになりきれない Main mainClass = new Main(new HelloWrapper()); public String useMain() { String res = mainClass.main(); // 何か後続処理 } }
ならどうするか?
そう考えた時に行き着く技術がある。
リフレクション である。
リフレクションを使ったインジェクション
まずリフレクションについて少し話そう。
通常、Java のオブジェクトとはクラスを書いて、new して初めて利用できる。
このオブジェクトの定義をクラス、new してできた実体をインスタンスと呼んでいる。
ここでいう所のクラスとは何かを考えると、インスタンスが実際に動かすことができるメモリ上のプログラムなら、それを形作る、もしくはその挙動を定義するクラスとは、すなわちメモリ確保時に使用する設計図だ。
普段は new しかしないが、そこに設計図がある。
なら、その設計図から出来上がったインスタンスの中身は、つまりメモリの形が把握できるということ。
なら、設計図を見ながらムリヤリでも書き換えることができるのではないか?
実際にそれを行う機能が「リフレクション」と呼ばれる機能群だ。
例えば、setter のない private な文字列フィールドを持つクラスを考える。
class Example { private String name; public Example(String value) { this.name = value; } public String getName() { return this.name; } }
普通に考えたら、name はコンストラクタからしか設定することができない。
だが、リフレクションを使うとここに値を設定できてしまう。
import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; class Example { private String name; public Example(String value) { this.name = value; } public String getName() { return this.name; } } public class Main { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException { // new を使わず設計図からムリヤリインスタンス作成 Example example = Example.class.getDeclaredConstructor(String.class).newInstance("Hoge"); System.out.println(example.getName()); // Hoge が表示 // name の設計図を取得 Field field = Example.class.getDeclaredField("name"); // セットアクセス権限を設定 field.setAccessible(true); // example の name をムリヤリ変更 field.set(example, "Update"); System.out.println(example.getName()); // Update が表示 } }
この機構を使い、設定ファイルか何かでクラスの外からの入力を制御すれば、なんとコードを一切いじらずに実装を変更、挙動を変えることができるのではないかと考える。
クラスの外(外部)から、依存する処理(依存)を流し込む(注入)ことで外部依存注入、DI となるわけだ。
この機能を使ったライブラリを、「public static void main」で初期化し、残りの全てのインスタンス生成してもらえば、あとは全てのクラスで new による直接的な依存がなくなる…すなわち、疎結合とテスタビリティの確保ができるようになる。
気が向いたら続く。