@Serviceに置くもの
このレッスンでは、@Service を付けたクラスに置くべきものと置くべきでないものを区別し、業務ルールを1か所に集める書き方を身につけます。
@Service はただの目印です
@Service は @Component の別名です。DI コンテナに登録するという働きは @Component とまったく同じで、追加の機能はありません。それでもわざわざ使い分けるのは、読む人に対して「このクラスは業務ルールの置き場です」と宣言するためです。
Java
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PointRepository pointRepository;
OrderService(OrderRepository orderRepository, PointRepository pointRepository) {
this.orderRepository = orderRepository;
this.pointRepository = pointRepository;
}
}依存はコンストラクタで受け取ります。フィールドを final にできるので、生成後に差し替わらないことが型で保証されます。
Service に置くもの
置くべきものは、次の4種類です。
- 可否の判断 — 出荷できる状態か、在庫は足りているか、この利用者に権限があるか
- 計算 — 送料、割引、ポイント、税額、締め日の判定
- 複数の操作をまとめた手続き — 注文を保存し、在庫を減らし、通知を積む、という一連の流れ
- トランザクション境界 — 上の手続きを、まとめて成功かまとめて失敗かにする
@Transactional
共通しているのは、どれも「業務としてどうあるべきか」を決めている点です。HTTP でも SQL でもない、その事業の決まりごとがここに集まります。
Service に置かないもの
逆に、次のものは Service に入れません。
| 置かないもの | 本来の置き場 | 理由 |
|---|---|---|
HttpServletRequest、ResponseEntity、ステータスコード | Controller | HTTP から呼ばれたことを Service が知る必要はありません |
| リクエスト DTO やレスポンス DTO | Controller | 入口の形に業務ルールが縛られます |
SQL 文字列、EntityManager の直接操作 | Repository | 保存先の都合が業務ルールに混ざります |
| 画面表示用の文字列組み立て | Controller または表示層 | 表示の都合が変わるたびに業務ルールを触ることになります |
System.out.println によるデバッグ出力 | ロガー | 出力先を後から切り替えられません |
判定に迷ったら、次の問いを使ってください。この処理は、同じアプリを画面のない定期バッチとして書き直しても、そのまま必要でしょうか。必要ならば Service です。
薄い Service は Service ではありません
よくある失敗が、Repository をただ呼び直すだけのメソッドで Service を埋めることです。
Java
@Service
public class OrderService {
public Order find(Long id) {
return orderRepository.findById(id).orElse(null);
}
}これは層を1つ増やしただけで、何の判断も引き受けていません。業務ルールが無いのに Service を置いても、読む場所が増えるだけです。逆に言えば、Service が薄くなっているときは、業務ルールが Controller かエンティティか、あるいは画面側に漏れている可能性を疑ってください。
計算ルールは1つのメソッドにまとめます
送料や割引のようなルールは、あちこちの if に散らすと必ず食い違います。注文画面と確認画面と管理画面で送料が違って見える不具合は、たいていこれが原因です。ルールごとに入口を1つ決め、そこだけを見れば答えが分かる形にします。
Java
@Service
public class PricingService {
public int shippingFee(int total) {
return total >= 5000 ? 0 : 500;
}
}呼ぶ側は判断せず、答えを受け取るだけにします。これで、無料ラインを変えるときに触る場所が1か所に決まります。
演習で書くもの
演習では、注文の合計金額と会員かどうかから、割引額と送料と請求額を決めます。Spring では @Service を付けたクラスのインスタンスメソッドとして書き、DI で受け取って Controller から呼ぶものですが、判断そのものはフレームワークと無関係な素の Java です。ここでは static メソッドとして書いて、ルールの形だけに集中します。
ルールは次の通りです。まず合計金額から割引率を決め、割引額を引きます。割引後の金額で送料を判定します。この順番が結果を変える点にも注目してください。
要件
- 戻り値は discount=<割引額>,fee=<送料>,charge=<請求額> の形にすること
- 割引率は member が true のときだけ適用し、10000 円以上で 10 パーセント、5000 円以上で 5 パーセント、それ未満は 0 パーセントとすること
- 送料は割引前ではなく割引後の金額で判定し、5000 円以上なら 0 円、未満なら 500 円とすること。ただし total が 0 のときは送料も 0 円とすること
入出力例
quote(12000, true) → "discount=1200,fee=0,charge=10800"
quote(10000, true) → "discount=1000,fee=0,charge=9000"
quote(5000, true) → "discount=250,fee=500,charge=5250"
quote(5000, false) → "discount=0,fee=0,charge=5000"
quote(4999, true) → "discount=0,fee=500,charge=5499"
quote(0, true) → "discount=0,fee=0,charge=0"