SODA Engineering Blog
🥕

DIの起源から遡ってDIを理解する

2024/12/19に公開

\スニダンを開発しているSODA inc.の Advent Calendar 2024 19日目の記事です!!!/

はじめに

DIと聞いてなんとなくで理解している方はいませんか?
言うまでもないかもしれませんがDIとはDependency Injectionのことであり、日本語では一般的に依存性注入と訳されることが一般的です。

弊社ではちょうどモジュラーモノリスを想定したバックエンドのモジュール分割を行っています。
モジュール分割において、DIどうするといったトピックがあったので調べてみました。

この記事ではDIについて起源から遡り、ふわっとした理解の解像度を高めることを目的とします。
DI自体はもちろん、DIの文脈で登場する用語を理解し、日常の業務や開発の場で野生のDIがでてきたときにすぐに理解できるようになるようになればよいなと思ってます。

(サンプルコードはGoで書いてます。)

Dependency Injectionの定義に遡る

タイトルのとおり、Dependency Injectionの起源まで起源まで遡ってみましょう。
原典となるMartin Fowlerのブログから確認してみます。

https://martinfowler.com/articles/injection.html

(これ読めば結構理解が進みます)

定義と思われる部分を抜粋してきました。

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選といったところです。

依存性逆転の原則について簡単に説明すると、ソフトウェアを疎結合に保つこための原則です。定義を元に以下をおさえておきましょう。

  • 上位モジュールが下位モジュールに依存してはいけない
  • 抽象は詳細に依存してはいけない

https://web.archive.org/web/20150905081103/http://www.objectmentor.com/resources/articles/dip.pdf

依存性逆転について実際にサンプルコードと図で確認していきましょう。
コードの元ネタは先程の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
 }

ここでMovieListerIMovieFinderを同じコンポーネントとしてその関係を俯瞰すると以下のよに表現できます。

なんということでしょう、依存関係が逆転してますね!

ここで先程登場した 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 {

結論としては、以下のような方法があると記載されてます。

  1. インターフェイス注入
  2. セッター注入
  3. コンストラクタ注入

サンプルコードを当てはめると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コンテナである「動的な依存解決」と「ライフサイクル管理」の動作がわかりやすかったので気になった方は確認してみてください。

https://learn.microsoft.com/ja-jp/dotnet/core/extensions/dependency-injection

Appendix: GoのDIライブラリについての比較

DIの解像度が高まったところで、GoのDIライブラリについて比較してみました。
今回はGoのDIライブラリとしてはよく名前を聞くgoogle/wireuber/digをピックアップして比較してみます。

google/wire

静的な依存解決を行うDIライブラリ

https://github.com/google/wire

uber/dig

動的な依存解決を行うDIコンテナ

https://github.com/uber-go/dig

先ほどのTipsを前提に、DIコンテナ or DIライブラリという観点でみていくと以下のような比較できます。

比較表

特徴 wire dig
解決タイミング コンパイル ランタイム
依存管理方法 静的コード生成 リフレクションによる動的解決
安全性 🟢 (コンパイルエラー) 🔺 (ランタイムエラー)
パフォーマンス 高速(コード生成済み) 比較的遅い
柔軟性 低い 高い

また「DIコンテナ or DIライブラリ」以外の側面では、以下のような観点がありそうです。

  • digはジェネリクス対応している
  • wireはメンテナンスフェーズに入っている

記載している情報から判断すると、

  • 柔軟な依存性の解決が必要である複雑で大きいアプリケーションではdig
  • 柔軟な依存性の解決が必要ではないパフォーマンスや安全性が優先なアプリケーションではwire

という総評になりそうです。

もちろんGoには紹介してないだけで他にも多くのDIライブラリはあります。
DIパターンの導入を検討している方は、ここで言及した観点を元にさらに比較検討してみても良いかもしれません。

まとめ

DIはソフトウェア開発において、疎結合な設計を実現し拡張性や保守性を向上させるための依存性逆転の原則を実践した具体的な設計手法です。
また、昨今のDIライブラリについては、DIをソフトウェアに容易に導入するためのヘルパーであり、静的解決・動的解決などの実装があること、Goでのライブラリの比較を例にして、それらがアプリケーション特性によるライブラリ選定の観点になることがわかりました。

日常的な開発の中でのDIを理解し活用する知識の助けになればと幸いです!

SODA Engineering Blog
SODA Engineering Blog

Discussion