2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【DDD】業務ロジックをレイヤードアーキテクチャで整理しよう

2
Posted at

はじめに

仕様変更のたびに、直すべきファイルを見落として修正漏れが起きる。
あるいは、業務ルールの一部が API のハンドラや SQL の近くに散らばり、同じ条件が複数箇所にコピーされている。
コードベースが成長すると、このような失敗は個人の注意力だけでは防ぎきれません。

業務ロジックを レイヤードアーキテクチャ で層に分けることで、責務の置き場所をコード上で固定することで、長期間管理運用していく必要があるコードの管理難易度を下げることができます。
「どこを直せばよいか」が構造から読み取れると、半年後の自分や新しく参加したメンバーが変更範囲を追いやすくなります。

この記事で得られる視点

  • レイヤードアーキテクチャの各層の責務と依存の向き
  • 業務ロジックが UI や永続化に漏れるときのリスク
  • レイヤー分割が向くケースと、アンチパターンの見分け方

なぜレイヤーで分けるのか

業務ロジックは、ソフトウェアの中で最も変更理由が多い部分です。
一方で、画面の見た目やデータベースの保存形式は、業務ルールとは別の理由で変わります。

これらを同じファイルに書くと、次のような問題が起きやすくなります。

  • 在庫チェックの条件を直したが、別 API のバリデーションを見落とした
  • テーブル構造の変更が、画面表示のロジックまで巻き込んだ
  • 単体テストで業務ルールを検証するために、毎回 DB を用意する必要がある

レイヤーは、変更理由ごとにコードを分ける引き出しのような役割です。
引き出しごとに入れるものが決まっていれば、どこを探せばよいか迷いにくくなります。
同じように、境界がはっきりしていれば、レビューで「業務ルールの変更はドメイン層だけ」と判断しやすくなります。

レイヤードアーキテクチャとは

レイヤードアーキテクチャは、ソフトウェアを複数の層に分け、上の層が下の層にだけ依存する 構造です。
下の層は上の層を知りません。業務ルールはドメイン層に集約し、UI や DB などの技術的な詳細は上下の層に分けます。
本稿では、クリーンアーキテクチャのように依存を逆転させるのではなく、層が上から下へ積み上がる単純なレイヤード として説明します。

矢印は「依存の向き」を表します。
各層は原則として 真下の層だけ を呼び出し、依存は上から下へ流れます。
アプリケーション層とサービス層は名前が似ていますが、前者はユースケースの入口、後者は業務処理の手順を担う 別の層 です。

主な責務 置くものの例
プレゼンテーション層 入出力の変換、ユーザー操作の受け口 コントローラ、画面コンポーネント、リクエスト検証
アプリケーション層 ユースケースの入口、要求の振り分け ユースケースクラス、Application Facade
サービス層 業務処理の手順、ドメイン層の操作の組み立て OrderService、トランザクション境界
業務ロジック層 業務ルールそのもの ドメインモデル、値オブジェクト、ドメインサービス
データアクセス層 永続化の具体手段 リポジトリ、ORM マッピング、SQL

各層の責務

プレゼンテーション層

HTTP リクエストや画面イベントを受け取り、アプリケーション層を呼び出す層です。
「JSON の形に合わせる」「ステータスコードを返す」といった 入出力の都合 を扱います。

業務ルール(在庫が足りなければ注文不可、など)はここに書かないのが原則です。
プレゼンテーション層に業務判断が入ると、別の API やバッチから同じルールを再利用しづらくなります。

アプリケーション層

「どのユースケースを実行するか」を決める、薄い入口 の層です。
プレゼンテーション層から渡されたパラメータを受け取り、対応するサービス層のメソッドを呼び出します。

業務ルールや永続化の手順はここに書きません。
アプリケーション層に処理が溜まると、サービス層との境界が曖昧になり、同じ手順が別の入口から重複しやすくなります。

サービス層

ドメイン層のオブジェクトを組み合わせ、業務処理の手順 を実装する層です。
「注文を作成して確定し、保存する」といった一連の流れや、トランザクションの開始・終了をここに置きます。

業務ルールそのものはドメイン層に残し、サービス層は「いつ・何を呼ぶか」に専念するのが一般的です。
ルールをサービス層に if 文で書き足すと、トランザクションスクリプトと同様、手続きが肥大化しやすくなります。

業務ロジック層(ドメイン層)

ソフトウェアの核となる層です。
「この数量は登録できない」「この状態からは遷移できない」といった 業務上の制約 をここに集約します。

