はじめに
C#でクラス設計をしていると、依存性注入 (DI : Dependency Injection) というものがとても重要になってきます。
以下の記事で説明したポリモーフィズムが依存性注入に深く関係しています。
はじめに こうちゃ こんにちは、こうちゃです。 オブジェクト指向の重要な概念として、ポリモーフィズム (多態性) というものがあります。 これまでみてきた abstract virtual […]
今回は、依存性注入 (DI) の仕組みとメリットについて 説明していきます。
- 依存性注入 (DI) とはなにか
- 依存性注入 (DI) のメリット
- クラス単位のテストを行うなど保守性について関心のある方
- オブジェクト指向に興味のある方
依存性注入 (DI) とは
DIなしの例
class Game
{
private Enemy enemy;
public Game()
{
enemy = new Enemy();
}
public void Start()
{
enemy.Attack();
}
}
上記のようなクラス設計の場合、何が問題になるでしょうか。
それは、メンバ変数「enemy」が他クラスに差し替えられない ということです。
もちろん、この「Game」クラス自体を修正すれば差し替えられますが、非常に柔軟性・拡張性が低いです。
現在のこの設計は「Game」クラスが「Enemy」クラスに強く依存している と言えます。
DIを利用しない際の問題点
- 特定のクラス (今回だと「Enemy」) に強く依存する
- 別クラスに差し替えができない
- テストなどが実施しにくい (後述)
DIありの例
interface IEnemy
{
void Attack();
}
class Enemy : IEnemy
{
public void Attack()
{
Console.WriteLine("攻撃!");
}
}
class Game
{
private IEnemy enemy;
public Game(IEnemy enemy)
{
this.enemy = enemy;
}
public void Start()
{
enemy.Attack();
}
}
DIなしの時と「Game」クラスを比較して、
- メンバのインスタンスが「Enemy」(クラス) から「IEnemy」(インターフェース) に変更
- コンストラクタで「IEnemy」のインスタンスを引数で渡し、それをメンバのインスタンスに注入
(DIなしでは「Enemy」クラスでインスタンス化)
という違いがあります。
こうすることで、インスタンス「enemy」は外部で作成して「Game」クラス生成時に注入することが可能になります。
IEnemy enemy = new Enemy();
Game game = new Game(enemy);
game.Start();
DI利用のポイント
- 「Game」は「enemy」の実体を知らない
- 「IEnemy」にのみ依存しており、「enemy」を差し替え可能
→DIを利用すると 依存度を弱め、柔軟性・拡張性が上がる
依存性注入 (DI) のメリット
インスタンス実体の差し替えが簡単
メリット1つ目として、差し替えが簡単になる ということがあります。
class Boss : IEnemy
{
public void Attack()
{
Console.WriteLine("強力攻撃!");
}
}
IEnemy boss = new Boss();
Game game = new Game(boss);
game.Start();
このように、「Game」クラスの「enemy」インスタンスに「Boss」クラスのインスタンスを渡すことも可能です。
「Game」クラスは一切変更せずに、中身の動き (「enemy」に関わる部分) を変えることができることはメリットと言えるでしょう。
テストが実施しやすい
メリット2つ目としては、「Game」クラスのテストがやりやすくなることです。
class MockEnemy : IEnemy
{
public int attackCallCount = 0;
public void Attack()
{
attackCallCount++;
}
}
IEnemy mockEnemy = new MockEnemy();
Game game = new Game(mockEnemy);
game.Start();
Console.WriteLine($"Attackメソッドの呼び出し回数: {((MockEnemy)mockEnemy).attackCallCount}");
「Attack」メソッドでメンバ変数をインクリメントする「MockEnemy」クラスを作成しました。
「MockEnemy」を「Game」クラスに渡すことで、「game.Start()」で「enemy.Attack()」が正常に呼ばれているかを確認できるようになります。
このように、「Game」クラスのテストの際に「enemy」にテスト用クラスを注入できるため、テストが簡単に実施できるのは大きなメリットと言えます。
クラス同士が疎結合になる
DIなしの例のように、「Game」クラスが「Enemy」クラスのインスタンスを所持している状態は、クラスが密結合であると言えます。
ですがDIを利用することで、「Game」クラスが「Enemy」クラスに依存しなくなったので、疎結合になりました。
このように、クラス同士の結びつきが弱くなると変更に強い設計となるため、より保守性が向上するというメリットがあります。
ポリモーフィズムと依存性注入 (DI) の関係
冒頭で、ポリモーフィズムはDIに深く関係していると言いました。
ポリモーフィズムは 同じ呼び出し方で動作が異なる仕組み でした。
DIはこのポリモーフィズムという仕組みがあるからこそ成り立っていると言えます。
ポリモーフィズムがあるから 異なる動作をする実体を外部から渡す ことができます。
今回の例では、ポリモーフィズムをインターフェースを使って実現しました。
この3つの関係は以下のようになります。
- インターフェース → 呼び方を統一
- ポリモーフィズム → 動作を変える
- DI → 動作を変えるための実体を差し替える
このように、DIは3つセットで初めて機能する仕組みになります。
まとめ
依存性注入 (DI) は、依存するインスタンスを外部から渡す仕組み になります。
以下のように考えましょう。
- DIなし → 単一のクラスしか使えない
- DIあり → 宣言されているインターフェースを継承している全クラスで差し替え可能
さいごに
今回は、依存性注入 (DI) について説明しました。
インターフェースとかポリモーフィズムとか、今まで取り上げてきた内容が登場しましたね。
DIについては、ポリモーフィズムという仕組みがあるからこそ成り立っている機能です。
このように、各機能・仕組みの関係がわかってくると、「なんでこの機能・仕組みが必要なの?」がわかってきておもしろいですね。
DIについては、今後の記事でも登場予定ですので、次の登場を楽しみにしていてください。(笑)
今回はここまで♪
楽しいプログラミングライフを!