Java : 依存性の注入(DI)をもっと気軽に
依存性の注入(DI) は、クラス間の依存を弱くするための設計パターンです。
本記事では、あまり本格的な DI ではなく、できるところから気軽に実践していけるような解説ができたらと思います。
概要
依存性の注入は DI と略します。
Dependency (依存性) Injection (注入) の頭文字ですね。
DI はクラス間の依存を弱くするために使われる設計パターンです。
依存性…と聞くと抽象的で少し分かりにくいかもしれません。
具体的には、依存性とは オブジェクト です。
DI とは、あるクラスに対して、依存性(オブジェクト)を 外側から 注入することをいいます。
Wikipedia には
- コンポーネント間の依存関係をプログラムのソースコードから排除
とあり、けっこう厳格な説明がされています。
本記事では、そこまで本格的なものではなく、ライブラリなども使わずに、
- できるところから
- 気軽に
- 手軽に
実践できるような解説をしていけたらと思っています。
ポイントは
- new は 強い依存
- 依存性の注入(DI)は 弱い依存
です。
コード例
それでは、依存性の注入をする例/しない例を、実際のコードで見ていきましょう。
2つのクラスを用意します。
Clientクラスは、Serviceクラスに 依存 しています。
Service クラスは、getWeek メソッドで現在の曜日(DayOfWeek)を返します。
class Service {
DayOfWeek getWeek() {
...
}
}
Client クラスは、getMessage メソッドで Service から取得した曜日をもとに
- 土曜日、日曜日 … "今日は休日!"
- それ以外 … "今日は平日!"
という文字列を返します。
class Client {
String getMessage() {
...
}
}
依存性の注入なし
まずはDIなしの例です。
class Service {
DayOfWeek getWeek() {
return LocalDate.now().getDayOfWeek();
}
}
Service の getWeek メソッドでは、LocalDate を使って現在の曜日を返します。
LocalDateの簡単な使用例です。
// 2022年5月19日(木曜日) に実行した例です。
final var now = LocalDate.now();
System.out.println(now); // 2022-05-19
final var week = now.getDayOfWeek();
System.out.println(week); // THURSDAY
次に Client です。
class Client {
// ★ポイント!
private final Service service = new Service();
String getMessage() {
final var week = service.getWeek();
return switch (week) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "今日は平日!";
case SATURDAY, SUNDAY -> "今日は休日!";
};
}
}
Client では、Service.getWeek で現在の曜日を取得します。
そして、switch式 で条件分岐させ、平日用と休日用のメッセージを作成して返します。
final var client = new Client();
final var message = client.getMessage();
System.out.println(message); // 今日は平日!
平日に Client を使った例です。
期待どおりに、"今日は平日!" という文字列を取得することができました。
DIなしのポイントは、Client のフィールドです。
class Client {
// ★ポイント!
private final Service service = new Service();
...
Client 自身が Service を new していますね。
つまり、依存性(Serviceオブジェクト)は Client の 内部で 作成されました。
依存性の注入あり
次にDIありの例です。
Service クラスはDIなしと同じです。
class Service {
DayOfWeek getWeek() {
return LocalDate.now().getDayOfWeek();
}
}
次に Client です。
class Client {
private final Service service;
// ★ポイント!
Client(Service service) {
this.service = service;
}
String getMessage() {
final var week = service.getWeek();
return switch (week) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "今日は平日!";
case SATURDAY, SUNDAY -> "今日は休日!";
};
}
}
コンストラクタで Service オブジェクトを受け取るように変わりました。
つまり、依存性(Serviceオブジェクト) を Client の 外部から 注入します。
final var client = new Client(new Service());
final var message = client.getMessage();
System.out.println(message); // 今日は平日!
実際に Client を使った例になります。
結果は、DIなしと同じですね。
メリット
DIによるメリットは、クラス間の依存が弱くなることです。
依存が弱くなる…といっても、なかなかメリットとして感じにくいかもしれません。
個人的には、一番メリットを感じるのはユニットテストを作るときです。
ユニットテストしやすい
先ほどの例をもとにユニットテストを作成してみましょう。
平日(月曜日~金曜日)に Client.getMessage が "今日は平日!" を返すかテストしてみます。
まずは DIなし の例です。
final var client = new Client();
final var actual = client.getMessage();
System.out.println("今日は平日!".equals(actual) ? "OK" : "NG");
このテストは、現在の日付が平日であるときに実行すれば OK が表示されます。
しかし、休日に実行したら NG が表示されます。
ユニットテストが現在時刻に依存するのはよくありませんね。
テスト毎にパソコン本体の現在時刻を変更する、という荒業があるかもしれませんが、それもおすすめはしません。
現在時刻に依存しないようにするにはどうすればよいのか…
DIなしだと難しいです。
次に、DIあり の例です。
Service が返す曜日を、現在の曜日ではなく、固定 で月曜日にしています。
final var dummy = new Service() {
@Override
DayOfWeek getWeek() {
// 月曜日
return DayOfWeek.MONDAY;
}
};
final var client = new Client(dummy);
final var actual = client.getMessage();
System.out.println("今日は平日!".equals(actual) ? "OK" : "NG");
// 結果
// ↓
// OK
DIありの設計をしているおかけで、Serviceのダミーを簡単に注入できます。
上記の例では、月曜日を固定で返すようにしていますが、休日である土曜日や日曜日も簡単にテストできます。
もちろん、テストは現在時刻に依存していません。いつ実行してもテストは OK となります。
日曜日(休日)をテストする例です。
final var dummy = new Service() {
@Override
DayOfWeek getWeek() {
// 日曜日
return DayOfWeek.SUNDAY;
}
};
final var client = new Client(dummy);
final var actual = client.getMessage();
System.out.println("今日は休日!".equals(actual) ? "OK" : "NG");
// 結果
// ↓
// OK
デメリット
依存性の注入のデメリットとしては、依存性(オブジェクト)を注入するための記述が面倒になることがあります。
先ほどの例の、DIなし/ありの Client 生成を比べてみましょう。
// DIなし
final var client = new Client();
// DIあり
final var client = new Client(new Service());
DIありのほうが、毎回 Service も new しなければならないため少し面倒です。
今回の例では、注入するのは Service が1つだけなのでまだよいですが、これが多くなってくると面倒ですよね。
例えば、3つのサービスを注入すると次のようになります。
final var client = new Client(new ServiceA(), new ServiceB(), new ServiceC());
そんなときは、new の代わりに、インスタンス生成用のクラスを別に作ってみてもよいかもしれません。
class ClientFactory {
static Client create() {
return new Client(new ServiceA(), new ServiceB(), new ServiceC());
}
}
final var client = ClientFactory.create();
新しいクラス(ClientFactory)を作るのが面倒であれば、Clientクラスにインスタンス生成用の static メソッド追加でもよいと思います。
class Client {
...
static Client create() {
return new Client(new ServiceA(), new ServiceB(), new ServiceC());
}
}
final var client = Client.create();
Clientを使う側では、new の代わりに create メソッドでインスタンスを生成します。
これなら、そんなに面倒さは感じないと思います。
それさえも面倒になってきたら、いよいよ DIコンテナ と呼ばれる依存性の注入のためのライブラリを導入するのもよいかもしれません。
new は強い依存を生む
さて、DIのメリットとして、ユニットテストがしやすくなることを説明しました。
しかし、なぜこのようなことが起こるのでしょうか?
それは、Java の new 演算子は 強い依存 を生むためです。
new 演算子は、具体的な クラスを指定する必要があります。
つまり、動的に置き換えられません。
言い換えると、多態性(ポリモーフィズム) の恩恵が得られない、ということです。
DIなしの例では、Client 内部で Service を new したので、Serviceのサブクラスに置き換えることができませんでした。
class Service {
...
}
class Client {
// ★ポイント!
private final Service service = new Service();
...
}
しかし、DIありの例では、Client では new せずに、Serviceオブジェクトを受け取るだけです。
それが Service であろうが、そのサブクラスの DummyService であろうが問題ありません。
class Service {
...
}
class Client {
private final Service service;
// ★ポイント!
Client(Service service) {
this.service = service;
}
...
}
new 演算子は強い依存。
DI は弱い依存。
なんとなくイメージできましたでしょうか。
気軽に DI してみよう
DIを厳密に実装しようとすると、
- インタフェースと実装をちゃんと分けて設計
- サードパーティ製のライブラリが必要
などなど、少し大変かもしれません。
また、すべてのクラスを DI に対応させよう!というのもやりすぎ感があります。
DIの目的は、
- クラス間の 依存を弱く する
です。
そこさえ意識できれば、本記事でご紹介してきた簡易的な DI でも、十分にメリットを受けることができます。
まずは
- このクラスはユニットテストしづらいな…
と感じるところを、少しずつ手動で実践してみるのがよいのかなと思います。
補足
DIの手法
DIの手法にはいくつか種類があります。
本記事でご紹介した DI は、コンストラクタ注入と呼ばれる手法です。
他にも
- インタフェース注入
- setter注入
というものもあります。
気になる方は、「依存性の注入 - Wikipedia」などご参照ください。
DIに否定的な意見
DIに否定的な意見もあります。
なかなか興味深い議論なので読んでみることをおすすめします。
まとめ
- new によるオブジェクト生成は 強い依存 を生みます。
- DIは 弱い依存 です。
- DIで依存を弱くすると、特にユニットテストがしやすくなるのがメリットです。
- 大きなデメリットはないので気軽に実践してみましょう。
関連記事
- 標準APIにならう命名規則
- コメントが少なくて済むコードを目指そう
- シングルトン・パターンの乱用はやめよう
- メソッドのパラメータ(引数)は使う側でチェックしよう
- 不変オブジェクト(イミュータブル) とは
- 不要になったコードはコメントアウトで残さずに削除しよう
- 簡易的な Builder パターン
- 読み取り専用(const) のインタフェースを作る
- 図解/デザインパターン一覧 (GoF)
- Abstract Factory パターン
- Adapter パターン
- Bridge パターン
- Builder パターン
- Chain of Responsibility パターン
- Command パターン
- Composite パターン
- Decorator パターン
- Facade パターン
- Factory Method パターン
- Flyweight パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- Prototype パターン
- Proxy パターン
- Singleton パターン
- State パターン
- Strategy パターン
- Template Method パターン
- Visitor パターン