はじめに
プログラミングのテクニックのひとつに、DI(依存性注入, Dependency Injection)というものがあります。
文字面からして難しそうですが、DIはコードを簡単に読み書きするためのやさしさに満ちた概念です。
辞書的な説明はWikipedia等にたくさんありますから、この記事ではC#のサンプルコードを使って、具体的で実践的なDIの概説をしていきます。はじめよう、DI!
この記事で分かること
- Dependency(依存性)という言葉の意味するところ
- DI(依存性注入)の概念と具体例
前提知識
- クラスの概念
- インターフェースの概念(ざっくりでOK)
具体的には、下記のコードが読めれば大丈夫です。
ホゲモンというモンスターが登場するデジタルゲームを想定したサンプルコードです。
// モンスターの動き(ふるまい)を定義するインターフェース
public interface IMonster
{
void Attack(); // 攻撃する関数
}
public class Hogemon : IMonster // IMonsterを実装したクラス。ホゲットモンスター、略してホゲモン。
{
public string name; // ホゲモンのなまえ
public void Attack() // IMonsterを実装しているので、Attack関数を実装をしないとエラー
{
// 攻撃する処理...
}
}
public class Trainer // ホゲモンの飼い主 ホゲモントレーナーの情報を表すクラス
{
private Hogemon myHogemon; // 飼っているホゲモン
}
そもそも、依存性(英:dependency)とは?
依存性注入を理解するために、いったん「注入」の部分は忘れましょう。
まずはプログラミングにおける「依存性」の意味を深堀りしていきます。
「依存」という言葉には、あまり良いイメージがありませんよね。アルコールとか。
「依存」は「独立」(英:independence)の対義語といえます。
依存とは、「あるものに頼りきって存在している状態」のことであり、「それなしでは存在できない」ことです。
依存は私たちの身の回りに溢れています。
たとえば、あなたがこの記事を読むのに使っているであろうパソコン、スマートフォン、あるいはニンテンドー3DSのようなデバイスはすべて「電気」に依存しています。
そのデバイスは電気なしで動作しません。ご飯やお水、マナのようなもので動かすことは不可能です。
依存は望ましくない状態でありながらも、このようにやむを得ず発生してしまうものです。
「依存」は、プログラミングの文脈でも全く同じ意味で用いられています。
ここで、先ほどのホゲモントレーナーの例を見てみましょう:
// モンスターの動き(ふるまい)を定義するインターフェース
public interface IMonster
{
void Attack(); // 攻撃する関数
}
public class Hogemon : IMonster // IMonsterを実装したクラス。ホゲットモンスター、略してホゲモン。
{
public string name; // ホゲモンのなまえ
public void Attack() // IMonsterを実装しているので、Attack関数を実装をしないとエラー
{
// 攻撃する処理...
}
}
public class Trainer // ホゲモンの飼い主 ホゲモントレーナーの情報を表すクラス
{
private Hogemon myHogemon; // 飼っているホゲモン
+
+ // 自分の飼っているホゲモンに攻撃させる
+ public void OrderHogemonAttack()
+ {
+ // ゲームのテキストメッセージを出す。例:ピカチュウ!攻撃しろ!
+ Console.WriteLine($"{this.myHogemon.name}!攻撃しろ!")
+ // 以下、ゲームに必要な処理...
+ }
}
OrderHogemonAttackという関数を追加しました。
ホゲモントレーナーがホゲモンに命じて攻撃させるときのテキストを出力しています。
さて、このOrderHogemonAttackという関数が「Hogemonクラスに依存している」ということに気づきましたか?
この関数の中で、Hogemonクラスのnameというメンバ変数を参照していますね。
// ゲームのテキストメッセージを出す。例:ピカチュウ!攻撃しろ!
Console.WriteLine($"{this.myHogemon.name}!攻撃しろ!")
このとき、もしホゲモンの世界で法改正があり、ホゲモンに姓と名が与えられたとします。
public class Hogemon : IMonster // IMonsterを実装したクラス。ホゲットモンスター、略してホゲモン。
{
- public string name; // ホゲモンのなまえ
+ public string firstName; // ホゲモンのなまえ
+ public string familyName; // ホゲモンのみょうじ
// 以下略
}
このように変更するとHogemonクラスからはnameというメンバ変数が失われ、OrderHogemonAttack関数はコンパイルエラーを起こします。
これはOrderHogemonAttack関数が、ひいてはTrainerクラスそのものが、「Hogemonクラスにはnameというpublicなメンバ変数がある」という前提に依存していることが原因です。
「Hogemonクラスに依存している」という言葉のニュアンスは掴めたでしょうか?
Hogemonクラスが自分の想定と異なる実装になっていると処理が失敗してしまう──。
このような状態のことを、プログラミングの文脈では「依存性」と呼ぶわけです。
依存性を安全に扱う方法
先ほどの「電気に依存している」という例で説明したように、「依存」は都合の悪い状態ではありますが、やむを得ない、避けて通れないという場合がほとんどです。
依存そのものは、悪ではありません。
プログラミングにおいて依存が危険になるのは、「Hogemonクラスの内容を変更すると、Hogemonクラスに依存している別のクラス(Trainerクラスなど)がエラーを起こす可能性がある」ような場合です。
この状況を解決する方法の一つが「DI = 依存性注入」なのですが、
DIを行うためには「抽象化」という考え方が必要になります。
C#では「インターフェース」を使うことで「抽象化」を実現できますので、まずはインターフェースについて基本的なことから確認していきましょう:
// モンスターの動き(ふるまい)を定義するインターフェース
public interface IMonster
{
void Attack(); // 攻撃する関数
}
public class Hogemon : IMonster // IMonsterを実装したクラス。ホゲットモンスター、略してホゲモン。
{
public string name; // ホゲモンのなまえ
public void Attack() // IMonsterを実装しているので、Attack関数を実装をしないとエラー
{
// 攻撃する処理...
}
}
このように、IMonsterインターフェースを実装したHogemonクラスは「public void Attack()という関数を実装すること」を強制されます。(実装しなければエラーになります。)
これは基本的なインターフェースの機能ですね。
さて、Trainerクラスはホゲモンの名前をOrderHogemonAttack関数内で使いたいわけですが、Hogemonクラスには依存させたくない。
こんなときは、次のように設計を変更します:
// モンスターの動き(ふるまい)を定義するインターフェース
public interface IMonster
{
+ string Name { get; set; } // モンスターのなまえ
void Attack(); // 攻撃する関数
}
public class Hogemon : IMonster // IMonsterを実装したクラス。ホゲットモンスター、略してホゲモン。
{
- public string name; // ホゲモンのなまえ
+ public string Name { get; set; } // ホゲモンのなまえ
public void Attack() // IMonsterを実装しているので、Attack関数を実装をしないとエラー
{
// 攻撃する処理...
}
}
名前を表す変数(プロパティ)をインターフェース上で定義しました。
するとHogemonクラスはNameという変数(プロパティ)を持つことを強制されるため、この変数が消えたり、名前が変わったりすることによるエラーは事前に防止することができます。
インターフェースで変数を定義したい場合は、プロパティの形式にしなければなりません。
本来インターフェースは関数を定義する場所であり、変数は書けません。
しかしプロパティは内部にgetter, setterという2種類の関数として動作するので、これを利用して変数を定義できるのです。
細かいところではありますが、プロパティは頭文字を大文字にするという命名規則が一般的です。今後はnameではなく、Nameという書き方で統一しましょう。
// ゲームのテキストメッセージを出す。例:ピカチュウ!攻撃しろ!
- Console.WriteLine($"{this.myHogemon.name}!攻撃しろ!")
+ Console.WriteLine($"{this.myHogemon.Name}!攻撃しろ!")
↑↑↑
ここまでの変更でNameという変数を使う処理に関してはHogemonクラスに依存しなくなったと言えます。しかし、まだ根本的な依存性は残っています。
private Hogemon myHogemon; // 飼っているホゲモン
Trainerクラスは、Hogemonクラスそのものへの参照を持っています。
現在の実装は、HogemonクラスがIMonsterインターフェースを実装していることを前提としているのです。
そのため、次のように変更します:
- private Hogemon myHogemon; // 飼っているホゲモン
+ private IMonster myMonster; // 飼っているモンスター
このように変更することで、TrainerクラスはmyMonsterという変数に入っている情報について、「IMonsterインターフェースを実装しているなにか」としか認識できなくなります。
つまり、TrainerクラスはIMonsterに書かれた情報(Nameというプロパティと、Attackという関数)にしかアクセスできなくなります。
これが「抽象化」です。
もともとこのように、TrainerがHogemonに一方的に依存していたものを
このように抽象化レイヤーを間に挟みTrainerクラスとHogemonクラスの双方がIMonsterに依存するよう変更したのです。
HogemonクラスとTrainerクラスは双方のお互いの実装内容を気にする必要がなくなり、ただひとつ、インターフェースだけに従えばよくなります。これにより、片方の変更によって相手の処理が壊れる(エラーになる)可能性をほとんどなくすことができ、依存性を安全に取り扱うことができます。
この考え方を「依存性の逆転」(英:Dependency Inversion)と言うのですが、今回のテーマであるDIと略称が被っており非常にややこしいです(笑)
これらを区別するため、DIP(Dependency Inversion Principle =依存性逆転の原則)という名称が用いられることもあります。
抽象的なインターフェース型に具体的なクラス型を注入する
いよいよDI(依存性注入)の登場です。
まずは、現在のソースコードを確認しましょう:
// モンスターの動き(ふるまい)を定義するインターフェース
public interface IMonster
{
string Name { get; set; } // モンスターのなまえ
void Attack(); // 攻撃する関数
}
public class Hogemon : IMonster // IMonsterを実装したクラス。ホゲットモンスター、略してホゲモン。
{
public string Name { get; set; } // ホゲモンのなまえ
public void Attack() // IMonsterを実装しているので、Attack関数を実装をしないとエラー
{
// 攻撃する処理...
}
}
public class Trainer // ホゲモンの飼い主 ホゲモントレーナーの情報を表すクラス
{
private IMonster myMonster; // 飼っているモンスター
// 自分の飼っているホゲモンに攻撃させる
public void OrderHogemonAttack()
{
// ゲームのテキストメッセージを出す。例:ピカチュウ!攻撃しろ!
Console.WriteLine($"{this.myMonster.Name}!攻撃しろ!")
// 以下、ゲームに必要な処理...
}
}
インターフェースを使って安全に依存性を扱えるようになっていますが、このままでは動作しません。
Trainerの飼っているIMonsterが具体的にはどんなモンスターなのか、知っている者が誰もいないのです。
myMonsterという変数に格納されるのは「IMonsterを実装したなにか」としか説明できないため、その正体は
public class Hogemon : IMonster
かもしれないし
public class Yokai : IMonster
かもしれません。
これではコンピュータはmyMonsterの中身をどう解釈すればよいのかわからず、処理できません。抽象化したい、というのはあくまでプログラマー(人間)の都合であり、コンピュータには具体的な答えが必要です。
ここで、IMonsterの中身をInject(注入)するクラスを外部に設置します。
// Trainerクラスのコンストラクタを呼び出し、IMonsterの中身を注入する
+ public class Injector
+ {
+ public void InjectDependency()
+ {
+ Trainer trainer = new Trainer(new Hogemon());
+ }
+ }
// public interface IMonster ... 略
// public class Hogemon : IMonster ... 略
public class Trainer // ホゲモンの飼い主 ホゲモントレーナーの情報を表すクラス
{
+ // コンストラクタを追加
+ public Trainer(IMonster monster)
+ {
+ //コンストラクタの引数で受け取ったIMonster型の何かをこのクラスのmyMonsterに代入
+ this.myMonster = monster;
+ }
private IMonster myMonster; // 飼っているモンスター
// 自分の飼っているホゲモンに攻撃させる
public void OrderHogemonAttack()
{
// ゲームのテキストメッセージを出す。例:ピカチュウ!攻撃しろ!
Console.WriteLine($"{this.myMonster.Name}!攻撃しろ!")
// 以下、ゲームに必要な処理...
}
}
Trainerクラスにコンストラクタを設置し、Trainerクラスは自分を生成した別のクラスからコンストラクタを介してmyMonsterの中身を手に入れるようにします。
InjectorクラスのInjectDependency()という関数は、アプリケーションのエントリーポイント(最初に実行するメソッド)の中で呼び出すようにし、プロジェクト全体の依存関係を最初に解決させます。
このように実装することで、myMonsterの中にHogemonクラスのインスタンスを代入しつつも、Trainerクラス自体はHogemonクラスに一切依存しない、という状況を作りだすことができます。
このように、依存関係(Dependency)を外部のクラスから注入(Inject)することが、DIという戦略なのです。
このようにコンストラクタを使った依存性注入を、コンストラクタ インジェクションといいます。
// Trainerクラスのコンストラクタを呼び出し、IMonsterの中身を注入する
public class Injector
{
public void InjectDependency()
{
Trainer trainer = new Trainer(new Hogemon());
}
}
Injectorクラスの中身は当然、このプロジェクト内の様々なクラス(HogemonやTrainer)に依存してしまいます。しかしこれは仕方のない、犠牲です。
誰かが依存関係を管理しなければならないのなら、それは一か所にまとめてしまおう。という戦略なのです。
依存性注入の目的
依存性注入(Dependency Injection)の目的は、依存性の逆転(Dependency Inversion)を使ったコードを動作させることです。依存性の逆転の目的は、プログラミングで必ず発生する依存性を安全に取り扱うことです。
おわりに
- 依存性とはなにか
- 依存性の注入とは具体的にどういうことか
この2点が少しでも伝われば幸いです。今回具体例として取り上げたのはコンストラクタインジェクションという手法ですが、実際にDIを実践する現場では「DIコンテナ」というツールが使用されることが多いです。
DIコンテナはDIの仕組みそのものをブラックボックス化して扱いやすくしたものです。この記事でDIの目的を理解できれば、DIコンテナの扱い方はすぐに納得して習得できるはずです。
個人開発でも集団開発でも、依存関係を分かりやすく安全にしておくことには価値があります。機会があれば、ぜひ試してみてください。