newで書くと何が困るのか
このレッスンでは、クラスの中で直接 new を書いてしまうと何が困るのかを、自分の手で確かめられるようになります。
まずは素朴に書いてみます
注文が確定したときに、ユーザーへ通知を送る処理を考えます。素直に書くと次のようになります。
Java
class MailSender {
String send(String to, String body) {
return "mail to " + to + " / " + body;
}
}
class OrderService {
private final MailSender sender = new MailSender(); // ここが問題
String complete(String user) {
return sender.send(user, "order completed");
}
}動きます。動きますが、この OrderService はもう MailSender から離れられません。
困りごとは3つあります
1つめは差し替えができないことです。 「メールではなく SMS で送りたい」という要望が来たとき、OrderService のソースコードを開いて new MailSender() を new SmsSender() に書き換えるしかありません。呼び出し側がどれだけ増えても、切り替えの主導権は OrderService の内部にあります。
2つめはテストが書けないことです。 complete をテストしたいだけなのに、実行すると本物の MailSender が動きます。もし本当にメールを送る実装だったら、テストを走らせるたびにメールが飛びます。テスト用の偽物に差し替えたくても、OrderService の中で new しているので外から手が出せません。
3つめは組み立ての知識が散らばることです。 MailSender が SMTP のホスト名を必要とするようになったら、OrderService がその設定値を知らなければなりません。本来 OrderService の関心事ではない設定が、業務ロジックのクラスに染み出してきます。
誰が誰を作るのか、という一点
3つの困りごとは、すべて同じ原因から来ています。使う側が、使われる側を自分で作っているという点です。
| 書き方 | 依存の向き | 差し替え |
|---|---|---|
| 中で new する | 使う側が実装クラスを名指しする | 不可 |
| 外から受け取る | 使う側は型だけを知る | 可能 |
作る責任を外に追い出せば、OrderService は「送れる何か」を受け取って使うだけの存在になります。この「作る責任を外に出す」考え方が、次のレッスンで扱う依存性注入 (Dependency Injection) です。
Spring は何をしてくれるのか
Spring では、この「外から渡す」役を DI コンテナが引き受けます。
Java
@Service
class OrderService {
private final Sender sender;
OrderService(Sender sender) { // 誰が渡すかは書かない
this.sender = sender;
}
}new OrderService(...) を書く場所はアプリケーションのどこにもありません。起動時にコンテナが Sender の実装を見つけ、コンストラクタに流し込んでくれます。差し替えたいときは、渡す実装を変えるだけです。OrderService のソースコードは1文字も変わりません。
ただし、この講座の実行環境では Spring のコンテナそのものは動きません。そこで演習では、コンテナが裏側でやっている「どの実装を使うか決める」という判断を、自分の手で書きます。
演習で書くこと
通知手段を表す文字列を受け取り、対応する実装を選んで送信結果を返す関数を書きます。new を1か所に閉じ込め、OrderService 自身は実装の名前を知らない形にするのがねらいです。
要件
- Sender インターフェースを作り、MailSender と SmsSender の 2 実装を用意すること
- OrderService は Sender をコンストラクタで受け取り、内部で new しないこと
- run は kind に応じて実装を選び、mail to taro / sms to taro のような文字列を返し、未知の kind では unknown を返すこと
入出力例
run("mail", "taro") → "mail to taro"
run("sms", "taro") → "sms to taro"
run("sms", "hanako") → "sms to hanako"
run("fax", "taro") → "unknown"