メインコンテンツまでスキップ

全体アーキテクチャ

業務ロジックを書き始めた開発者が最初に迷うのは、データベースを読み書きするコードをどのクラスに置くかです。 テーブルを引く処理を API の入口に直接書いても、動くものは動きます。 それでも JavaEE 開発モデルでは、入口から SQL の実行までを四つの層に分け、層をまたぐ呼び出しの向きを一方向に固定します。

レイヤの構成​

プレゼンテーション層 … REST API の入口。入力検証と DTO の変換
↓
アプリケーション層 … ユースケースの実行。バッチジョブの入口
↓
ドメイン層 … 業務ルール。ドメインモデルとインタフェースの定義
↓
インフラストラクチャ層 … 永続化と外部連携。ドメインインタフェースの実装
↓
Database

矢印は呼び出しの向きです。 プレゼンテーション層はアプリケーション層のユースケースを呼び、ユースケースはドメイン層のサービスを呼びます。 エンドポイントがドメインモデルを直接操作したり、DAO を直に呼んだりする実装は、層を飛び越えるため認められません。

レイヤ責務主なクラス
プレゼンテーション層外部インタフェース、リクエストとレスポンスの変換、入力検証、認証と認可Endpoint、Request DTO、Response DTO、Validator
アプリケーション層ユースケースの実行、ドメインサービスの調停UseCase、Job
ドメイン層業務ルール、ドメインモデルとインタフェースの定義Model、Service インタフェース、Repository インタフェース、Exception
インフラストラクチャ層データの永続化、外部システム連携、ドメインインタフェースの実装Entity、DAO、Standard実装クラス

以降は、発注情報を保持する foo_order テーブルと、その周りに並ぶ Order 系のクラスを例に説明します。

依存が内向きになる仕掛け​

呼び出しの順序では、ドメイン層のあとにインフラストラクチャ層が来ます。 依存の向きはその逆です。

ドメイン層に置くのは OrderRepository や OrderService といったインタフェースだけです。 StandardOrderRepository のような実装クラスはインフラストラクチャ層に置き、ドメイン層で宣言されたインタフェースを実装します。 サービスがリポジトリを呼ぶとき、参照しているのはドメイン層のインタフェースであり、その先で im_mirage が動いていることをサービスは知りません。

同心円で描けば、業務ルールを持つドメイン層が中心にあり、プレゼンテーション層もインフラストラクチャ層もその外側に並びます。 依存が内向きだというのは、外側の層は中心を知っているが、中心は外側を知らない、という意味です。

依存の向きが守れているかどうかは、import 文を見れば分かります。 ドメイン層のクラスが jp.co.intra_mart.mirage.ext.session.SessionTemplate や jp.co.intra_mart.mirage.ext.dao.DAOFactory を import していたら、DBアクセスの手段がドメイン層に入り込んでいます。

パッケージ構成​

パッケージはレイヤ単位で分割し、レイヤの下に役割ごとのパッケージを置きます。

jp.co.example.foo
├── presentation
│ ├── endpoint // REST API エンドポイント
│ ├── request // リクエスト DTO
│ ├── response // レスポンス DTO
│ └── validator // 入力検証
├── application
│ ├── usecase // ユースケース
│ ├── job // バッチジョブ
│ └── exception // アプリケーション例外
├── domain
│ ├── model // ドメインモデル
│ ├── service // サービスインタフェースとファクトリ
│ ├── repository // リポジトリインタフェースとファクトリ
│ └── exception // ドメイン例外
└── infrastructure
├── entity // エンティティ(DBマッピング)
├── dao // DAO(im_mirage の呼び出し)
├── model // インフラストラクチャ層の内部で使うモデル
├── repository // リポジトリの実装と既定のファクトリ
└── service // サービスの実装と既定のファクトリ

クラス名の付け方​

クラス名のサフィックスは、そのクラスがどの層に属するかを示します。

レイヤクラス命名
プレゼンテーション層エンドポイント{リソース名}Endpoint
リクエスト DTO{操作名}Request
レスポンス DTO{操作名}Response
バリデータ{操作名}Validator
アプリケーション層ユースケース{操作名}UseCase
バッチジョブ{ジョブ名}Job
ドメイン層ドメインモデルドメイン名そのまま(Order)
サービスインタフェース{ドメイン名}Service
リポジトリインタフェース{ドメイン名}Repository
ファクトリ{ドメイン名}ServiceFactory / {ドメイン名}RepositoryFactory
インフラストラクチャ層エンティティ{ドメイン名}Entity
DAO{ドメイン名}DAO
実装クラスStandard{ドメイン名}Service / Standard{ドメイン名}Repository
既定のファクトリStandard{ドメイン名}ServiceFactory / Standard{ドメイン名}RepositoryFactory

インタフェース名に I プレフィックスは付けません。 実装クラスにも Impl を機械的には付けず、既定の実装であることを示す Standard を接頭辞に置きます。

