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

ドメイン層の実装ルール

ドメイン層に置くのは、業務そのものを表すドメインモデルと、リポジトリやサービスのインタフェースです。 実装クラスはここにはありません。

パッケージクラス役割
domain.modelOrder、OrderStatus業務ルールを持つドメインモデル
domain.repositoryOrderRepository、OrderRepositoryFactory永続化のインタフェースと取得口
domain.serviceOrderService、OrderServiceFactory業務ロジックのインタフェースと取得口
domain.exceptionOrderServiceException、RepositoryException、OrderRuntimeExceptionこの層が送出する例外

この層が持たない依存​

ドメイン層は、DBアクセスの手段を知りません。 SessionTemplate も DAOFactory も、この層には現れず、import 文にも出てきません。

プレゼンテーション層への参照も同じく持ちません。 ドメインモデルに toResponse() を生やすと、業務の型が API の都合に縛られます。

この原則の抜け道はファクトリだけです。 既定の実装を返すために、ファクトリはインフラストラクチャ層の Standard ファクトリを参照します。 その一点に集めることで、他のクラスからの参照を残しません。

ドメインモデル​

ドメインモデルは、業務ルールを持つイミュータブルなクラスです。

package jp.co.example.foo.domain.model;

import java.math.BigDecimal;

/**
* 発注を表すドメインモデルです。
*/
public class Order {

private final String orderId;

private final String customerName;

private final BigDecimal amount;

private final OrderStatus status;

public Order(final String orderId, final String customerName,
final BigDecimal amount, final OrderStatus status) {
this.orderId = orderId;
this.customerName = customerName;
this.amount = amount;
this.status = status;
}

public String getOrderId() {
return orderId;
}

public String getCustomerName() {
return customerName;
}

public BigDecimal getAmount() {
return amount;
}

public OrderStatus getStatus() {
return status;
}

/**
* 編集できる状態かどうかを判定します。
* @return 編集できる場合は true
*/
public boolean isEditable() {
return status == OrderStatus.DRAFT;
}
}

フィールドは private にして getter だけを公開します。 setter を持たせないため、値を変えたい場合は新しいインスタンスを作ります。

エンティティが status を String で保持するのに対し、ドメインモデルは OrderStatus という列挙型で保持します。 文字列から列挙型への変換はリポジトリの実装が行い、この層には解釈済みの値だけが届きます。

エンティティとの違いを整理した表は全体アーキテクチャにあります。

業務ルールをモデルとサービスのどちらに置くか​

判定に必要な材料で決めます。

  • モデル自身の状態だけで決まるルールはモデルに置く:isEditable() は自分の status を見れば判定できます。
  • 複数のモデルや外部の状態を突き合わせるルールはサービスに置く:在庫数と発注数量の比較のように、別のリポジトリから取り出した値が要るものはサービスの仕事です。

モデルに置けるルールをサービスに書くと、同じ判定が複数のサービスに複製されます。 逆に、リポジトリを必要とするルールをモデルに書こうとすると、モデルがリポジトリを参照することになり、モデルを単体でテストできなくなります。

リポジトリインタフェース​

インタフェースには、永続化の操作だけを並べます。 引数と戻り値にはドメインモデルを使います。

package jp.co.example.foo.domain.repository;

import java.util.List;

import jp.co.example.foo.domain.exception.RepositoryException;
import jp.co.example.foo.domain.model.Order;

/**
* 発注情報の永続化を担う Repository インタフェースです。
*/
public interface OrderRepository {

Order findByOrderId(String orderId) throws RepositoryException;

List<Order> findByStatus(String status) throws RepositoryException;

void save(Order order) throws RepositoryException;
}

ここで OrderEntity を使うことはできません。 エンティティはインフラストラクチャ層のクラスであり、この層のインタフェースに現れると依存が外を向きます。

書き込み系のメソッドは、ドメインモデル1件を単位にします。 複数件を扱いたい場合は、サービスでループするか、DAO のバッチ系メソッドを使います。 取得系は、条件に一致した全件をリストで返してかまいません。

メソッド名は役割ごとに揃えます。 検索は findBy{条件} と findAll(対象を名前に含める場合は findAll{エンティティ名}s)、最新の1件は findLatest{エンティティ名}、保存は save、削除は remove です。

サービスインタフェース​

サービスは、ユースケースから呼ばれる業務ロジックの入口です。

package jp.co.example.foo.domain.service;

import java.util.List;

import jp.co.example.foo.domain.exception.OrderServiceException;
import jp.co.example.foo.domain.model.Order;
import jp.co.example.foo.domain.model.OrderItem;

/**
* 発注情報のビジネスロジックを提供する Service インタフェースです。
*/
public interface OrderService {

void register(Order order, List<OrderItem> items) throws OrderServiceException;

List<Order> findByStatus(String status) throws OrderServiceException;
}

名前は OrderService の形にし、I プレフィックスは使いません。 業務操作は process{操作} や calculate{指標}、検証は validate{ルール} や is{状態} と名付けます。

