- サービスクラスが太るのは設計が下手だからではありません
- 典型的な肥大化の例
- なぜサービスにロジックが集まるのか
- ドメインモデルへ移す
- アプリケーションサービスの本来の役割
- それでもサービスが太るケース
- よくある誤解:サービスを分割すれば解決
- 見分ける基準
- 注意点:モデルへ詰め込みすぎる
サービスクラスが太るのは設計が下手だからではありません
仕様駆動開発(SDD)で実装を進めていると、ほぼ確実に一度は「サービスクラスが巨大になる」状態に遭遇します。行数が増え、if文が増え、メソッドが増え、レビューのたびに「分割しましょう」と言われるようになります。
しかしこの状態は、開発者の能力の問題ではありません。ドメインモデルが不足しているサインです。
多くの場合、サービスクラスは悪者扱いされますが、実際にはサービスクラスは「本来ドメインが持つべき振る舞いを一時的に引き受けている場所」です。肥大化は原因ではなく結果です。
典型的な肥大化の例
例えば注文確定処理を考えます。
public class OrderService { public void confirm(OrderId id, Payment payment){ Order order = orderRepository.findById(id); if(order.isCancelled()){ throw new IllegalStateException("キャンセル済み"); } if(!payment.isCompleted()){ throw new IllegalStateException("未決済"); } if(order.isShipped()){ throw new IllegalStateException("発送済み"); } order.setStatus(OrderStatus.CONFIRMED); orderRepository.save(order); } }
一見、普通のビジネスロジックです。しかしここに大きな問題があります。仕様のルールがすべてサービスに書かれています。つまりOrderという存在が「状態を持つだけのデータ」になっています。
このとき、SDDの観点ではOrderはドメインモデルになっていません。
なぜサービスにロジックが集まるのか
原因は単純です。データ中心に設計しているからです。
最初にテーブル設計を行うと、次の構造になります。
- Entity:DBレコード
- Service:業務ロジック
- Controller:呼び出し
この構造では、ルールを書く場所がServiceしかありません。結果として、すべての仕様がServiceへ集まります。
ドメインモデルへ移す
SDDでは、仕様のルールは対象の振る舞いとして表現します。つまりOrderが自分で「確定できるか」を判断します。
public class Order { public void confirm(Payment payment){ if(isCancelled()){ throw new IllegalStateException("キャンセル済み"); } if(!payment.isCompleted()){ throw new IllegalStateException("未決済"); } if(isShipped()){ throw new IllegalStateException("発送済み"); } this.status = OrderStatus.CONFIRMED; } }
そしてサービスは調整役になります。
public class ConfirmOrderService { public void execute(OrderId id, Payment payment){ Order order = orderRepository.findById(id); order.confirm(payment); orderRepository.save(order); } }
ここで初めて、仕様がモデルに宿ります。サービスは「処理の流れ」を担当し、ルールはモデルが持ちます。
アプリケーションサービスの本来の役割
アプリケーションサービスはビジネスロジックを書く場所ではありません。ユースケースを成立させるための調整役です。
役割は3つだけです。
- モデルを取得
- 振る舞いを呼び出す
- 永続化する
これ以上のロジックが増えたら、ドメインモデルへ移動する候補です。
それでもサービスが太るケース
それでも肥大化する場合があります。特に次のケースです。
- 複数集約にまたがる処理
- 外部システム連携
- トランザクション制御
例えば「注文確定時にポイント付与」を考えます。これはOrderだけでは完結しません。この場合、サービスに処理が残るのは正常です。無理にOrderへ押し込むと、モデルが外部依存を持ってしまいます。
よくある誤解:サービスを分割すれば解決
クラスを分けても本質は変わりません。
- OrderCheckService
- OrderUpdateService
- OrderValidationService
この分割は責務分離ではなく責務拡散です。ルールが依然としてモデル外に存在するため、仕様の一貫性は保てません。
見分ける基準
どこに置くべきか迷ったら、次の基準を使います。
| 処理 | 置き場所 |
| 対象の状態を変える | ドメインモデル |
| 処理の順序を制御 | アプリケーションサービス |
| 外部通信 | インフラ層 |
この基準で判断すると、過剰なサービスロジックは自然に減ります。
注意点:モデルへ詰め込みすぎる
逆の失敗もあります。すべてをモデルへ入れると、巨大エンティティが生まれます。
- メール送信
- API呼び出し
- ログ出力
これらはドメインの責務ではありません。モデルは「ルール」を持ち、「手続き」は持ちません。外部と通信する時点で、それはドメインではなくアプリケーションの関心事です。
最後にまとめです。サービスクラスが肥大化したとき、最初にやるべきは分割ではありません。サービスに書かれているif文を見てください。それらは仕様の制約です。本来はドメインモデルの不変条件です。サービスを減らすのではなく、モデルを増やすと設計は安定していきます。