Dependency Injection Principles, Practices, and Patterns
なぜ依存性を注入するのか 〜DIの原理・原則とパターン〜
超まとめ
本質ではない面倒で不安定な依存を揮発性依存という。 揮発性依存にはコードから直接依存してはいけない。 代わりに、コードにはインターフェースだけ定義して、コードの外部(合成基点)から依存を与えよう。
それはつまり、依存を制御したり生存管理したりする心配を手放し、外部に任せるということである。
そうすることでコードが疎結合になり、拡張・保守・テストが容易になる。 具体的には、介入したり、置き換えたり、モックしたりできるようになる。
また、依存を合成基点で一括して扱えるようになり、保守容易性が向上する。
1. 依存注入(Dependency Injection :DI)の基本
DIとは、様々なソフトウェア設計の原則やパターンを集めたもの。 DIの目的は、コードを疎結合にすることで、保守容易性を高めること。
DIの文脈では、抽象のことをサービス、実装のことをコンポーネントということがある。
DIの目的
以下はDIに対してよくある誤解なので、完全に忘れること。
- ❌️ DIは遅延バインディング(インターフェースだけを事前に定義して、実装は実行時に選択する)にしか使えない
- それはDIの用途のごく一部にすぎない
- ❌️ DIは単体テストにしか使えない
- それはDIの用途のごく一部にすぎない
- ❌️ Abstract Factory パターンと似たものである
- Abstract Factoryとは、OSごとにUIを出し分けるときなどに使うと便利なやつで、必要な抽象メソッド名だけが定義されたファクトリーのこと
- 依存を必要になった場所でAbstract Factoryを使うようなコードのことをService Locator パターンという
- 依存を必要とするクラス自身が外部に対して命令的に依存を取りにいくもので、DIとはむしろ対極にある
- ❌️ DIするにはDIコンテナが必要である
- DIコンテナはあくまで任意であり、Pure DIという方法もある
- なお、DIコンテナを Service Locator として使うのは完全な間違い
DIとの関係でよく出てくる4パターン
- Decorator パターン
- 機能追加や横断的関心を、既存コードを変えることなく実装するためのパターン
type Decorator<T> = (service: T, ...deps: any[]) => Tなイメージ、Tが抽象- e.g. コンセントと機器の間にUPSを入れる
- Composite パターン
- 既存の実装クラスとは別の実装クラスを追加してリファクタリングするためのパターン
- e.g. 電源タップを使って複数の機器を使う
- Adapter パターン
- 微妙に異なる2つのインターフェースを組み合わせてつけるようにするためのパターン
- e.g. 海外で形の違うコンセントにアダプタを使う
- Nullオブジェクト
- サービスが利用不可能でもエラーにならないようにするためのパターン
- e.g. 使わないコンセントにダミーのキャップをはめておく
リスコフの置換原則とは、将来起きうる未知の変更に対応するための考え方。 実装は、ほかの実装に取り替え可能であるべきという原則。
開放閉鎖の原則(Open/Closed Principle: OCP) とは、 既存のコードを変更することなく機能追加を可能にするための考え方。 いつでも拡張はできるけど、既存のコードは変更されないし、する必要もない状態に保つ。
DIの本質とは、コンセントと家電の例のように、基準を定めて疎結合にすることで、 既存コードに手を入れることもなく、将来の想定外の変化に応えられるようにすること。
疎結合にする簡単な方法はインターフェースに対してプログラミングをするということ。 そうすることで、インターフェースはnewできないので「どこでオブジェクトを生成するか」という問いが生まれる。 その問いを解決するのが、DIである。
DIのメリット
DIを実現する方法の一つに、コンストラクタ経由でのDI(Constructor Injection)がある。 必要とする依存を、コンストラクタの引数にインターフェースとして静的に定義する方法。
以下はPure DIの例。
// 依存するインターフェース(抽象)
interface Logger {
log(message: string): void
}
// Loggerの具体的な実装
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`[LOG] ${message}`)
}
}
// UserServiceクラスは、コンストラクタでLoggerを受け取る
class UserService {
// コンストラクタで依存を注入
constructor(private logger: Logger) {}
createUser(name: string): void {
// ユーザー作成処理
this.logger.log(`ユーザー「${name}」を作成しました`)
}
deleteUser(name: string): void {
// ユーザー削除処理
this.logger.log(`ユーザー「${name}」を削除しました`)
}
}
// 使用例
const logger = new ConsoleLogger()
const userService = new UserService(logger) // コンストラクタ経由で依存を注入
userService.createUser('太郎')
userService.deleteUser('次郎')
上記のコードは、インターフェースに対してプログラミングをすることで、疎結合を実現している。 でも、本来なら1行で書けるものを、こんなに大量のコードにする正当性がある? つまり、疎結合(DI)のメリットとは?
- 遅延バインディング / Late binding
- JSONなどの設定ファイルによって挙動を制御できるようになる
- アプリの種類によっては不要ではあるが
- 拡張容易性 / Extensibility
- 抽象を使ってコードを書くと、できることが規定&制限され、自然と変更に対してClosedになる
- 実装にはDecoratorパターンにより、あとから機能を追加できるので、拡張に対してOpenになる
- 並列開発 / Parallel development
- 抽象が唯一の契約となり、複数チームでの開発が容易になる
- 保守容易性 / Maintainability
- 単一責任の原則により、影響範囲が明確になる
- テスト容易性 / Testability
- ユニットテストも統合テストもやりやすくなる
- アプリの種類に関わらず必須
DIすべき対象
安定依存(stable dependency) とは、常に存在し、後方互換が保証され、決定的(純粋関数的)な振る舞いをするもの。 また、将来にわたって、その依存を置き換えたり、DecorationやInterceptionする必要がないもの。 言語の標準ライブラリや、ドメインに特化した権威的なライブラリなどが当てはまる。
揮発性依存(volatile dependency) とは、前述の「疎結合のメリット」を崩すもの。 「テストや拡張を難しくするもの」といえる。
- 導入に際して設定や調整が必須の依存(例えばRDBの設置など)
- 非決定的な振る舞いの依存
- 実装がまだない依存
- 一部の環境でしか動作しない依存
接合部(stem) とは、抽象と実装の境目のこと。
なんでも抽象化すればいいわけではなく、揮発性依存をDIで扱えるようにするのが大事。 これを後からやるのは至難の業なので、最初からアプリ全体でDIできるようにするのが大事。
DIの特性
オブジェクト合成(object composition) は、 依存となるオブジェクトを外部で組み立て、その依存を必要とするクラスにあたえ、 そのクラスのオブジェクトを生成すること。
クラスが依存を制御することを放棄して、その責務を外部に丸投げすることにより、 依存の一元管理・横断的管理や、生存期間(lifetime) の管理がやりやすくなる。
介入(interception) は、 実装を差し替えることで処理を追加したり改変したりする能力のこと。 つまり、Decoratorパターンのこと。 ログ、アクセス制御、監視といった横断的関心事を、疎結合なコードとして追加するために必要である。
なお、DIと制御の反転(Inversion of Control: IOC)は似ているが、DIはIoCの一例にすぎない。
2. 密結合したアプリケーション
合成容易性を評価するためには「別のモジュールに置き換えられるか」を見ていくと良い。 レイヤリングしているにも関わらず、一部のモジュールを置き換えようとしときに、依存元コードを大量に修正しないといけないなら、負け。 通称、ラザニアアーキテクチャ。
当たり前にコードを書いていくと、IDEの自動インポート支援なども相まって、簡単に密結合する。 すべての密結合が悪ではないが、揮発性依存は疎結合にしなければならない。
クラスを変更する要因が2つ以上あるなら、単一責任の原則に反していることを疑え。 凝集性の高いコードなら、要因は1つしかないはず
遅延バインディングする場合、コンフィグファイルの読み込みはアプリケーションのエントリポイントに近い場所で行い、 それ以外のコードは単に設定を受け取るだけにするべき。
3. 依存性を注入したアプリケーション
どこから作るか
外から見える場所(UI側)からアプリを作っていくと、より早いフィードバックと効率的な開発スピードを得られる。 これはYAGNI原則と関連性を持った、理にかなった進め方だ。
その意味で、まずはUI側で必要なモデル、View Model から作ると便利。
たとえばProductViewModelみたいな。
そうすれば、とりあえずUI層の中だけで、そのインターフェースにハマるダミーデータを作り、仮実装ができるから。
とはいえ、これらDTOはDIの観点ではあまり重要ではない。 DTOはただのデータに過ぎず、差し替えたり介入したりすることは無いから。
合成基点
コンストラクタ経由での注入 は、依存を制御をする責務を他のクラスに押し付けているだけ、といえばそれだけだ。 でもそれが重要で、責務を合成基点(Composition Root) まで持っていけることにつながる。
合成基点は、まるでコンセントとプラグを自由に組み合わせるように、アプリケーションを柔軟に組み替える場となる。 合成基点はエントリポイントに限りなく近い場所になる。 ここでは、Pure DIしてもいいし、DIコンテナへ委譲してもいい。
合成基点はUI層と同じ層に置かれることも多い が、これはある種のトレードオフによる意図的な選択であり、 合成基点がUI層の一部になるわけではないので注意する。 本来なら合成基点とUI層は別の場所であるのが原則。
IoCの効能と使い所
DIはIoCと深く関わる。 IoCの文脈で大切なのは、主役となるレイヤーがインターフェースを主体的に管理し、 もっとも使いやすい形で定義する権限 を持っていることである。
何でもインターフェース化すればいいわけではない。 短命なオブジェクトには、置き換え・介入・モックが不要なので、抽象化しても無駄骨に終わる。 例えば、DDDのエンティティ、DTO、View Modelなど。
DIコンテナ
DIコンテナとは、オブジェクト合成(Object composition)、介入(interception)、 生存管理(lifetime management)に関する作業を自動化するソフトウェアのこと。 合成基点の保守をより行いやすくしてくれるものといえる。 IoCコンテナと呼ばれることもある。
DIコンテナを使うとコードの全体像を把握するのが難しくなるというデメリットもある。 その点、Pure DIであれば、合成基点を見れば何をやっているかはすぐに分かる。
その他
- メソッド経由での注入(Method Injection) とは、関数の引数で依存を受け取る方法のこと。 エンティティなど、比較的短命なオブジェクトが依存を必要とする場合に使われる。
- ユーザーに関するコンテキスト は、ドメイン層でインターフェースとして定義しておき、 UI層などで事前にアダプトさせてからドメイン層に渡す形にすると良い。 フレームワークごとの流儀を考えなくて済むし、別のフレームワークに乗り換えるのも簡単になるので。
4. DIのデザインパターン
合成基点 / Composition Root
合成基点はエントリポイントに限りなく近いところに置く。例えばMainメソッドなど。
その中で、UI層、ドメイン層、データアクセス層のモジュールを好きに組み合わせて、アプリケーションをスタートする。 合成基点に関するコードは独立した関数に切り出しておくと保守性が高まる。
// 合成基点 - Mainメソッドに限りなく近い場所で使う
function createHomeController() {
// データアクセス層
const userRepository = new UserRepository()
// ドメイン層
const userService = new UserService(userRepository)
// UI層
const homeController = new HomeController(userService)
return homeController
}
大切なのは、合成(オブジェクトグラフの構築)は合成基点でのみ行なうということ。 そうしないと介入が難しくなるから。
また、もしDIコンテナを使う場合、合成基点の外で使うのはNG。 Service Locator というアンチパターンになっちゃうから。
合成基点から、UI層、ドメイン層、データアクセス層などに向いた、多くの依存が発生することは問題ない。 依存は推移的に発生するものであるため、たとえ直線的な依存関係で組み立てたとしても、 コードベースが大きくなるにつれて依存関係の数は爆発的に大きくなってしまう。 一方で、合成基点でまとめて依存関係を処理する構成にすれば、結果的に依存関係の数を少なく保てる。
コンストラクタ経由での注入 / Constructor Injection
ほとんどの場合に最善の選択肢となるのが、Constructor Injection だ。
アプリケーション起動時に、合成基点でまとめて、一度だけ、依存をセットアップする。 依存の存在が常に保証され、使い方も簡単であるという利点がある。
特に、依存のローカルデフォルトを用意できない場合に最適だ。 ローカルデフォルト とは、同じレイヤーに所属する、(抽象に対する)実装のこと。
例えば、リポジトリのローカルデフォルトは作れない。 なぜなら、それを作るということはドメイン層から特定のRDBへの依存、 つまりデータアクセス層への依存を作ってしまうからだ。
こういった場合、抽象だけ用意して外部から受け取る、つまりDIが必要になる。 そして、常に依存の存在が保証される Constructor Injection がマッチする。
メソッド経由での注入
メソッド呼び出し時に依存を外部から渡す方法。 以下のようなケースで利用する。
- Data-centric Objectが、依存を必要とする場合
- Data-centric Objectとは:
- データの保持・表現が主な目的のオブジェクト
- 実行時に動的に生成される
- 合成基点の時点ではまだ存在しない
- e.g. ドメインエンティティが特定のメソッドを実行するときに依存を必要とする場合など
- Data-centric Objectとは:
- メソッドを呼び出すたびに違う種類の依存が必要となる場合
- e.g. 価格計算をする際にユーザーコンテキストを依存として必要とする場合など
外部から見分けることのできない密結合、いわゆる 一時的結合 / Temporal Coupling を生み出しがちなので、注意して使うこと。 (例えば、あらかじめInitializeメソッドを呼んで依存を与えておかないと、クラスを使ったときにエラーになる、みたいな)
プロパティ経由での注入 / Property Injection
ローカルデフォルトが用意できており、必要に応じて任意に上書きしたい場合に使う。 ローカルデフォルトをクラスプロパティに持っておき、 必要に応じて一度だけセットできるようにすることで実現する。 (関数型言語なら、カリー化 x 部分適用 x デフォルト値の設定で対応可能だろう)
ただし、依存が任意になる場合というのはめったにない。 特に、ライブラリで使うのはいいが、アプリケーションでは使うな。 合成基点でローカルデフォルトを注入すれば済む話なので。
また、そもそもアプリケーションではローカルデフォルトは避けるべきものである。
5. DIのアンチパターン
以下は、すべてコンストラクタによる注入などにリファクタリングすべし。
Control Fleak
IoCに反して、依存に直接依存したり、依存を直接制御したりすること。
合成基点の外で、揮発性依存の実装をnewすることで発生する。
ファクトリを使うと、この問題が発生しやすい。 (Concrete|Abstract|Static) Factory のいずれを使ったとしても、 結局は実装への直接依存が発生してしまうのでダメ。
また、依存が未指定の場合に外部デフォルトにフォールバックするコードでも発生しやすい。 これは、実のところ別レイヤーへの依存が常に発生してしまっている。
Service Locator
合成基点の外で、揮発性依存の取得を無制限に行えるようにするアンチパターン。 依存を必要とするクラスに、その依存を外部から見えない形で提供できるようにする仕組み。
DIコンテナもサービスロケーターも、機能・役割はほぼ同じだが、 後者には以下の欠陥がある。
- サービスロケーターを使うクラスは、サービスロケーターを余分な依存として持つはめになる
- サービスロケーターを利用しているクラスを外から見ても、必要な依存が何なのか分からない
- 依存が常に提供されていることが保証されない
- エラーが起こるとすれば、コンパイル時ではなくランタイム時であり、危険
Ambient Context
揮発性依存へのアクセスを、staticなアクセス・メソッドを介して、 シングルトンな形で提供すること。
例えば、現在時刻の取得を行う静的メソッド関数を持つクラスを各所で呼び出して使うなど。
例えばログなどの横断的関心事を扱おうとすると、過剰なConstructor Injectionが発生しがちで、 それを避けたいという同期から、Ambient Contextが生まれることが多い。
依存を隠してしまうため、テストや介入が難しくなる。
Constrained Construction
遅延バインディングを使うときにのみに発生する。 特定の抽象に対するすべての実装に対して、コンストラクタに特定のシグネチャを持つことを強いること。 詳細略。
6. Code Smell
アンチパターンとは、確実に間違ったやり方を指す。 一方で、Code smellは明確に間違いであるとは言いづらいものの、問題を含む可能性を示唆しているものを指す。
Constructor Over-Injection
コンストラクタ経由での注入が多すぎる場合、いくつかのリファクタリング方法がある。
まず、単一責任の原則 (Single Responsibility Principle: SRP)が守られているかまずはチェックして、 必要に応じてコードを分割しよう。
依存が横断的関心事であれば、Decoratorパターンで引数を減らそう(詳細は9章)。
複数の依存間で意味的なまとまりや処理のまとまりがある場合は、 新たに別の抽象を作ってそこに集約することで、引数を減らそう。 このような抽象をFacadeサービスという。 特に、同じ種類の抽象を集約する場合は Compositeパターンが使える。
ドメインイベントによる処理系に変更することも効果的だ。
イベントを処理するIEventHandler<TEvent>のような、総称型を持つハンドラーを外からDIしてやる。
ハンドラーはComposite可能なものにすることで、イベントの種類が増えたり、
イベントごとに必要な処理が増えても、シンプルに対応が可能になる。
Abstract factory の誤用
ある抽象は、別の抽象を入力値として受け取るべきでなく、別の抽象を戻り値として返すべきでもない。 なぜなら、そうすると別の抽象について把握する必要性が出てしまうからだ。
Abstract Factory が生成するオブジェクトは抽象型であるため、 前述の問題が発生してしまう。よって、できるかぎり使うべきではない。
そもそも、Abstract Factory を使うと Service Locator パターンになってしまうので、その意味でも使うべきでない。 ただし、合成基点の中で使うのは全く問題ない。
以下の2つは、よくあるパターンとその改善策だ。
しばしば、すでに存在する具象をもとに、抽象化をしないといけないときがある。 こういうときは、引数や戻り値にうっかり実装詳細が含まれないように注意深く作業する。 それができている丁寧な抽象化を Deep Extraction や Deep Interface という。 それができておらず、本来は特定の層でしか使われないはずの振る舞いを漏洩している抽象を Leaky Abstraction という。
例えば、使い終わった後にdispose()的なことをしないといけないリポジトリ実装があるとする。
これを安直に抽象化すると、利用側にリポジトリの生存管理の責務(disposeする役割)が発生してしまう。
このため、利用箇所に対して Abstract Factory を注入したうえで、
利用箇所においてリポジトリを生成し、生存管理をする必要性が出てくる。
しかし、これは実装詳細の都合が利用側に漏れ出している悪い状態であるといえる。
こういうときは Proxy パターン を使い、 生存管理の責務だけをProxyに持たせることで解決できる。 Proxyはデータアクセス層に置き、合成基点ではProxyを介してリポジトリを作成する。
ランタイムの値に基づいて揮発性依存(e.g. アルゴリズム等)を動的に選択したいときにも、 安易に Abstract Factory を使って、命令的に依存を取得しがちである。 このときは Adapter パターン を使って間に仲介者を挟み、 そこで適切な依存をLookupする機能を持たせることでリファクタリングできる。
循環依存
実装Aが抽象Bに依存し、抽象Bが抽象Aに依存すると、発生する。 単一責任の責任の原則を守れていないときに発生しがち。
解決方法としては以下がある。
- クラスを分割する
- 責務を整理し、多くのメソッドを持つクラスを細かく分けることで依存グラフを断ち切る
- イベントを発行する
- 依存を直接呼び出して処理するのではなく、イベントを発行してそれに反応させるアーキテクチャに変更する
- プロパティ経由での注入に変更する
- 循環している依存を任意とすることで、依存をまずは仮で作成し、後から依存を注入する
- Virtual Proxyを作成してラップし、依存の存在をチェックする形にすればより安全
- 悪魔的手法なので最後まで使うな
7. オブジェクト合成
合成基点とすべき場所は、アプリケーションの種類によって異なる。 いずれにせよ、適切な接合部(stem)を探し出すことが重要で、 それさえできればどんなアプリケーションでもDIは可能である。
合成基点となる代表例として、CLIならMainメソッド、UWPならAppクラス、 .NETならコントローラーアクティベーターなどが挙げられる。 いずれもアプリケーションのルートに近い場所である。
合成基点では以下の4つを行う。
- コンフィグの取得 (独立したメソッドにする)
- オブジェクトグラフの構築
- 目的の機能の呼び出し
- オブジェクト・グラフの解放
8. オブジェクトの生存期間 (lifetime)
Composerとは、依存を合成するメソッド群のこと。 DIコンテナか、Pure DIの場合は開発者自身が作成したメソッドを指す。
生存戦略(lifestyle) とは、依存がどれくらい生存することを意図しているか、をパターン化したもので、 主にComposerがその管理責任を負う。
Singleton Lifestyle は、一度だけ作成した依存を複数のクラスで共有して使う生存戦略のこと。 合成基点で一度だけ生成して使い回すので、メモリ効率がいい。
Transient(短命) Lifestyle は、その都度作り直す、短命な生存戦略のこと。 例えばHTTPリクエストごとにリポジトリを作成し直す場合など。 (スレッドセーフではない言語ではそうせざるをえないことがある)
Captive(捕らわれた) Dependency とは、間違って、想定よりも長生きさせられてしまう依存のこと。 原因は、その依存を利用するクラスが、その依存が想定した生存期間を超えて保持しつづけるから。 DIコンテナを使うとよく発生する。
9. 介入
介入とは、連携する2つのオブジェクトの間に割り入り、 2つのオブジェクト自体には一切変更は加えずに、 追加の処理を加えたり、別の振る舞いに変更したりすること。
多くの場合で非機能要件である、横断的関心事 に使われることが多い。 例えば監査証跡に関する機能などが代表例だ。
介入は Decorator パターン で行う。 これは、クラスをクラスでラップして、本来の処理の前後に任意の処理を加えるやり方。
Decoratorは、内包するクラスと同じ抽象を実装する。 Decoratorの中に直接指示は書かずに、デコレートしたい内容も抽象で受け取って、インターフェースで処理を書く (Decoratorはあくまでデコレートするだけ)。 関数型言語なら、Higher-order functionを使うことで同じことができるだろう。 なお、デコレーターはマトリョーシカのようにネストさせることも可能である。
横断的関心事(Cross-cutting concern) の具体例としては、 監査・監視・ログ・セキュリティ・キャッシュ・エラーハンドリング等がある。 層をまたいで実装されることもある。
10. アスペクト指向プログラミング
横断的関心事は、その性質から、どうしても複数のクラスやメソッドに散らばってしまいがち。
アスペクト指向プログラミング(Aspect-oriented programming: AOP)は、 こうした横断的な機能を「アスペクト」として切り出し、本来のビジネスロジックから分離する。
特殊なツールを使って、実行時やコンパイル時に強制的にコードを突っ込む方法が一般的だが、ツールなしでもできる。
よくわからんので詳細略。
12. DIコンテナ
DIコンテナは、オブジェクトグラフの構築・管理を行うライブラリ。
自動解決(auto-wiring)は、コンテナに登録された抽象と実装の紐づけを元に、 対象のオブジェクトグラフを自動で構築する能力。 合成基点への変更が少なくなるメリットがある。
紐づけは、コンフィグファイルで行う、コードで行う、自動登録で行う、の3つがある。
DIコンテナを自作するな。ライブラリを使うか、Pure DIのどちらかにしろ。
合成基点が小さいなら、Pure DIを使え。 合成基点の保守が辛くなったら、はじめてDIコンテナの自動登録を使うといい。
その他
- SOLID原則
- 単一責任の原則 / S
- クラスは責務を一つしか持つべきでない
- 変更される理由が1つしかない状態を保て
- 開放閉鎖の原則 / O
- 新しいことをしようとしたときにコード全体を修正しなくて済むようにしろ
- リスコフの置換え原則 / L
- 抽象を利用するコードは、どんな実装が与えられても、正常に動作するようにしろ
- インターフェイス分離の原則 / I
- インターフェイスを利用するコードに対して、使用しないメソッドに依存することを強いるべきではない
- 依存関係逆転の法則 / D
- 上位モジュールは下位モジュールに依存するな。どちらも抽象に依存しろ。
- 単一責任の原則 / S