依存性注入の基礎概念
依存性注入(Dependency Injection、DI)はオブジェクト指向プログラミングにおいて最も誤解されやすい概念の一つです。用語、目的、仕組みに関して混乱が広がっています。これは依存性注入、依存性の逆転、制御の反転、あるいはサードパーティプラグインと呼ぶべきでしょうか。DIの目的は単に単体テストを支援するためだけでしょうか、それともより広範な目的があるのでしょうか。DIとサービスロケータは同じものなのでしょうか。DIを適用するにはDIコンテナが必要なのでしょうか。
DIについて議論するブログ記事、雑誌記事、カンファレンスプレゼンテーションは数多く存在しますが、残念ながら多くの記事は矛盾する用語を使用したり、不適切なアドバイスを提供したりしています。Microsoftのような大手企業でさえ、この混乱を増幅させています。
しかし、このようにである必要はありません。本書では、一貫した用語を導入・使用します。多くの場合、他の人が定義した既存の用語を採用・明確化しますが、時に以前存在しなかった用語を追加することもあります。これにより、DIの範囲や境界に関する規範を確立するのに大きく役立ちます。
すべての不一致と誤ったアドバイスの背後にある根本的な原因の一つは、DIの境界が非常に曖昧であることです。DIがどこで終わり、他のオブジェクト指向の概念がどこから始まるのでしょうか。私たちは、DIと優れたオブジェクト指向コードを書くための他の側面との間に明確な境界線を引くことは不可能だと考えています。DIについて議論するためには、SOLID、クリーンコード、さらにはアスペクト指向プログラミングのような他の概念を導入する必要があります。他のトピックに触れずにDIについて確実に記述することはできないと考えています。
保守可能なコードの記述
DIの目的は何でしょうか。DI自体が目標ではなく、目的を達成するための手段です。最終的に、ほとんどのプログラミング技術の目的は、可能な限り効率的に動作するソフトウェアを提供することです。その一側面が保守可能なコードを記述することです。
プロトタイプのみを記述する場合や、アプリケーションの最初のバージョンを記述したことがない場合を除いて、既存のコードベースの保守と拡張を行っていることに気づくでしょう。通常、そのようなコードベースを効果的に使用するためには、保守性が高いほど良いです。
コードをより保守可能にする優れた方法の一つが疎結合です。1994年に「四人のギャング」が『デザインパターン』を執筆した頃から、これは常識でした:
「実装ではなく、インターフェースに対してプログラムする」
重要なアドバイスは結論ではなく、デザインパターンの前提です。疎結合はコードを拡張可能にし、拡張可能性は保守性をもたらします。DIは疎結合を実現する技術に過ぎません。
DIに関する一般的な神話
DIについて議論する際、多くの誤解が存在します。学習を始める前に、これらの神話を払拭する必要があります。
遅延バインディング
この文脈での遅延バインディングとは、コードを再コンパイルすることなくアプリケーションの各部分の機能を置き換える能力を指します。DIは遅延バインディングをサポートしますが、DIが遅延バインディングのシナリオにのみ関連するという考えは誤りです。DIの機能は遅延バインディングをはるかに超えています。
単体テスト
DIが単体テストのサポートにのみ関連すると考える人もいます。DIは単体テストをサポートする重要な要素ですが、それだけが目的ではありません。単体テストを記述しない場合でも、DIが提供する他のすべての利点のために、DIは依然として重要です。
抽象ファクトリ
最も危険な誤解は、DIがアプリケーションで必要な依存関係のインスタンスを作成するために使用できる某种の普遍的な「抽象ファクトリ」であるというものです。しかし、これはサービスロケータと呼ばれるアンチパターンであり、DIとは完全に逆のアプローチです。
DIコンテナ
DIにはDIコンテナが必要だという誤解も広まっています。DIコンテナはオプションのライブラリであり、アプリケーションの組み立て時にクラスの記述を容易にしますが、決して必須ではありません。DIコンテナを使用せずにアプリケーションを記述することをPure DIと呼びます。
DIの目的の理解
DIは最終目標ではなく、目的を達成するための手段です。DIは疎結合をサポートし、疎結合はコードを保守しやすくします。このメッセージを伝えるため、ソフトウェア設計と電気配線を比較してみましょう。
実生活の例:安価なホテルの滞在
安価なホテルに滞在すると、デバイスが壁のコンセントに直接接続されている状況に遭遇するかもしれません。これは、密結合されたコードを記述する一般的なアプローチに相当します。
ドライヤーが故障したときどうなるでしょうか。ホテルは熟練した専門家を呼ぶ必要があります。配線されたドライヤーを固定するには、部屋の電源を切る必要があります。これは密結合されたコードを処理する方法です。
配線とデザインパターンの比較
通常、電気機器を接続する際にケーブルを壁に直接接続することはありません。代わりに、プラグとコンセントを使用します。コンセントはプラグが適合する必要がある形状を定義します。
ソフトウェア設計と同様に、コンセントはインターフェース、デバイス付きのプラグは実装です。これは電気機器を接続するための疎結合モデルを定義します。
シンプルな例:はじめてのDI
コード例
static void Main()
{
IMensajeEscritor escritor = new ConsolaMensajeEscritor();
var saludo = new Saludo(escritor);
saludo.Exclamar();
}
このアプリケーションはコンソールに書き込む必要があるため、その機能をカプセル化する新しい`ConsolaMensajeEscritor`インスタンスを作成します。メッセージライターを`Saludo`クラスに渡し、`Saludo`インスタンスがメッセージをどこに書き込むべきかを認識できるようにします。
アプリケーションロジックの実装
public class Saludo
{
private readonly IMensajeEscritor escritor;
public Saludo(IMensajeEscritor escritor)
{
if (escritor == null)
throw new ArgumentNullException(nameof(escritor));
this.escritor = escritor;
}
public void Exclamar()
{
this.escritor.Escribir("¡Hola DI!");
}
}
`Saludo`クラスは`IMensajeEscritor`インターフェースに依存し、コンストラクタを通じてインスタンスを要求します。この手法はコンストラクタインジェクションと呼ばれます。
public interface IMensajeEscritor
{
void Escribir(string mensaje);
}
public class ConsolaMensajeEscritor : IMensajeEscritor
{
public void Escribir(string mensaje)
{
Console.WriteLine(mensaje);
}
}
DIの利点
一見すると、一行のコードを二つのクラスと一つのインターフェースに置き換えることは過剰設計に思えるかもしれません。しかし、DIは多くの利点をもたらします。
| 利点 | 説明 | 価値がある場合 |
|---|---|---|
| 遅延バインディング | サービスを他のサービスと交換できる | 標準ソフトウェアでは価値が高い |
| 拡張性 | 明示的に計画されていない方法でコードを拡張・再利用できる | 常に価値がある |
| 並行開発 | コードを並行して開発できる | 大規模アプリケーションで価値が高い |
| 保守性 | クラスに明確な責任がある | 常に価値がある |
| テスト可能性 | クラスを単体テストできる | 常に価値がある |
テスト可能性
アプリケーションが単体テスト可能な場合、テスト可能であると見なされます。疎結合は偶然にも単体テストをサポートします。なぜなら、消費者はリスコフの置換原則に従うからです。これは依存関係の具体的な型を気にしません。
何を注入し、何を注入しないか
すべての依存関係を等しく扱う必要はありません。.NET BCLの多くの型は、アプリケーションの結合度に影響を与えずに使用できます。依存関係を安定依存と不安定依存に分類することが役立ちます。
シーム--コンポーネントの接続点
抽象クラスではなく具象型に対してプログラミングすることを決定する場所ではどこでも、アプリケーションにシームを導入します。シームは、アプリケーションの様々な部分からアプリケーションを組み立てる場所です。
安定依存
BCLの多くのモジュールは、アプリケーションのモジュール化の脅威にはなりません。これらは安全または安定依存と見なすことができます。安定依存の重要な基準には:
- クラスまたはモジュールが既に存在する
- 新しいバージョンに互換性のない変更が含まれないと期待できる
- 関連する型が決定的アルゴリズムを含む
- そのクラスまたはモジュールを置換、ラップ、装飾、インターセプトする必要がないと予想される
不安定依存
依存関係をシームの後ろに分離する必要がある理由は多数ありますが、これらの理由は疎結合の利点と密接に関連しています。以下のいずれかの条件が満たされる場合、依存関係を不安定と見なすべきです:
- 依存関係がアプリケーションのランタイム環境をセットアップ・構成する要件を導入する
- 依存関係がまだ存在しない、またはまだ開発中である
- 依存関係が開発組織のすべてのコンピュータにインストールされていない
- 依存関係が非決定的動作を含む
DIのライフサイクル
DIの重要な要素の一つは様々な責任を別々のクラスに分解することです。クラスから取り除く責任の一つが、依存関係インスタンスの作成です。依存関係インスタンスの作成タスクはオブジェクトコンポジションと呼ばれます。
クラスが依存関係の制御を放棄すると、特定の実装を選択する決定だけでなく、オブジェクトのライフサイクルを制御する能力も失います。
オブジェクトコンポジション
拡張性、遅延バインディング、並行開発の利点を得るためには、クラスをアプリケーションに組み合わせる能力が必要です。これは、電気機器を差し込むのと同じように、クラスをまとめてアプリケーションを作成することを意味します。
オブジェクトライフタイム
依存関係を共有できる場合、単一の消費者はそのライフサイクルを制御できません。.NETではガベージコレクタがこれらの処理を行ってくれますが、依存関係が`IDisposable`インターフェースを実装する場合、事態はより複雑になります。
インターセプション
依存関係の制御を第三者に委ねることで、依存関係を消費クラスに渡す前に変更する能力も得られます。このインターセプト能力により、ロギング、監査、アクセス制御、検証などの横断的関心事を構造化された方法で適用できます。
まとめ
- 依存性注入は、疎結合なコードを開発可能にするソフトウェア設計原則とパターンの集合です。疎結合はコードを保守しやすくします。
- 疎結合なインフラストラクチャがある場合、誰でもそれを使用でき、変化する要件や予期せぬ要件に対応できます。
- DIは遅延バインディングをサポートし、コードを再コンパイルせずにクラスやモジュールを置換できます。
- DIは、明示的に計画されていない方法でコードを拡張・再利用しやすくします。
- DIはテスト可能性を高め、単体テスト時に依存関係をテスト実装で置換できます。
- クラスは第三者に依存関係を尋ねるべきではありません。これはサービスロケータと呼ばれるアンチパターンです。
- 不安定依存がDIの焦点です。クラスのコンストラクタに不安定依存を注入します。
- 成功するためには、DIを広範に適用する必要があります。既存のコードベースに疎結合とDIを改善するのは困難です。