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

アプリケーション層の実装ルール

エンドポイントからサービスを直接呼べば、この層は省けます。 それでもあいだにユースケースを挟むのは、業務の単位と API の単位が一致しなくなるときのためです。 一つの操作が複数のドメインサービスにまたがるようになったとき、その調停をエンドポイントに書くと、同じ手順をバッチジョブから呼べなくなります。

アプリケーション層に置くのは、外部からの入口となる二種類のクラスと、それらが送出する例外です。

パッケージクラス役割
application.usecase{操作名}UseCaseAPI から呼ばれるユースケース
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 を通りません。 入力の妥当性はドメイン層のサービスが判定します。 サービス側の検証を省けない理由がここにあります。

関連ドキュメント​