送出する例外は {ドメイン名}ServiceException に揃えます。 リポジトリが投げる RepositoryException をそのまま通すと、呼び出し側が永続化の失敗と業務の失敗を区別できません。

ファクトリ​

インスタンスの取得口はファクトリに集めます。 ファクトリは抽象クラスとして、インタフェースと同じパッケージに置きます。

package jp.co.example.foo.domain.repository;

import jp.co.example.foo.domain.exception.OrderRuntimeException;
import jp.co.example.foo.domain.exception.RepositoryException;
import jp.co.example.foo.infrastructure.repository.StandardOrderRepositoryFactory;
import jp.co.intra_mart.common.aid.jdk.java.util.ServiceLoaderUtil;
import jp.co.intra_mart.common.platform.log.Logger;

/**
* {@link OrderRepository} を取得するファクトリクラスです。
*/
public abstract class OrderRepositoryFactory {

private static final Logger LOGGER = Logger.getLogger(OrderRepositoryFactory.class);

private static final class LazyHolder {
private static final OrderRepositoryFactory INSTANCE = loadOrderRepositoryFactory();

private static OrderRepositoryFactory loadOrderRepositoryFactory() {
try {
final OrderRepositoryFactory factory = ServiceLoaderUtil.loadFirst(OrderRepositoryFactory.class);
if (factory != null) {
return factory;
}
return new StandardOrderRepositoryFactory();
} catch (final Exception e) {
LOGGER.error("Failed to load OrderRepositoryFactory", e);
throw new OrderRuntimeException("Failed to load OrderRepositoryFactory", e);
}
}
}

protected OrderRepositoryFactory() {
super();
}

/**
* {@link OrderRepository} のインスタンスを取得します。
* @return Repository インスタンス
* @throws RepositoryException 取得に失敗した場合
*/
public abstract OrderRepository getOrderRepository() throws RepositoryException;

/**
* ファクトリのインスタンスを取得します。
* @return ファクトリ
*/
public static OrderRepositoryFactory getInstance() {
return LazyHolder.INSTANCE;
}
}

既定の実装を返すファクトリは、インフラストラクチャ層に置きます。

package jp.co.example.foo.infrastructure.repository;

import jp.co.example.foo.domain.repository.OrderRepository;
import jp.co.example.foo.domain.repository.OrderRepositoryFactory;

/**
* {@link OrderRepositoryFactory} の標準実装クラスです。
*/
public class StandardOrderRepositoryFactory extends OrderRepositoryFactory {

@Override
public OrderRepository getOrderRepository() {
return new StandardOrderRepository();
}
}

呼び出し側は、getInstance() でファクトリを取り出し、getOrderRepository() でリポジトリを受け取ります。

final OrderRepository orderRepository = OrderRepositoryFactory.getInstance().getOrderRepository();

サービスのファクトリも同じ形で書きます。 抽象クラスの OrderServiceFactory と、既定の実装を返す StandardOrderServiceFactory の組になり、取得のメソッドは getOrderService() です。

ServiceLoaderUtil はプラットフォーム共通のユーティリティで、im_jdk_assist モジュールが提供します。 im_mirage の API ではないため、ドメイン層から呼んでもこの層の制約には触れません。

差し替えの仕組みは Java 標準の ServiceLoader に乗っています。 META-INF/services/jp.co.example.foo.domain.repository.OrderRepositoryFactory というファイルに、OrderRepositoryFactory を継承したファクトリクラスの完全修飾名を1行書くと、そのファクトリが検出されます。 loadFirst は検出したファクトリのうち最初の一つを返し、登録がなければ null を返します。 登録がない場合は、既定の StandardOrderRepositoryFactory が使われます。

これにより、他モジュールがファクトリを登録するだけで、呼び出し側のコードに手を入れずに振る舞いを差し替えられます。

ファクトリのロードに失敗したときは、非検査例外の OrderRuntimeException を投げます。 ロードの失敗は起動時点で壊れている状況であり、呼び出し側に対処の余地がないためです。

例外クラス​

domain.exception に置く例外は三つです。 このうち OrderRuntimeException は、ファクトリがロードに失敗したときに使います。

// 検査例外。業務ルール違反、入力不正、対象データの不在
public class OrderServiceException extends Exception {

public OrderServiceException(final String message) {
super(message);
}

public OrderServiceException(final String message, final Throwable cause) {
super(message, cause);
}
}
// 検査例外。永続化の失敗
public class RepositoryException extends Exception {

public RepositoryException(final String message, final Throwable cause) {
super(message, cause);
}
}
// 非検査例外。ファクトリのロード失敗など、実装の誤り
public class OrderRuntimeException extends RuntimeException {

public OrderRuntimeException(final String message, final Throwable cause) {
super(message, cause);
}
}

呼び出し側に対処の余地がある失敗は検査例外に、対処の余地がない失敗は非検査例外にします。 発注が見つからないのは呼び出し側が扱える状況であり、ファクトリが実装をロードできないのは起動時点で壊れている状況です。

メッセージは日本語で書き、原因の特定に必要な変数値を含めます。 運用の担当者や利用者の目に触れるためです。 一方、ログメッセージは英語で書きます。

関連ドキュメント​