アプリケーション層の実装ルール
エンドポイントからサービスを直接呼べば、この層は省けます。 それでもあいだにユースケースを挟むのは、業務の単位と API の単位が一致しなくなるときのためです。 一つの操作が複数のドメインサービスにまたがるようになったとき、その調停をエンドポイントに書くと、同じ手順をバッチジョブから呼べなくなります。
アプリケーション層に置くのは、外部からの入口となる二種類のクラスと、それらが送出する例外です。
| パッケージ | クラス | 役割 |
|---|---|---|
application.usecase | {操作名}UseCase | API から呼ばれるユースケース |
application.job | {ジョブ名}Job | 定期実行の入口 |
application.exception | {アプリ名}Exception | ユースケースの失敗を表す検査例外 |
ユースケース
ユースケースが担うのは、DTO とドメインモデルの変換、ドメインサービスの呼び出し、例外の変換の三つだけです。
package jp.co.example.foo.application.usecase;
import jp.co.example.foo.application.exception.OrderAppException;
import jp.co.example.foo.domain.exception.OrderServiceException;
import jp.co.example.foo.domain.model.Order;
import jp.co.example.foo.domain.service.OrderService;
import jp.co.example.foo.domain.service.OrderServiceFactory;
import jp.co.example.foo.presentation.response.OrderResponse;
import jp.co.intra_mart.common.platform.log.Logger;
/**
* 発注情報を取得するユースケースです。
*/
public class GetOrderUseCase {
private static final Logger LOGGER = Logger.getLogger(GetOrderUseCase.class);
private final OrderService orderService;
public GetOrderUseCase() {
try {
this.orderService = OrderServiceFactory.getInstance().getOrderService();
} catch (final OrderServiceException e) {
throw new RuntimeException("OrderService の初期化に失敗しました", e);
}
}
/**
* 発注情報を取得します。
* @param orderId 発注ID
* @return 発注情報。該当がない場合は null
* @throws OrderAppException 取得に失敗した場合
*/
public OrderResponse execute(final String orderId) throws OrderAppException {
try {
final Order order = orderService.findByOrderId(orderId);
return order != null ? OrderResponse.fromDomainModel(order) : null;
} catch (final OrderServiceException e) {
LOGGER.error("Failed to get order: orderId=" + orderId, e);
throw new OrderAppException("発注情報の取得に失敗しました", e);
}
}
}
サービスは new ではなくファクトリから取得します。
new StandardOrderService() と書くと、呼び出し側が既定の実装に固定され、テストでモックを差し込めなくなります。
ファクトリの getOrderService() は検査例外の OrderServiceException を送出するため、コンストラクタの中で非検査例外に置き換えます。
入口の名前は execute で統一します。
ユースケースとジョブのどちらも execute を持つため、呼び出し側は入口を探さずに済みます。
薄く保つ
業務ルールの判定をユースケースに書くと、同じルールが複数のユースケースに散らばります。 発注の取得と発注の更新で「編集できる状態か」の判定が二重に書かれると、片方だけ直したときに挙動がずれます。 判定はドメイン層に置き、ユースケースはその呼び出しに徹します。
トランザクション境界もここには置きません。
SessionTemplate をユースケースに書くと、im_mirage への依存がアプリケーション層まで届きます。
境界を張るのはサービスとリポジトリの実装であり、それはインフラストラクチャ層の実装ルールで扱います。
複数のドメインサービスを順に呼ぶ調停は、ユースケースの仕事です。 ただし、それらを一つのトランザクションにまとめたい場合は、まとめる単位をサービス側に用意します。
例外の変換
ユースケースは、ドメイン例外をアプリケーション例外に置き換えてからプレゼンテーション層へ渡します。
} catch (final OrderServiceException e) {
LOGGER.error("Failed to get order: orderId=" + orderId, e);
throw new OrderAppException("発注情報の取得に失敗しました", e);
}
OrderAppException は検査例外にします。
プレゼンテーション層はこれを @Response(code = 422) を付けた例外に置き換え、クライアントには 422 が返ります。
@Response を付けていない例外は、想定外の RuntimeException を含めて 500 になります。
種類ごとのステータスコードはプレゼンテーション層の実装ルールで扱っています。
捕捉した OrderServiceException は、第二引数に渡して手放さないようにします。
ここで捨てると、失敗の原因が業務ルールの判定なのか永続化なのかを、スタックトレースから追えなくなります。
バッチジョブ
ジョブはこの層のもう一つの入口です。
BaseJob を継承し、execute にユースケースと同じ役割を持たせます。
package jp.co.example.foo.application.job;
/**
* 未処理の発注を定期的に処理するジョブです。
*/
public class OrderCleanupJob extends BaseJob {
@Override
public JobResult execute() throws JobExecuteException {
final String targetStatus = getParameter("targetStatus");
int processedCount = 0;
// サービスを呼び出して処理し、処理した件数を数える
return JobResult.success("処理を完了しました。対象件数: " + processedCount);
}
}
ジョブは HTTP を経由しないため、プレゼンテーション層の Validator を通りません。
入力の妥当性はドメイン層のサービスが判定します。
サービス側の検証を省けない理由がここにあります。
関連ドキュメント
- ドメイン層の実装ルール:ユースケースが呼び出すサービスのインタフェース
- プレゼンテーション層の実装ルール:ユースケースを呼び出すエンドポイント
- 全レイヤ縦断の実装例:エンドポイントから DB までの一式
- 全体アーキテクチャ:レイヤの責務と例外の階層