SDDでサービスクラスが肥大化する原因と対策

サービスクラスが太るのは設計が下手だからではありません

仕様駆動開発(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文を見てください。それらは仕様の制約です。本来はドメインモデルの不変条件です。サービスを減らすのではなく、モデルを増やすと設計は安定していきます。