依存性注入(Dependency Injection:DI)。コードの保守性やテスト性を向上させる、強力なデザインパターンです。
よく知られる便利なテクニック。しかしこの中身、難しい説明がとても多いです。
ここでは、ごく簡単なサンプルを題材にして、依存性注入が持つ本質を、体感してみましょう。
まずは『依存性注入』のポイントを押さえよう
依存性注入は、ソフトウェア設計におけるデザインパターンの一つです。
ソフトウェアの依存関係における問題を解決する、方針のひとつです。
まず誤解なきように言うと、
依存性注入は
- コーディングをラクにするためのものじゃ、ありません
どちらかといえば、
依存性注入は
- テストをラクにするもの
- メンテナンスをラクにするもの
なので、コーディング自体はちょっと大変になります。でもテストはすごくラクになる。
どういうことなのか?実例で見てみましょう。
『依存性注入』を導入する過程を追う
新人くんの作る、簡単Javaプログラムを例題にします。
プログラムに、依存性注入を導入するまでの過程を、見ていきましょう。
テストをどうしよう?
新人くん。Javaで、かんたんカレンダークラスをコーディングしています。
MyCalenderクラス作りました!
現在の日付をプリントする、シンプルなカレンダークラスです。
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class MyCalender {
// 現在の日付を表示する
public void printCurrentDate() {
// 現在の日時を取得
LocalDateTime now = LocalDateTime.now();
// フォーマットに従って出力
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
String formattedDate = now.format(formatter);
System.out.println(formattedDate);
}
}
先輩が声をかけてきました。
ようし、それじゃこのクラスをテストするぞ!
新人くん、MyCalenderクラスを使ったアプリを作って、テストします。
はい、今日の日付が表示できましたー♪
public class Main {
public static void main(String[] args) {
// MyCalenderクラスのインスタンスを作成
MyCalender myCalender = new MyCalender();
// 現在の日付を表示
myCalender.printCurrentDate();
}
}
2024/11/03
先輩はさらに、こんなことを言ってきます。
ようし、それじゃ次は、表示パターンの確認だ!
2028年2月29日、の表示を確認して
……え?
そんなこと言ったって、このMyCalenderクラス、今日の日付しか表示することができません。
どうしようかな?
依存部分を分離する
表示パターンを確認するために、日付を取得している部分、ここを何とかしよう。
日付を取得するメソッドを分けようかな…
現在日時を取得するところを、getCurrentDate
メソッドで分離します。
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class MyCalender {
// 現在の日付を表示する
public void printCurrentDate() {
// 現在の日時を取得
LocalDateTime now = getCurrentDate();
// フォーマットに従って出力
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
String formattedDate = now.format(formatter);
System.out.println(formattedDate);
}
// 現在日付を取得する
private LocalDateTime getCurrentDate() {
return LocalDateTime.now();
}
}
で、このメソッドをちょっと直して、日付を変えれば……
// 現在日付を取得する
private LocalDateTime getCurrentDate() {
return LocalDateTime.of(2028, 2, 29, 0, 0, 0); // 2028年2月29日
}
No-!
テスト中のクラスを編集するなんてダメだー!
そうですね。テスト中に、テスト対象のソースを書き換えちゃいけません。
依存部分を別クラスに分離する
新人くん、別の手を考えます。
じゃあ、日付を取るところを、別のクラスにすればいいかな?
日付を取得するための、DateProvider
クラスを新たに作ります。
import java.time.LocalDateTime;
public class DateProvider {
// 現在日付を取得する
public LocalDateTime getCurrentDate() {
return LocalDateTime.now();
}
}
MyCalender
クラスは、DateProvider
クラスを使って、日時を取得するようにします。
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class MyCalender {
// 現在の日付を表示する
public void printCurrentDate() {
// 現在の日時を取得
DateProvider provider = new DateProvider();
LocalDateTime now = provider.getCurrentDate();
// フォーマットに従って出力
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
String formattedDate = now.format(formatter);
System.out.println(formattedDate);
}
}
そして、DateProviderクラスを書き換えて、テスト用の日時を返してあげればOK。
import java.time.LocalDateTime;
public class DateProvider {
// 現在日付を取得する
public LocalDateTime getCurrentDate() {
// テスト用
return LocalDateTime.of(2028, 2, 29, 0, 0, 0); // 2028年2月29日
}
}
2028/02/29
これなら、MyCalenderクラス本体はいじらなくても、テストできます。
よし、表示確認できたぞ
OK、じゃあ次は、
1月1日、12月31日、も確認して!
ええー…………
表示パターンが増えました。まあ、パターンを色々確認するもの、大事ではありますね。
ただ、そのためには、こんな作業が必要になります。
- DateProviderクラスを、1月1日を返すように改造
- コンパイル
- できたら実行
- DateProviderクラスを、12月31日を返すように改造
- コンパイル
- できたら実行
毎回直してコンパイルして…と、いかにも面倒。
これからテストケースがどんどん増えていったら、手間がかかってしょうがない。
依存部分を外から設定できるようにする
ここで、先輩からのアドバイス。
日付のオブジェクトを、外から設定できるようにするといいぞ!
これをすると、コンパイルすることなしに、日時を差し替えて動かせるようになる、らしい。
新人くん、MyCalenderクラスを、さらに改造します。
コンストラクタを追加するといい、って言ってたな…
MyCalender
クラスのコンストラクタで、DateProvider
オブジェクトを入力するようにします。
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class MyCalender {
private DateProvider provider;
// コンストラクタ
MyCalender(DateProvider provider) {
this.provider = provider;
}
// 現在の日付を表示する
public void printCurrentDate() {
// 現在の日時を取得
LocalDateTime now = provider.getCurrentDate();
// フォーマットに従って出力
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
String formattedDate = now.format(formatter);
System.out.println(formattedDate);
}
}
DateProvider
クラスは、テスト用に改造しておきます。任意の日付を設定できるようにしておきましょう。
import java.time.LocalDateTime;
public class DateProvider {
public LocalDateTime dateTime;
// 現在日時を取得する
public LocalDateTime getCurrentDate() {
// テスト用データを返却
return dateTime;
}
}
そしてテストコードは、こう。
import java.time.LocalDateTime;
public class Main {
public static void main(String[] args) {
// 2025年2月29日
DateProvider provider1 = new DateProvider();
provider1.dateTime = LocalDateTime.of(2028, 2, 29, 0, 0, 0);
MyCalender calender1 = new MyCalender(provider1);
calender1.printCurrentDate();
// 2025年1月1日
DateProvider provider2 = new DateProvider();
provider2.dateTime = LocalDateTime.of(2028, 1, 1, 0, 0, 0);
MyCalender calender2 = new MyCalender(provider2);
calender2.printCurrentDate();
// 2025年12月31日
DateProvider provider3 = new DateProvider();
provider3.dateTime = LocalDateTime.of(2028, 12, 31, 0, 0, 0);
MyCalender calender3 = new MyCalender(provider3);
calender3.printCurrentDate();
}
}
こうすると、たくさんある日付確認のテストを、一気に流せます。
おおー、いろいろなパターンを、まとめてテストできるようになった!
依存部分をインターフェースにする
テストがだいぶラクになってきました。
でも、まだ手間な部分が残ってます。DateProvider
クラスです。
DateProvider
クラス。実際の日付を返す、この本物と
import java.time.LocalDateTime;
public class DateProvider {
// 現在日付を取得する
public LocalDateTime getCurrentDate() {
return LocalDateTime.now();
}
}
テスト用に準備した、このダミー。
import java.time.LocalDateTime;
public class DateProvider {
public LocalDateTime dateTime;
// 現在日付を取得する
public LocalDateTime getCurrentDate() {
// テスト用データを返却
return dateTime;
}
}
テストの度に、本物とダミーを差し替えるのも、面倒だよね…
ここでまた、先輩からのアドバイス。
日付オブジェクトのインターフェースを作るといいぞ!
DateProvider
を、本物とダミー共通の定義を持つ、インターフェースにします。
新人くん、先輩からの教えに従い、インターフェースを作ります。
インターフェースって、こんなだっけ?
import java.time.LocalDateTime;
public interface DateProvider {
// 現在日付を取得する
public LocalDateTime getCurrentTime();
}
そして、このインターフェースを持つ、本物クラスとダミークラスを作ります。
import java.time.LocalDateTime;
public class DateProviderImpl implements DateProvider {
// 現在日時を取得する
public LocalDateTime getCurrentTime() {
return LocalDateTime.now();
}
}
import java.time.LocalDateTime;
public class DateProviderTest implements DateProvider {
public LocalDateTime dateTime;
// 現在日時を取得する(ダミー)
public LocalDateTime getCurrentTime() {
return dateTime;
}
}
すると、テストコードは、こうなります。
import java.time.LocalDateTime;
public class Main {
public static void main(String[] args) {
// 現在日時
DateProviderImpl provider = new DateProviderImpl(); // 本物
MyCalender calender = new MyCalender(provider);
calender.printCurrentDate();
// 2025年2月29日
DateProviderTest provider1 = new DateProviderTest(); // テスト用
provider1.dateTime = LocalDateTime.of(2028, 2, 29, 0, 0, 0);
MyCalender calender1 = new MyCalender(provider1);
calender1.printCurrentDate();
// 2025年1月1日
DateProviderTest provider2 = new DateProviderTest(); // テスト用
provider2.dateTime = LocalDateTime.of(2028, 1, 1, 0, 0, 0);
MyCalender calender2 = new MyCalender(provider2);
calender2.printCurrentDate();
// 2025年12月31日
DateProviderTest provider3 = new DateProviderTest(); // テスト用
provider3.dateTime = LocalDateTime.of(2028, 12, 31, 0, 0, 0);
MyCalender calender3 = new MyCalender(provider3);
calender3.printCurrentDate();
}
}
これでなんと、
- 本物のクラスを一切いじらずに
- いろいろな表示パターンのテストができます
すごい!
いちいちコードを書き換えなくても、テストが全部できるぞ!
コードを書き換えるという行為は、それだけでプログラムが動かなくなったりする危険があります。
なので、コードを書き換えることなく、色々なパターンのテストができるのは、とても便利なのです。
「依存」するオブジェクトを、外部から「注入」する
ここまでを復習しましょう。
- MyCalenderクラスは、日付を取得するのに、DateProviderクラスを使っています
- それはすなわち、DataProviderがいないと困ってしまう
これを
といいます。
MyCalenderクラスは、DateProviderオブジェクトを、なんとか準備しないといけないのですが、
- MyCalenderクラスの中でDateProviderオブジェクトを作るのではなく
- 外部でDateProviderオブジェクトを作り、コンストラクタなどで入力する
このイメージが
という表現になります。
自分が「依存しているオブジェクト」を、外部から「注入」するのだ!
これが、「依存性を注入する」というテクニックの考え方です。
コンストラクタにより依存性注入する手法を「コンストラクタインジェクション」と呼びます。他にも、メソッドで注入する「セッターインジェクション」などもあります。ただ大抵の場面では、コンストラクタインジェクションのほうが推奨です。
テストがラクになる『依存性注入』
依存性注入とは、ごく簡単にいうと、こういうテクニック。
先ほどの例でメリットはありそうですね。でも確実に、コードは複雑になります。
そこまでして、「依存性注入」を導入する価値は、どこにあるのでしょうか?
依存性注入の使いどころ
依存性注入を使うと、テストプログラムの動作中に、オブジェクトをいろいろ差し替えできます。
特に効果が高いのは、その時の状況によって結果が変わるオブジェクトに依存する場合です。
- 外部データ(データベース、ファイル)
- 外部からの入力(ネットワーク通信)
- 日付や時刻
- ランダムデータ
こういうクラス、テスト時に本物を使うと、特定の状況を再現するのが、とても難しい。
これを、外から挿入できる仕掛けにしておくと、テスト時に任意に差し替えができて、便利です。
ユニットテストと相性抜群
依存性注入のこの仕掛けは、ユニットテストと相性抜群です。
ユニットテストでは、あらかじめ仕掛けたテストコードを、まとめて流します。
このとき、依存性注入の仕掛けで、任意にスタブ差し替えができるようにしておきます。
すると、テストプログラムの中で、色々なパターンを一度に流すことができるのです。
いつでもすぐにテストを流せるのは、安心だよね!
フレームワークが作れる『依存性注入』
依存性注入が活躍する、もうひとつの大きな舞台。それがフレームワークです。
修正や再コンパイルをすることなく、依存オブジェクトを差し替えられる。
この特性が、フレームワークの実装と相性がよいのです。
基本的なフレームワークの仕掛け
「フレームワーク」とは、プログラム開発において、よく利用される機能をあらかじめ備えた骨組みのことです。
つまるところ、あらかじめ作られたプログラム部品の集合。似た概念に「ライブラリ」がありますが、両者にはこのような違いがあります。
- ライブラリ ユーザープログラムが「ライブラリ」を呼び出す
- フレームワーク 「フレームワーク」がユーザープログラムを呼び出す
フレームワークには、GUIフレームワーク、Webフレームワークといったものがあり、だいたいこういう構造になっています。
- フレームワーク なんらかのイベントで、ユーザープログラムを呼び出す
- ユーザープログラム イベントに対応した独自処理を実装する
ここで依存性注入を使うと、フレームワークを再コンパイルすることなく、ユーザープログラムを差し替えられるわけです。
依存性注入を採用するフレームワーク
いまでは、世の中の多くのフレームワークが、依存性注入(DI)を採用しています。
- Spring Framework Javaで圧倒的人気を誇るフレームワーク
- ASP.NET Core Micosoft .NET Core向けのWebフレームワーク
- Laravel PHPの人気Webフレームワーク
- Django Pythonの代表的なWebフレームワーク
これらはDIフレームワークと呼ばれたりもします。「DIコンテナ」「サービスコンテナ」といった仕掛けでDIを実現しています。
具体的な実装方法は、フレームワークによって様々。ただ依存性注入の本質は変わりません。
それぞれのフレームワークの流儀に沿って、DIを実現するのだ!
依存性注入の本質を念頭に置いて、フレームワークを使いましょう。
依存性注入が関係する原則
この依存性注入は、SOLID原則とよばれるソフトウェア設計原則と、密接な繋がりがあります。
特に関連するのが、「依存性逆転原則」。
上位モジュールが下位モジュールに依存する、という関係。これを逆転させるテクニックが「依存性注入」なのです。
この原則を理解すると、依存性注入の役割がよく分かるようになるでしょう。
まとめ
ここでは、依存性注入を理解するための、はじめの一歩を説明しました。
依存性注入(Dependency Injection:DI)の世界は奥深いです。
例ではJavaを取り上げましたが、言語やフレームワークによって、実現方法は様々。
でも本質の考え方はみんな同じ。原則を忘れないようにして、そのメリットを活かしましょう。
依存性注入を覚えると、テストやメンテナンスが格段にラクになるぞ!
コメント