メソッド名も役割ごとに揃えます。 検索は findBy{条件}、findAll{エンティティ名}s、findLatest{エンティティ名}、業務操作は process{操作} と calculate{指標}、検証は validate{ルール} と is{状態}、変換は convertTo{型} と transformTo{形式} です。 ユースケースとジョブの入口は、どちらも execute で統一します。

エンティティとドメインモデルを分ける理由​

同じ「発注」を表すクラスが、infrastructure.entity と domain.model の二か所に現れることがあります。 重複に見えますが、この二つは所属する層も、満たすべき制約も違います。

観点エンティティドメインモデル
所属レイヤインフラストラクチャ層ドメイン層
目的DBテーブルとの1対1マッピングビジネスロジックの表現
フィールドpublic フィールド(マッピングの要件)private フィールド + getter
可変性ミュータブル(マッピング時に値が設定される)イミュータブル推奨
バリデーションなし(DB制約に依存)ビジネスルールを実装
変換リポジトリ層で相互変換リポジトリ層で相互変換

エンティティが public フィールドを持つのは、それが読みやすいからではありません。 オブジェクト関係マッピング(ORM)を担う im_mirage が public フィールドへ直接値をマッピングする設計であり、この制約はインフラストラクチャ層の都合です。 業務ルールをエンティティに書くと、DBアクセス基盤の都合がドメインの表現に染み出します。 業務ルールはドメインモデルに実装し、両者の変換はリポジトリの実装クラスに閉じ込めます。

同じ理由で、エンティティを API のレスポンスとして返すこともできません。 カラム名と監査項目がそのままクライアントに見えるうえ、テーブル定義の変更が API の互換性を壊します。 外部に返すのはレスポンス DTO です。

エンティティの詳細な規約はエンティティと DAO の作成で扱います。

例外の階層​

例外は層をまたぐたびに、その層の語彙に変換します。

レイヤ例外クラス種別用途
プレゼンテーション層ValidationException非検査例外入力形式の不備
アプリケーション層{アプリ名}Exception検査例外ユースケースの失敗
ドメイン層{ドメイン名}ServiceException、RepositoryException検査例外業務ルール違反、永続化の失敗
インフラストラクチャ層・ファクトリ{ドメイン名}RuntimeException非検査例外ファクトリのロード失敗など、実装の誤り

インフラストラクチャ層のリポジトリは、im_mirage の SQLRuntimeException を RepositoryException に変換してからドメイン層へ渡します。 変換のたびに、元の例外を cause として渡します。 cause を落とすと、スタックトレースから原因の層が消えます。

例外クラスの定義とメッセージの規約はドメイン層の実装ルール、変換の規則はインフラストラクチャ層の実装ルールで扱います。

生成をファクトリに集める​

サービスとリポジトリのインスタンスは、new ではなくファクトリから取得します。

final OrderService orderService = OrderServiceFactory.getInstance().getOrderService();

new StandardOrderService() と書くと、呼び出し側が既定の実装に固定されます。 ファクトリを挟めば、ServiceLoaderUtil による実装の差し替えができ、テストではモックを注入できます。

ファクトリは抽象クラスとして、インタフェースと同じドメイン層のパッケージに置きます。 既定の実装を返す StandardOrderServiceFactory はインフラストラクチャ層に置き、差し替えがないときだけ抽象ファクトリがこれを使います。 既定の実装との結び付きを、この一点に閉じ込めるためです。

ファクトリの実装はドメイン層の実装ルールで扱います。

トランザクション境界はサービスとリポジトリで張る​

DBへのアクセスは、更新か参照かを問わず SessionTemplate.execute(SessionCallback) の中で実行します。 この境界は、ドメインサービスの実装クラスとリポジトリの実装クラスの両方で張ります。

SessionTemplate は入れ子にでき、内側の境界は外側の境界に合流します。 サービスから呼ばれたリポジトリはサービスのトランザクションに加わるため、コミットの単位を決めるのはサービスです。 どのリポジトリを何回呼ぶかを知っているのはサービスだけであり、複数のリポジトリにまたがる更新を一つの単位にまとめられるのもこの層だけです。 リポジトリ側にも境界を置いておくと、リポジトリを単独で呼んだときにも境界の外で DB にアクセスすることがありません。

逆にエンドポイントやユースケースに SessionTemplate を書くと、im_mirage への依存が外側の層まで広がり、リポジトリをインタフェースで抽象化した意味が薄れます。

境界の張り方はインフラストラクチャ層の実装ルール、入れ子になったときの挙動はトランザクション制御で扱います。

関連ドキュメント​

ソースツリーの配置規約は、モジュールプロジェクトの構成で扱っています。

スクリプト開発モデルとの関係

ここで扱うのは JavaEE 開発モデルの実装です。 スクリプト開発モデル(JSSP)のDBアクセスは TenantDatabase と SharedDatabase の API を使い、SQLの書き方も呼び出し方も別物になります。 開発モデルの異なる実装を、そのまま流用しないよう注意してください。