ドメインモデルや値オブジェクトを使う場合も、この層が主な置き場所になります。
アクティブレコードを採用する場合でも、永続化の詳細と業務ルールの境界を意識して分ける判断が必要です。

データアクセス層

データベースや外部ストレージへの読み書きを担当します。
テーブル名、カラム型、ORM の設定など 保存の仕組み を扱います。

ドメイン層から呼び出され、ドメインオブジェクトの読み書きを SQL や ORM マッピングで行います。
保存形式の変更は、この層の修正に閉じ込められるのが理想です。

サンプルコード

注文作成の責務分担のイメージです。
業務ルールは Order に閉じ、手順はサービス層、入口はアプリケーション層に分けています。

/** 業務ロジック層:注文の不変条件を守る */
export class Order {
  private constructor(
    private readonly id: string,
    private readonly lines: readonly OrderLine[],
    private status: 'draft' | 'placed',
  ) {}

  static create(id: string, lines: readonly OrderLine[]): Order {
    if (lines.length === 0) {
      throw new Error('明細が空の注文は作成できません');
    }
    return new Order(id, lines, 'draft');
  }

  place(): void {
    if (this.status === 'placed') {
      throw new Error('すでに確定済みです');
    }
    this.status = 'placed';
  }
}

/** データアクセス層:永続化の具体 */
export interface OrderRepository {
  save(order: Order): Promise<void>;
}

/** ドメイン層:確定した注文の永続化をデータアクセス層に委譲 */
export class OrderPersister {
  constructor(private readonly orders: OrderRepository) {}

  async save(order: Order): Promise<void> {
    await this.orders.save(order);
  }
}

/** サービス層:業務処理の手順 */
export class OrderService {
  constructor(private readonly persister: OrderPersister) {}

  async placeOrder(orderId: string, lines: readonly OrderLine[]): Promise<void> {
    const order = Order.create(orderId, lines);
    order.place();
    await this.persister.save(order);
  }
}

/** アプリケーション層:ユースケースの入口 */
export class PlaceOrderUseCase {
  constructor(private readonly orderService: OrderService) {}

  async execute(orderId: string, lines: readonly OrderLine[]): Promise<void> {
    await this.orderService.placeOrder(orderId, lines);
  }
}

Order はリポジトリの存在を知りません。
PlaceOrderUseCaseOrderServiceOrder / OrderPersisterOrderRepository の順に、上の層が真下の層だけを呼び出す構成です。
業務ルールのテストは Order だけで実行できます。

アンチパターン

  • コントローラに在庫チェックを書く:同じ条件が別エンドポイントにコピーされ、修正漏れの温床になる
  • アプリケーション層とサービス層の責務を混同する:入口と業務手順が同一クラスに集まり、層の意味が失われる
  • ドメイン層から ORM を直接 import する:テーブル変更が業務ルールのテストまで壊す
  • サービス層に if 文の業務判断を集める:手続きが長くなり、ドメインモデルが薄くなる
  • 層の存在を口頭ルールだけで運用する:レビュー依存になり、境界違反が静かに増える

個人の注意に頼るより、ディレクトリ構成や Lint ルールで import を制限するほうが、チームでは堅牢です。
私は、ディレクトリ単位で import を制限する Lint 設定を併用し、レビューだけに頼らない運用にしています。

レイヤードアーキテクチャに向いているケース

  • 業務ルールが複数の入口(画面・API・バッチ)から共有される
  • 永続化方式や UI フレームワークの変更が想定される
  • チームで「業務の変更はどの層か」を共通認識にしたい

向いていない・別の検討が必要なケース

  • ドメインが極めて単純で、CRUD だけで完結する小規模ツール
  • 中核ドメインが非常に複雑で、層の内側だけでは表現しきれない(ポートとアダプタを明示する設計の検討)
  • 最初から過度な層分けを行い、変更のたびに複数ファイルを行き来する手間が利益を上回る

レイヤーは 目的 ではなく 手段 です。
複雑さに見合った分だけ層を設け、不要な抽象化は避けるのが現実的です。

まとめ

レイヤードアーキテクチャは、業務ロジック・入出力・永続化の 変更理由 を分離するための構造です。
上の層が下の層にだけ依存する一方通行を守ると、業務ルールの所在がコードから読み取りやすくなります。

私は、修正漏れを減らすために「注意喚起」だけでなく、層ごとの責務と import の制約をセットで設計することを優先しています。
次に仕様変更が入るとき、まず「この変更はどの層の責務か」を確認する習慣から始めると、整理の効果を実感しやすくなります。

参考文献

  • ドメイン駆動設計をはじめよう(著:Vlad Khononov 訳:増田 亭、綿引 琢磨)
2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?