この記事は『CRESCO Advent Calendar 2022』 22日目の記事です。
こんにちは!
ディベロップメントテクノロジーセンターの Dai Otsuka です。
今回の記事はデザインパターンの1つとして知られている Dependency Injection (DI) を学びなおしてみるというものです。
- DI って名前だけ聞いたことある程度だから学びたい
- むかし研修でやったけど覚えていない
- とりあえず register や bind すれば動くと思っている (ある意味正解)
という方々の学びの一助となればよいなと思っております。
今回の記事の執筆を通して、今までの経験に加えて各種参考文献を改めて読み、 DI について整理をしました。
正確な知識をこちらから一方的に伝えるというより、私も読み手の皆さんと一緒に勉強しているんだよというスタンスでございます。
※) 記事中にcode specific な表現が混じりますがほとんど Java だと思ってください。
目次
そもそも私がなぜこのテーマについて執筆しようと思ったのかを冒頭に軽く書いておきます。(早速本編を読みたい方は次のセクションに進んでください。)
私の現場では DI を当たり前のように使っており、私自身も DI が組み込まれたベースの設計に沿って特に悩むことなく開発・保守をしています。
ただ、ふと内省してみると DI のメリットをメリットとしてとらえられているか正直疑問が浮かびました。
というのも次に自分がアプリケーションの初期設計などを任される立場になることを想定した場合に下記のような不安がありました。
- DI で解決すべき課題を理解して、ベースの設計を作れるか?
- 結局どうしてよいかわからず依存地獄を作ってしまうのではないのか?
DI はもはやアプリケーション開発者の間では常識化しているものの1つとなっており、使用しないという選択は少なくなっていると感じています。
そこで、そもそも DI が生まれた背景から整理を行い、DI で解決すべき課題を明確化していき、質の高いアプリケーションを設計する力をつけたいなと感じています。
平易な言葉で言えば、このままだとやばそうなので基本に立ち戻って学びなおしをするということです。
深堀していく前に、まずは DI について簡単におさらいをしたいと思います。
Dependency Injection(DI) は DI パターンとも呼ばれ、コーディングにおける生成のデザインパターンの1つです。
モジュール間の依存関係を疎結合にすることを目指したパターンとして知られています。
Web の参考記事に記載されている、DI の説明を引用します。
Java Dependency Injection design pattern allows us to remove the hard-coded dependencies and make our application loosely coupled, extendable and maintainable.
Java Dependency Injection デザインパターンは、ハードコードされた依存関係を取り除き、アプリケーションを疎結合、拡張、保守可能なものにすることができます。
デザインパターンというと、 Gang of Four(Gof) のデザインパターン本は有名ですよね。
この本の初版は1994年で意外と古く、その時点では DI への言及がないのです。
しかし、初版から15年後の 2009年に Gof の執筆メンバがインタビューを受けたことがあり、モダンなアプリケーションの設計についてディスカッションをしたことがありました。
そこでは、新しく追加すべきデザインパターンついても言及されており、生成のデザインパターンとして DI がめでたくメンバ入りしています。
2022年現在、このインタビューからも10年以上経過しています。
しかし、現在のアプリケーション設計には DI が組み込まれていることが多く、アプリケーションエンジニアにはもはや常識化されつつあるデザインパターンの1つと言えるでしょう。
このセクションでは、DI が生まれてきた背景について自分なりに考察した内容を述べていきます。
はじめに、コーディングにおいて生成することの特徴について言及していきます。
モジュールは他のモジュールに依存して自分の役割を全うすることがほとんどではないでしょうか。
その場合、単純に考えると下記のような流れになると思います。
依存する必要がある ー> 依存先を知る必要があるー> 依存先を new して生成するー> 依存先の機能を利用する
この過程で依存先を生成するという行為が発生しています。
これを素直にコードに起こすと下記のような流れになると思います。
public class Avengers implements Assemble { |
private final TonyStark tonyStark; |
private final SteveRogers steveRogers; |
private final Thor thor; |
public Avengers() { |
this.tonyStark = new TonyStark(); |
this.steveRogers = new SteveRogers(); |
this.thor = new Thor(); |
} |
@Override |
public void assemble() { |
// omitted |
tonyStark.callUp(); |
steveRogers.callUp(); |
thor.callUp(); |
// omitted |
} |
} |
ここで当然のことながら依存先のコンストラクタやパブリックメソッドに変更があった場合は呼び出し側を変更しなければなりません。
一度きりの変更であればそこまで問題になることはないでしょう。では、この依存先モジュールがまだ開発段階のもだとしたらどうでしょうか?
依存先モジュールの生成方法、呼び出し方が変更されるたびに依存元モジュールの実装は壊れていきます。
このように、あるものに対する変更がどれくらい別のものの変更を必要とするかの程度を結合(Coupling)と表現します。
今回の例では依存先のコードに変更があった場合、呼び出し元を変更しなければならない状態のため密結合といえるでしょう。
このように、コーディングにおける生成はモジュール間の結合を高めやすいという特徴があります。
そして、設計の仕方によっては密結合となり、保守性が低下してしまいます。
そのため、モジュール間の依存関係は疎結合にしていきたいよねというのがコーディングにおける生成することが持つ課題だと認識しています。
では、疎結合に保つためにはどのようにすればよいのでしょうか?
ここで Clean Architecture 達人に学ぶソフトウェアの構造と設計 で語られている Dependency Inversion Principle (DIP) 依存関係逆転の法則 を引用します。
上位レベルの方針の実装コードは、下位レベルの詳細の実装コードに依存すべきではなく、逆に詳細側が方針に依存すべきである
シンプルに考えると、開発をするうえでは上位レベルの方針(仕様や抽象化されたふるまい)を満たすために、下位レベル(具体的な解法)の実装をコードで表現します。
これを軸に考えた場合、依存元のモジュールが依存先のモジュールに期待することは上位レベルの方針の通りに動作することであるはずです。
つまり、仕様を満たすことがモジュールの責務であり、その手段や具体的な中身の構造まで依存する必要はないということになります。
DIP の原則に則ると、具体的な実装手段に依存した関係ではなく仕様や抽象化されたふるまいに依存した関係を作るべきであるということが言えます。
チームで開発していくことを想定した場合、下位レベルの具体的な実装モジュールに依存することによって生ずる弊害がいくつか考えられます。
- 依存先モジュールが開発途中だと、頻繁に更新されてしまう
- Interface の変更数より、Interface の実装モジュールの変更数が多い
これらより、具体的な実装モジュールはシステム内において変化しやすい要素といえるでしょう。
チーム内でも下記のような方針をとることが多いのではないでしょうか?
- Interface には変更をいれず、実装モジュールのみの修正に極力抑えようとする
- 機能追加時には既存の Interface のまま実装する形で済ませられるようにする
Interface などの上位レベルの方針は下位レベルの実装モジュールと比べ、変更は少なく安定しているといえます。
つまり、上位レベルの方針に依存することによってモジュール間の結合度が下がることが期待できます。
ここまで DIP に則り、モジュールを疎結合に保つことについて考えてきました。
では、今回の記事のテーマである DI に戻ってみましょう。
本記事では DI の代表例であるコンストラクタインジェクションを取り上げて述べていきます。
コンストラクタインジェクションは Dependency Injection Principles, Practices, and Patterns にて言及があるので引用します。
Constructor Injection is the act of statically defining the list of required Dependencies by specifying them as parameters to the class’s constructor.
コンストラクタ・インジェクションとは、クラスのコンストラクタにパラメータとして指定することで、必要な依存関係のリストを静的に定義する行為です。
平易に言うと、 依存先のモジュールを自ら探して依存するのではなく、コンストラクタの引数として生成時にもらうようにするということです。
このセクションでは、サンプルコードを交えながらコンストラクタインジェクションの実装方法について述べていきます。
まず、 Interface を引数としてもらうパターンになります。
下記のように依存先のモジュールでは各種 Interface を定義し、その Interface を実装したモジュールを用意します。
public interface Ironman { |
void callUp(); |
} |
public interface Avenger { |
void callUp(); |
} |
public class TonyStark implements Ironman, Avenger { |
@Override |
public void callUp() { |
// omitted |
System.out.println("This is Tony : The truth is… I am Iron Man."); |
} |
} |
依存元モジュールは下記のように、3つの Interface に依存しています。
依存元のコンストラクタでは、Interface を実装したインスタンスをコンストラクタの引数としてもらうようにしています。
よって、必要な依存を自ら生成することなく依存先の機能を利用することができます。
※特に言及していないコンストラクタの引数も同様だと思ってください。
public class Avengers implements Assemble { |
private final Ironman ironman; |
private final CaptainAmerica captainAmerica; |
private final GodOfThunder godOfThunder; |
public Avengers(Ironman ironman, CaptainAmerica captainAmerica, GodOfThunder godOfThunder) { |
this.ironman = ironman; |
this.captainAmerica = captainAmerica; |
this.godOfThunder = godOfThunder; |
} |
@Override |
public void assemble() { |
// omitted |
ironman.callUp(); |
captainAmerica.callUp(); |
godOfThunder.callUp(); |
// omitted |
} |
} |
Interface を実装したクラスのインスタンスを直接もらう以外にも Abstract Factory を引数とするパターンもあります。
下記のように Abstract Factory を定義し、 それを実装する Factory を用意します。
public abstract class AvengerFactory { |
public abstract List<Avenger> factorize(); |
} |
public class Big3Factory extends AvengerFactory { |
@Override |
public List<Avenger> factorize() { |
return Arrays.asList(new TonyStark(), new SteveRogers(), new Thor()); |
} |
} |
依存元は下記のように、コンストラクタで Abstract Factory を引数にもらうようにします。
Abstract Factory の生成メソッドを経由して、依存を取得できるようになっています。
本来必要な依存先モジュールの生成を Abstract Factory の実装モジュールに代わりに生成してもらえるようになります。
public class Avengers implements Assemble { |
private final List<Avenger> avengerList; |
public Avengers(AvengerFactory factory) { |
this.avengerList = factory.factorize(); |
} |
@Override |
public void assemble() { |
avengerList.forEach(Avenger::callUp); |
} |
} |
ここまで素直に DI の仕組み通りにサンプルコードを作ってみました。
しかし、依存先モジュールの生成を別の第三者に委譲しているだけでは下記のような問題点が生じてきます。
- 根っこですべてを知るモジュール(例えば main )が各モジュールの引数に渡すすべての依存を管理しないといけない
前のセクションで紹介したサンプルコードでは Interface で渡されるパターンと Abstract Factory で渡されるパターンどちらも main は下記のようになっています。
// Interface で渡されるパターンの main method |
public static void main(String[] args) { |
TonyStark tonyStark = new TonyStark(); |
SteveRogers steveRogers = new SteveRogers(); |
Thor thor = new Thor(); |
Avengers avengers = new Avengers(tonyStark, steveRogers, thor); |
avengers.assemble(); |
} |
// Abstract Factory で渡されるパターンの main method |
public static void main(String[] args) { |
AvengerFactory avengerFactory = new Big3Factory(); |
Avengers avengers = new Avengers(avengerFactory); |
avengers.assemble(); |
} |
main でたくさん new していますね。。。
まだ依存の数がそこまで多くないので気にならないかもしれませんが、扱うモジュールの数が増えて依存関係が複雑になっていくとどうでしょうか?
main のようなすべてを呼び出しているモジュールが代わりにすべての依存を管理しなければならなくなり、モジュールの数が増えるたびに保守性が低下していくことが予想できます。
- Abstract Factory がリポジトリ内に乱立してしまう
Abstract Factory で渡されるパターンのサンプルコードを参照してください。このパターンだと、依存先モジュールの生成を Factory に委譲していますね。
ただ、Abstract Factory の方針で設計してしまうと、扱うモジュールの数が増えるたびに Abstract Factory を定義する必要が出てきてしまいます。
そのため、リポジトリ内に Abstract Factory が乱立し、モジュールの生成に関するコードが膨大な Abstract Factory の実装モジュールに散らばっていくことが予測できます。
結局これでは、依存管理の責任を別モジュールに押し付けているようにも見えてしまいます。
これらの課題は容易に想像がつきますし、起こりうるリスクといってもよいでしょう。
DI は生成する際に必要な依存の管理を誰かが請け負う前提で効果を発揮する仕組みといえると思います。
前のセクションで述べた課題に対して、どのように解決することが一般的でしょうか?
複雑化した依存関係を一括で管理し、ほしい依存モジュールを自動で生成してくれる仕組みがあれば DI の良さは活きそうです。
一般的に、このような課題は DI コンテナといわれるツールを用いて解消することが多いです。
DI コンテナは Dependency Injection Principles, Practices, and Patterns で言及があるので引用します。
A DI Containers is a software library that that provides DI functionality and allows automating many of the tasks involved in Object Composition, Interception, and Lifetime Management.
DI コンテナは、DI の機能を提供するソフトウェア・ライブラリで、オブジェクトコンポジション、インターセプト、ライフタイム管理に関わる多くのタスクを自動化することができます。
このセクションでは Interface を渡されるパターンを guice で置き換えたサンプルを作ってみました。
下記のように、依存元モジュールの作りはほとんど変わりません。
DI コンテナから 依存を渡されるコンストラクタには JSR 330 で規定された仕様に従い、 @Inject
をつけています。
public class Avengers implements Assemble { |
private final Ironman ironman; |
private final CaptainAmerica captainAmerica; |
private final GodOfThunder godOfThunder; |
@Inject |
public Avengers(Ironman ironman, CaptainAmerica captainAmerica, GodOfThunder godOfThunder) { |
this.ironman = ironman; |
this.captainAmerica = captainAmerica; |
this.godOfThunder = godOfThunder; |
} |
@Override |
public void assemble() { |
// omitted |
ironman.callUp(); |
captainAmerica.callUp(); |
godOfThunder.callUp(); |
// omitted |
} |
} |
main では、依存先モジュールの生成ルールを DI コンテナに登録しています。
今回のサンプルでは、 各 Interface に bind する実装クラスを guice の Injector に登録しています。
このようにすることで、 欲しい依存先モジュールを自ら生成することなく自動的にコンストラクタに渡すことができるようになります。
public class GuiceMain { |
public static void main(String[] args) { |
Avengers avengers = Guice.createInjector(new AbstractModule() { |
@Override |
protected void configure() { |
super.configure(); |
bind(Ironman.class).to(TonyStark.class); |
bind(CaptainAmerica.class).to(SteveRogers.class); |
bind(GodOfThunder.class).to(Thor.class); |
} |
}).getInstance(Avengers.class); |
avengers.assemble(); |
} |
} |
これで依存元モジュールにも main にも依存先モジュールを new するコード をなくすことができましたね。
ここまで DI が生まれた背景や DI コンテナの利用ついて述べてきました。
このセクションでは、DI コンテナを利用して、 DI をアプリケーションの設計に組みこむとどのような効果が生まれるのかについてまとめていきたいと思います。
DI がもたらす全ての効果を網羅的に書き上げることはできませんが、私の経験上チームで開発する上で恩恵を受けていることなどを中心に書こうと思います。
依存モジュール同士が疎結合になることで依存先の具体的な実装の変更に依存元のコードは影響を受けなくなります。
加えて、特定の Interface を実装している複数の実装パターンをそれぞれ切り替えたい場合は、 DI コンテナへ登録する実装モジュールを変更するのみに変更量を抑えることができます。
さらに、テストをする際には対象の Interface を実装した簡易的なモックを作成して DI コンテナに登録することで、シンプルかつ再現性の高いテストを実現できます。
DI コンテナを利用すると、基本的に自ら依存先モジュールを生成する必要がなくなります。
そのため、依存先モジュールを生成するためだけに用意していた不要なコード群をリポジトリから大幅に排除することができます。
DI コンテナを利用する前は、ほとんどのクラスで new HogeHoge()
していたり、 Abstract Factory がはびこっていたと思います。
しかし、DI コンテナを利用することで、それら生成に関するコードを一掃することができます。
不要なコードを大量にリポジトリから削除できるのは保守性の観点からもよいことだと思います。
利用者は依存先モジュールが Interface 通りの動きをしてくれて、ほしい依存先モジュールを DI コンテナが自動で Inject してくれることを期待しています。
よって、利用者が依存先モジュールの生成に関する固有のお約束や具体的な内部構造のような変化しやすい要素にタッチしなくて済むようになります。
これは依存先モジュールの利用方法をよりシンプルにすることにつながります。
目的を達成するために、余計なことを考える必要がなくなるというイメージです。
そして、目的を達成する上で使いたい機能をシンプルに利活用できるようになることは、
チーム全体での機能開発の生産性を向上させることにつながると示唆します。
今回の記事では Dependency Injection の学び直しを行い、生まれた経緯、仕組み、効果について整理をしました。
適度に深堀したため、そこそこ多めの文章量となってしまいました。大学時代のレポート地獄が少しフラッシュバックしてきました。。。
この記事を通して、 DI について少しでも理解が深まったなと思っていただけたらうれしいです。
※記事中で参照したコード類はこちらに挙げております。