インターフェースへの依存
このレッスンでは、実装クラスではなくインターフェースに依存させることで何が得られるのかを、説明できるようになります。
型に何を書くかで運命が決まります
前のレッスンで、依存を外から渡す形にしました。ただし、外から渡していても受け取る型が実装クラスのままだと、うまみは半分しかありません。
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 が裏でやっている「名前から実装を選ぶ」判断を、自分で書くことになります。
要件
- Formatter インターフェースと upper / lower / trim の 3 実装を作ること
- 名前と実装の対応を Map に持ち、run はそこから引いた実装を ReportService に渡すこと
- 未登録の名前のときは no bean: xxx の形の文字列を返すこと
入出力例
run("upper", "spring boot") → "SPRING BOOT"
run("lower", "Spring Boot") → "spring boot"
run("trim", " hello ") → "hello"
run("kebab", "hello") → "no bean: kebab"