DIの起源から遡ってDIを理解する
\スニダンを開発しているSODA inc.の Advent Calendar 2024 19日目の記事です!!!/
はじめに
DIと聞いてなんとなくで理解している方はいませんか?
言うまでもないかもしれませんがDIとはDependency Injectionのことであり、日本語では一般的に依存性注入と訳されることが一般的です。
弊社ではちょうどモジュラーモノリスを想定したバックエンドのモジュール分割を行っています。
モジュール分割において、DIどうするといったトピックがあったので調べてみました。
この記事ではDIについて起源から遡り、ふわっとした理解の解像度を高めることを目的とします。
DI自体はもちろん、DIの文脈で登場する用語を理解し、日常の業務や開発の場で野生のDIがでてきたときにすぐに理解できるようになるようになればよいなと思ってます。
(サンプルコードはGoで書いてます。)
Dependency Injectionの定義に遡る
タイトルのとおり、Dependency Injectionの起源まで起源まで遡ってみましょう。
原典となるMartin Fowlerのブログから確認してみます。
(これ読めば結構理解が進みます)
定義と思われる部分を抜粋してきました。
As a result I think we need a more specific name for this pattern. Inversion of Control is too generic a term, and thus people find it confusing. As a result with a lot of discussion with various IoC advocates we settled on the name Dependency Injection.
意訳すると、「この設計パターンはInversion of Controlは用語としては一般的すぎるため Dependency Injectionと定義した」とあります。
結論からいうと、Inversion of Control(制御の反転)の具体的な設計パターンの1つということになります。
一方で、Inversion of Controlについても理解が曖昧なので理解を深めるために深ぼってみます。
依存性逆転の原則からDependency Injectionの成り立ちまで
そのために依存性逆転の原則(Dependency Inversion Principle) についても言及します。
DIPなどと略されていたりして、DIがゲシュタルト崩壊してたことがある人もいるのではないでしょうか?(過去の自分もそうでした😳)
DIPは有名なSOLIDのDですね。
SOLIDはRobert C. Martin(Uncle Bob)が提唱していた数々の設計原則の中から抜粋された重要な設計原則5選といったところです。
依存性逆転の原則について簡単に説明すると、ソフトウェアを疎結合に保つこための原則です。定義を元に以下をおさえておきましょう。
- 上位モジュールが下位モジュールに依存してはいけない
- 抽象は詳細に依存してはいけない
依存性逆転について実際にサンプルコードと図で確認していきましょう。
コードの元ネタは先程のMartin FowlerのDIのブログのサンプルコードをGoで書き換えたものです。
MovieLister
は下位モジュールであるMovieFinder
を使用して動作する上位モジュールということだけおさえておけば問題ありません。
interface IMovieFinder {
FindAll() []Movie
}
type MovieFinder struct {}
func (m *MovieFinder) FindAll() []Movie {
return nil
}
type MovieLister struct {}
func (m *MovieLister) MoviesDirectedBy(director string) []Movie {
finder := MovieFinder{}
// 映画をすべて取得
movies := make([]Movie, 0, len(allMoveies))
allMoveies := finder.FindAll()
// ディレクターが一致する映画を取得
movies := make([]Movie, 0, len(allMoveies))
for _, movie := range allMoveies {
if movie.Director == director {
return append([]Movie{}, movie)
}
}
return movies
}
コード例のざっくりとした依存関係は以下のようになってます。
詳細化すると以下のように表現できます。
依存性逆転の原則に従って、上位モジュールであるMovieLister
を下位モジュールであるMovieFinder
に依存しないようにします。
そのためにコードを改善すると以下のように修正できます。
// MovieListerはIMovieFinderをフィールドとしてもつ
type MovieLister struct {
+ movieFinder IMovieFinder
}
+// この例では構造体の初期化関数で受け取るようにする
+func NewMovieLister(finder IMovieFinder) *MovieLister {
+ return &MovieLister{movieFinder: finder}
+}
func (m *MovieLister) MoviesDirectedBy(director string) []Movie {
- finder := MovieFinder{}
// 映画をすべて取得
movies := make([]Movie, 0, len(allMoveies))
//フィールドのmovieFinderを使う
- allMoveies := finder.FindAll()
+ allMoveies := m.movieFinder.FindAll(director)
// ディレクターが一致する映画を取得
movies := make([]Movie, 0, len(allMoveies))
for _, movie := range allMoveies {
if movie.Director == director {
return append([]Movie{}, movie)
}
}
return movies
}
ここでMovieLister
とIMovieFinder
を同じコンポーネントとしてその関係を俯瞰すると以下のよに表現できます。
なんということでしょう、依存関係が逆転してますね!
ここで先程登場した Inversion of Control(制御の反転) について説明すると、「依存性逆転の原則を満たすようにするアプリケーションの組み立て方」と解釈できました。(しっかりとした定義が見つることができなかった...)
そしてサンプルコードで確認したような、上位モジュールに対して下位モジュールの実装を何らかの形で外から渡してもらう(注入する)ようなInversion of Control(制御の反転) の具体的な設計パターンがDependency Injectionと言えます。
Dependency Injectionの具体化
一方でDIについてはパターンがいくつかあります。
ここでも元となる記事から流れを抜粋してきます。
but then, how do we make an instance to work with?
では、使用するインスタンスをどのように作成すればよいのでしょうか。
ここで次の課題となるのは具体の実装をどのようにインスタンス化するかということです。
The problem is how can I make that link so that my lister class is ignorant of the implementation class, but can still talk to an instance to do its work.
問題は、MovieLister
クラスが実装クラス(IMovieFinder
の実装クラス)を認識せずに、インスタンスと通信して作業を実行できるように、そのリンクをどのように作成するかです。
今回の例でいうと以下の箇所です。
// この例では構造体の初期化関数で受け取るようにする
func NewMovieLister(finder IMovieFinder) *MovieLister {
結論としては、以下のような方法があると記載されてます。
- インターフェイス注入
- セッター注入
- コンストラクタ注入
サンプルコードを当てはめると3のコンストラクタ注入となりますね。
Goにはclassの概念がないので、構造体の初期化関数での表現になってますね。
(GoのDIライブラリも構造体の初期化関数で依存を定義するような形になってます。)
アプリケーションで効率的にDIを実現するには?
DIの具体的な例を見てきましたが、アプリケーションで効率的にDIを実現するにはどうするとよいでしょうか?
毎回アプリケーションの各ユースケースのロジックでインスタンスを手動で生成するのではなく、一括で依存を解決してくれるような仕組みがあればよさそうです。
以下の図ではAssemblerがその仕組みを担当してます。
MovieLister
の依存しているIMovieFinder
の具体的な実装としてMovieFinder
を生成して依存を解決している様子です。
サンプルコードでは1つの依存を1つのインスタンスに対して注入する例でしたが、実際のアプリケーションコードとなるとユースケースの数も多く、さらにクリーンアーキテクチャのようなソフトウェアアーキテクチャを採用するとレイヤーも増えるので、毎回手動による依存の注入は大変になります。
また、場合によっては一度生成したインスタンスをアプリケーション内で効率的に使い回すほうが効率的になる場合もあります。
これらを解決するヘルパーが昨今のDIライブラリとなります。
Tips: DIコンテナについて
DIコンテナという名称も聞いたことがあるかもしれません。
結論としては、DIコンテナはDIライブラリの高機能な実装となります。
例えば、Goではgoogle/wire
といった有名なDIライブラリがありますがDIコンテナとは厳密には違います。
大まかに述べるとDIコンテナはそう呼ばれないDIライブラリと比較して以下のような機能を持ちます。
- 動的な依存解決
- ライフサイクル管理
一方でgoogle/wire
といったDIライブラリは静的な依存解決(Static Dependency Injection)となっておりライフサイクルの管理機能は提供されていません。
(DIコンテナは、「動的に依存性を決定して、モノ(インスタンス)を自由に出し入れできる」という点でコンテナという呼ばれ方をしているという記述もありました。)
.NET(C#)の標準のDIコンテナのドキュメントを参照すると、DIコンテナである「動的な依存解決」と「ライフサイクル管理」の動作がわかりやすかったので気になった方は確認してみてください。
Appendix: GoのDIライブラリについての比較
DIの解像度が高まったところで、GoのDIライブラリについて比較してみました。
今回はGoのDIライブラリとしてはよく名前を聞くgoogle/wire
とuber/dig
をピックアップして比較してみます。
google/wire
静的な依存解決を行うDIライブラリ
uber/dig
動的な依存解決を行うDIコンテナ
先ほどのTipsを前提に、DIコンテナ or DIライブラリという観点でみていくと以下のような比較できます。
比較表
特徴 | wire |
dig |
---|---|---|
解決タイミング | コンパイル | ランタイム |
依存管理方法 | 静的コード生成 | リフレクションによる動的解決 |
安全性 | 🟢 (コンパイルエラー) | 🔺 (ランタイムエラー) |
パフォーマンス | 高速(コード生成済み) | 比較的遅い |
柔軟性 | 低い | 高い |
また「DIコンテナ or DIライブラリ」以外の側面では、以下のような観点がありそうです。
-
dig
はジェネリクス対応している -
wire
はメンテナンスフェーズに入っている
記載している情報から判断すると、
- 柔軟な依存性の解決が必要である複雑で大きいアプリケーションでは
dig
- 柔軟な依存性の解決が必要ではないパフォーマンスや安全性が優先なアプリケーションでは
wire
という総評になりそうです。
もちろんGoには紹介してないだけで他にも多くのDIライブラリはあります。
DIパターンの導入を検討している方は、ここで言及した観点を元にさらに比較検討してみても良いかもしれません。
まとめ
DIはソフトウェア開発において、疎結合な設計を実現し拡張性や保守性を向上させるための依存性逆転の原則を実践した具体的な設計手法です。
また、昨今のDIライブラリについては、DIをソフトウェアに容易に導入するためのヘルパーであり、静的解決・動的解決などの実装があること、Goでのライブラリの比較を例にして、それらがアプリケーション特性によるライブラリ選定の観点になることがわかりました。
日常的な開発の中でのDIを理解し活用する知識の助けになればと幸いです!
株式会社SODAの開発組織がお届けするZenn Publicationです。 是非Entrance Bookもご覧ください! → recruit.soda-inc.jp/engineer
Discussion