インターフェースへの依存
このレッスンでは、実装クラスではなくインターフェースに依存させることで何が得られるのかを、説明できるようになります。
型に何を書くかで運命が決まります
前のレッスンで、依存を外から渡す形にしました。ただし、外から渡していても受け取る型が実装クラスのままだと、うまみは半分しかありません。
Java
// 受け取ってはいるが、型が実装クラス
class OrderService {
private final MailSender sender;
OrderService(MailSender sender) { this.sender = sender; }
}この OrderService に SmsSender を渡すことはできません。コンパイルが通らないからです。渡せるのは MailSender かそのサブクラスだけで、結局 OrderService はメール送信という具体的な手段に縛られたままです。
受け取る型をインターフェースにすると、話が変わります。
Java
interface Sender {
String send(String user);
}
class OrderService {
private final Sender sender;
OrderService(Sender sender) { this.sender = sender; }
}OrderService が知っているのは「send を持つ何か」という契約だけになりました。メールでも SMS でも、テスト用の何も送らない偽物でも渡せます。
依存の向きが逆転します
実装クラスに依存していたときは、上位の業務ロジックが下位の技術的詳細を名指ししていました。インターフェースを間に挟むと、この向きが変わります。
| 依存先 | 誰が誰を知っているか |
|---|---|
| 実装クラス | OrderService が MailSender を知っている |
| インターフェース | OrderService も MailSender も Sender だけを知っている |
実装クラスの側が「インターフェースに合わせにいく」立場になります。これを依存関係逆転の原則と呼びます。Spring のレイヤ分割で Repository がインターフェースとして定義されるのは、まさにこの理由です。業務ロジックを持つ Service が、データベースという技術的詳細を名指ししないで済みます。
Spring はどう解決しているのか
Spring のコンテナは、コンストラクタの引数の型を見て、その型に代入できる Bean を探します。
Java
@Service
class OrderService {
private final Sender sender;
OrderService(Sender sender) { this.sender = sender; }
}Sender を実装した Bean が1つだけなら、それが自動的に選ばれます。ここで実装が2つあると、コンテナはどちらを渡せばよいか決められず、起動時にエラーになります。この曖昧さを解く手段が2つあります。
- 片方に
@Primaryを付けて既定の実装だと宣言する - 注入する側で
@Qualifier("mailSender")のように名前で指定する
名前で指定する形は、演習で書く「名前から実装を引く」処理とほぼ同じことをしています。
インターフェースを常に作るべきか
実装が1つしかなく、当面増える見込みもないクラスにまでインターフェースを用意すると、ファイルが倍になるだけで得るものがありません。差し替える理由があるところ、つまり外部システムとの境界やデータベースアクセスに絞るのが現実的です。
演習で書くこと
用意済みの同じインターフェースの複数の実装から、名前で指定された実装を使って処理した結果を返します。Spring の @Qualifier が裏でやっている「名前から実装を選ぶ」判断を、自分で書くことになります。
演習で使うひな形
3つのクラスはどれも Formatter を実装しています。登録済みの beans から Formatter formatter = beans.get("upper"); と取り出し、new ReportService(formatter) に渡せます。サービスは具体的なクラス名を知らず、受け取った Formatter の format を呼びます。
要件
- beans.get(name) で Formatter として実装を取得すること
- 取得できなければ指定のエラーを返すこと
- 取得した実装を ReportService のコンストラクタに渡し、build の結果を返すこと