コンストラクタインジェクション
このレッスンでは、依存を受け取る3つの書き方を比べ、なぜコンストラクタインジェクションが推奨されるのかを説明できるようになります。
注入には3つの入口があります
Spring が依存を渡す経路は3種類あります。
Java
// 1. フィールドインジェクション
@Service
class OrderService {
@Autowired
private Sender sender;
}
// 2. セッターインジェクション
@Service
class OrderService {
private Sender sender;
@Autowired
void setSender(Sender sender) { this.sender = sender; }
}
// 3. コンストラクタインジェクション (推奨)
@Service
class OrderService {
private final Sender sender;
OrderService(Sender sender) { this.sender = sender; }
}3番目が推奨されます。しかも Spring Boot では、コンストラクタが1つしかないクラスであれば @Autowired を書く必要すらありません。理由を1つずつ見ていきます。
理由1 final にできます
コンストラクタで受け取った値だけが final フィールドに入れられます。フィールドインジェクションではリフレクションで後から値が差し込まれるため、final は付けられません。final が付くと、生成のあとで依存がすり替わらないことがコンパイラによって保証されます。マルチスレッドで共有される singleton の Bean にとって、これは地味に効きます。
理由2 必須の依存が引数として見えます
コンストラクタの引数リストは、そのクラスが動くために何が要るのかの一覧そのものです。引数が6個も7個もあるなら、そのクラスは責務を持ちすぎているという設計上の警告になります。フィールドインジェクションではフィールドが何個増えても引数リストは膨らまないので、この警告灯が点きません。
理由3 テストで素直に差し替えられます
コンストラクタがあれば、テストコードは Spring を起動せずに new OrderService(fakeSender) と書くだけで済みます。フィールドインジェクションだと、依存を差し込む手段がリフレクションかテスト用フレームワークの助けしかありません。「素の Java として組み立てられる」という性質が、そのままテストの書きやすさになります。
理由4 循環依存が起動時に分かります
A が B を必要とし、B も A を必要とする状態を循環依存と呼びます。コンストラクタインジェクションでは、どちらを先に作ることもできないので、アプリケーションの起動時点で失敗します。フィールドインジェクションだと空のインスタンスを先に作って後から差し込めてしまうため、起動は通り、実行時になって初めておかしくなります。壊れているなら早く壊れてほしいというのが設計判断の要点です。
| 書き方 | final | 必須依存が見える | Spring 無しでテスト | 循環依存の発覚 |
|---|---|---|---|---|
| フィールド | 不可 | 見えにくい | 面倒 | 実行時 |
| セッター | 不可 | 任意扱い | 可能 | 実行時 |
| コンストラクタ | 可 | 引数で明示 | 容易 | 起動時 |
セッターインジェクションが役に立つのは、依存が任意である場合や、あとから差し替える余地を意図的に残したい場合に限られます。
Lombok の @RequiredArgsConstructor
実務では final フィールドを並べて @RequiredArgsConstructor を付け、コンストラクタを自動生成させる書き方もよく見ます。生成されるのは普通のコンストラクタなので、上の利点はそのまま保たれます。
演習で書くこと
2つの実装を持つインターフェースを用意し、コンストラクタで受け取る Service を書きます。渡した実装によって結果が変わることを、返り値で確かめます。
要件
- DiscountPolicy インターフェースと NoneDiscount / RateDiscount の 2 実装を作ること
- PriceService は DiscountPolicy をコンストラクタで受け取り、final フィールドに保持すること
- run は policy 名と支払額を rate:900 の形で連結して返し、未知の policy では unknown を返すこと
入出力例
run("none", 1000) → "none:1000"
run("rate", 1000) → "rate:900"
run("rate", 2500) → "rate:2250"
run("cash", 1000) → "unknown"