#はじめに
「Dependency Injection: 依存性の注入」 って、とっても分かりづらくないですか?
色んなところに解説記事があって、Qiitaでも検索すると沢山見つかりますが筆者は文章を読んだだけでは全然分かりませんでした。
「直接呼び出すのではなく、インターフェースを介して実装する?」「制御の反転(inversion of control)? 中から外ではなく外から中?」「サンプルを真似てみたけど複雑な実装している。これ何が嬉しいの?」そんな疑問ばかりでした。言葉は知っているけど使えないってやつです。
ようやく理解できたのは、実際に「あれ?困ったぞ」となって「ここでDIを使うと便利なのか!」と実感してからでした。
「ここで便利なんだ!」や「こういう時に使うといいんだ!」という使い所が分かると応用が効き、これが本当に理解することなんだなと思いましたので、それを書いてみたいと思います。
あくまで「使い所の例」ですので、そもそもの概念的なところは他の記事を参照されると良いかと思います。
例:
- 猿でも分かる! Dependency Injection: 依存性の注入
- 「なぜDI(依存性注入)が必要なのか?」についてGoogleが解説しているページを翻訳した
- 依存性の注入(Wikipedia)
なお、サンプルはC#で書いていますが、何か一つでもオブジェクト指向言語を学ばれている方でしたら問題なく理解できると思います。
DIを使わない例
天気に応じてメッセージを変える機能
それでは明日の天気に応じて出力するメッセージを変えるという機能を例にしていきたいと思います。素直に書くとこんな感じでしょうか。天気の情報の取得はWeatherService()
が担当しています。
/// <summary>
/// メッセージを担当するクラス
/// </summary>
public class Messenger
{
/// <summary>
/// メッセージを取得する
/// </summary>
/// <returns></returns>
public string GetMessage()
{
var service = new WeatherService();
var tomorrowWeather = service.GetTomorrowWeather();
switch (tomorrowWeather)
{
case "晴れ":
return "明日は良い天気です。頑張りましょう";
case "雨":
return "明日は雨みたいです。傘を忘れないように!";
case "曇り":
return "明日は曇りのようです。気をつけて!";
default:
throw new Exception("想定外エラー");
}
}
}
テストが出来ない
これのテストコードを書くとすると次のようになりますが、このままですとテストをするタイミングによって結果が変わってしまいます。明日が晴れの時にはテストは成功しますが、雨の時にはテストは失敗します。実行するタイミングで結果が変わるなんて嫌ですね。
[TestClass]
public class MessengerTest
{
[TestMethod]
public void GetMessage_晴れの時のテスト()
{
// Arrange
var messenger = new Messenger();
// Act
var msg = messenger.GetMessage();
// Assert
Assert.AreEqual("明日は良い天気です。頑張りましょう", msg);
}
}
また、もし仮にタイミングを合わせられたとしても、GetTomorrowWeather()
が出来ていないとテストができません。一人で作っているなら「GetTomorrowWeather()
の作成から優先しよう」で解決できますが、大抵の開発プロジェクトは複数人で作っているわけで、「GetTomorrowWeather()
の担当者が早くしてくれないと、Messenger()
担当者の仕事が進まないんだけど…」という状態になるのは宜しくありません。
public class WeatherService
{
public string GetTomorrowWeather()
{
return null;
}
}
テストが出来ないのは依存しているから
何故テストできないのか。
それはGetMessage()
が、WeatherService()
に依存しているからです。こいつがいなかったらテストできるのです。「あーあ、いなくならないかな、WeatherService()
のやつ」と思いますよね。それを実現する手法が「Dependency Injection: 依存性の注入」になります。
DIを使う例
それではWeatherService()
にいなくなってもらいます。依存性の解消です。
もちろん完全にいなくなられると困りますから、直接WeatherService()
を呼び出すのではなくインターフェースを通じて結びつけます。
public string GetMessage()
{
var service = new WeatherService(); // ←ここが依存している箇所
// 中略
}
インターフェースを作成する
WeatherService()
用のインターフェースを作って継承しましょう。
public interface IWeatherService
{
string GetTomorrowWeather();
}
public class WeatherService: IWeatherService
{
public string GetTomorrowWeather()
{
return null;
}
}
コンストラクタで外から注入できるようにする
次にこれを使用するMessenger()
クラスのコンストラクタにて、外からIWeatherService
を実体化したクラスを「注入」できるようにします。そしてそれをGetMessage()
で使うように変更します。
/// <summary>
/// メッセージを担当するクラス
/// </summary>
public class Messenger
{
IWeatherService _weatherService;
/// <summary>
/// コンストラクタ。
/// Messengerを呼び出すのと同時に、WeatherServiceを「注入」する
/// </summary>
/// <param name="weatherService"></param>
public Messenger(IWeatherService weatherService)
{
// ここで外から実体化したクラスを受け取る
_weatherService = weatherService;
}
/// <summary>
/// メッセージを取得する
/// </summary>
/// <returns></returns>
public string GetMessage()
{
// コンストラクタで実体化した機能を使用する
var tomorrowWeather = _weatherService.GetTomorrowWeather();
// 中略
}
}
これでクラス同士が直接の結びつきがなくなり、インターフェースを通しての関係性になりました。もうどこにもWeatherService()
クラス自体は出てきていません。いなくなってもらいました。
テスト用の偽物(Mock)クラスを作る
これでテストが出来るようになります。テストに必要な「偽物(Mock)」を作って外から注入してあげればいいのです。たとえば常に晴れが返ってくるSunnyWeatherService()
クラスを作ってみます。もちろんこのクラスは本番のプロジェクトではなく、テストプロジェクト内で作成します。
/// <summary>
/// 常に晴れが返ってくるWeatherService
/// </summary>
public class SunnyWeatherService: IWeatherService
{
public string GetTomorrowWeather()
{
return "晴れ";
}
}
偽物(Mock)を外から注入する
先ほど作った「偽物(Mock)」クラスを外から注入します。すると、このテストはどんな時でも成功するようになります。
[TestMethod]
public void GetMessage_晴れの時のテスト()
{
// Arrange
var sunny = new SunnyWeatherService(); // 常に晴れが返ってくるMock
var messenger = new Messenger(sunny);
// Act
var msg = messenger.GetMessage();
// Assert
Assert.AreEqual("明日は良い天気です。頑張りましょう", msg);
}
あとは曇りや雨の場合の時のテストも同様に「偽物」のWeatherService()
を作成すればテストが可能になります。
ここで「そういう偽物(Mock)を使ってテストになるのか」と思うかもしれませんが、ここではあくまでMessenger()
クラスのGetMessage()
のテストをしたいだけであり、このメソッドの役割は天気によってメッセージを作り出しているだけですので、これで十分テストになります。GetTomorrowWeather()
が信頼が出来るかどうかは、GetTomorrowWeather()
のテストで判断すればいいからです。
外から機能を注入する
実際に使用する場合もWeatherService()
をMessenger()
に注入して使用します。Dependency Injectionの日本語訳は「依存性の注入」ですが1 、個人的には「機能の注入」といった方がしっくりきます。メソッドの中ではインターフェースだけで実装しておいて、実際の機能を外から注入するというイメージです。
static void Main(string[] args)
{
var weatherService = new WeatherService();
var messenger = new Messenger(weatherService); //呼び出す時に機能を注入
Console.WriteLine(messenger.GetMessage());
Console.Read();
}
かつて疑問符だらけだったと冒頭で紹介した「制御の反転(inversion of control)」ですが、確かにこうしてみると、GetMessage()
から呼び出していたWeatherService()
が、外から中へという構造になって「反転」しています。
テスト以外のお役立ち例
「それじゃあ、テストをしない場合は意味ないのか」と思われるかもしれませんが、それだけではありません。(テストはあった方がいいと思いますが)
「依存していない」ということは他のコードの変更の影響を受けにくいということになります。
たとえば気象情報を取ってくるサービスを任意で変えたいとします。今までは気象庁から取ってきていましたが、GoogleさんだったりYahooさんだったり、色んなところから取得できるようにしたいというパターンです。
そういう時はIWeatherService()
を継承してそれぞれのサービスを作成します。機能が増えた形ですが、GetMessage()
のコードを変える必要はありません。
public class GoogleWeatherService: IWeatherService
{
public string GetTomorrowWeather()
{
// Googleから情報を取ってくる機能
}
}
public class YahooWeatherService : IWeatherService
{
public string GetTomorrowWeather()
{
// Yahooから情報を取ってくる機能
}
}
public class JMAWeatherService : IWeatherService
{
public string GetTomorrowWeather()
{
// 気象庁から情報を取ってくる機能
}
}
呼び出す方で、どのサービスを使うかを決定してあげる必要がありますが、GetMessage()
の変更は必要ありません。
static void Main(string[] args)
{
IWeatherService weatherService;
// 本事例では引数でどのWeatherServiceを使うかを決定
switch (args[0])
{
case "google":
weatherService = new GoogleWeatherService();
break;
case "yahoo":
weatherService = new YahooWeatherService();
break;
case "jma":
weatherService = new JMAWeatherService();
break;
default:
throw new Exception("想定外エラー");
}
var messenger = new Messenger(weatherService); //呼び出す時に機能を注入
Console.WriteLine(messenger.GetMessage());
Console.Read();
}
もし一番最初のコードのように直接参照していたら、次のコードのように呼び出す方とGetMessage()
の両方を変える必要があるため、影響が大きくなってしまいます。
/// <summary>
/// メッセージを取得する
/// </summary>
/// <returns></returns>
public string GetMessage(string selectedServiceName)
{
var tomorrowWeather = "";
switch (selectedServiceName)
{
case "google":
var googleService = new GoogleWeatherService();
tomorrowWeather = googleService.GetTomorrowWeather();
break;
case "yahoo":
var yahooService = new YahooWeatherService();
tomorrowWeather = yahooService.GetTomorrowWeather();
break;
case "jma":
var jmaService = new JMAWeatherService();
tomorrowWeather = jmaService.GetTomorrowWeather();
break;
default:
throw new Exception("想定外エラー");
}
switch (tomorrowWeather)
{
case "晴れ":
return "明日は良い天気です。頑張りましょう";
case "雨":
return "明日は雨みたいです。傘を忘れないように!";
case "曇り":
return "明日は曇りのようです。気をつけて!";
default:
throw new Exception("想定外エラー");
}
}
機能を増やしたりといった他のコードの変更を受けにくい。つまり変化に強い。これがDIの特徴の一つになります。
まとめ
- DIの理解は難しいけれど、「役に立つ」事例があると理解が進むのでそれを書いてみました。参考になれば幸いです。
- 依存性の注入。個人的には「機能の注入」の方がしっくりきます。
- 外から「機能を注入」するので、テストがしやすくなったり、変化に強い構造になります。
-
割と色んなところで訳語が分かりにくいって論争を見かけます ↩