はじめに
本記事では、ドメイン駆動設計のパターンとして用いられるリポジトリについて、初心者向けに解説したものです。
対象読者層
- ドメイン駆動設計における、リポジトリがわからない方
- リポジトリの実装について悩んでいる方
前提条件
- ドメイン駆動設計における以下パターンの概念について理解していること
- 値オブジェクト
- エンティティ
- ドメインサービス
本記事で登場する用語説明
用語 | 概要 | 具体例 |
---|---|---|
データストア | システム上で利用するデータを保存する媒体を指す | データベース、CSVファイル等 |
結論
- リポジトリは、データストアに対してデータ操作を行うオブジェクトを指す
- ドメインに関するロジックとデータ操作ロジック(ドメイン知識と関係ない詳細ロジック)を分離するのが目的
- 単純なデータ操作ルール(メソッド)のみ定義するのがミソ
解説
リポジトリは、データストアとやり取りする役割を担う
ほとんどのシステムには、使用するデータを格納したデータストアが存在します。
データストアへの操作は、採用するデータストアの種類によって異なります。
代表的なもので言えば、データストアがデータベースの場合、SQLを使用してデータ操作を行います。
しかし、データストアへのアクセスは、ほとんどのシステムにおいてドメイン知識とは無関係の処理です。
ドメインオブジェクト内にデータ操作ロジックが混入すると複雑さが増してしまう
はじめに悪い例として、勤怠管理システムのコードを見てみましょう。
以下は、新しい社員データをシステムに登録するサンプルコードです。
社員登録時には同じ社員IDは登録できないルールが存在すると仮定します。
// 社員を表現するクラス
public class Employee {
// 社員ID
private String employeeId;
// 名前
private String name;
// 入社日
private String hireDate;
public Employee(String employeeId, String name, String hireDate) {
this.employeeId = employeeId;
this.name = name;
this.hireDate = hireDate;
}
public String getEmployeeId() {
return employeeId;
}
public String getName() {
return name;
}
public String getHireDate() {
return hireDate;
}
}
/**
* 社員データを登録するドメインサービス
*/
public class EmployeeService {
private static final String DB_URL = "jdbc:mysql://localhost:3306/attendance";
private static final String DB_USER = "root";
private static final String DB_PASSWORD = "password";
/**
* 社員情報を登録します
*/
public void registerEmployee(Employee employee) {
if (isEmployeeIdDuplicated(employee.getEmployeeId())) {
System.err.println("Error: Employee ID " + employee.getEmployeeId() + " is already registered.");
return;
}
String sql = "INSERT INTO employees (employee_id, name, hire_date) VALUES (?, ?, ?)";
try (Connection connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setString(1, employee.getEmployeeId());
preparedStatement.setString(2, employee.getName());
preparedStatement.setString(3, employee.getHireDate());
preparedStatement.executeUpdate();
System.out.println("Employee registered successfully!");
} catch (SQLException e) {
System.err.println("Failed to register employee: " + e.getMessage());
}
}
/**
* 渡された社員IDがすでに登録済みか確認します
*/
private boolean isEmployeeIdDuplicated(String employeeId) {
String sql = "SELECT COUNT(*) FROM employees WHERE employee_id = ?";
try (Connection connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setString(1, employeeId);
try (ResultSet resultSet = preparedStatement.executeQuery()) {
if (resultSet.next()) {
return resultSet.getInt(1) > 0;
}
}
} catch (SQLException e) {
System.err.println("Failed to check duplicate employee ID: " + e.getMessage());
}
return false;
}
}
public class Main {
public static void main(String[] args) {
Employee employee1 = new Employee("E001", "Yamada Taro", "2025/01/21");
Employee employee2 = new Employee("E001", "Suzuki Hanako", "2025/02/01"); // 同じ社員IDを持つ
EmployeeService employeeService = new EmployeeService();
employeeService.registerEmployee(employee1);
employeeService.registerEmployee(employee2); // 重複エラーが発生する
}
}
上記のコードのは、以下の問題点があります。
-
ドメインサービス(EmployeeService.java)内に、データベース接続・データ登録に関するSQLロジックが書かれており、重要なドメインルールである同じ社員IDは登録できないルールが分かりづらい
-
DB_URLから、MySQLを採用していることがわかるが、仮に他のデータベース(SQLServer等)に移行する場合、ドメインサービスのロジックも変更が必要になる
※本来ドメインルールとは無関係な修正がドメインサービスへ影響を与える -
ドメインサービスがデータベースに依存しているため、単体テストでデータベースをモックすることが難しい
リポジトリを採用すると、ドメインオブジェクト内ロジックの可読性が向上する
先程の問題点を、リポジトリを使って解決します。
リポジトリを採用すると、ドメインオブジェクト内のロジックと、データストア操作のロジックを分離することができます。
これにより、ドメインオブジェクトでは、データストア操作を気にすること無くより重要なビジネスロジックに注力することができます。
先程の勤怠管理システムのコードを、リポジトリを使った形にリファクタリングを行います。
リポジトリを使ってロジックの分離を行ったコード
※ここでは変更したクラス部分のみ紹介します。
// リポジトリインターフェース
public interface EmployeeRepository {
boolean isEmployeeIdDuplicated(String employeeId);
void save(Employee employee);
}
/**
* リポジトリの具体的な実装クラス
*/
public class EmployeeRepositoryImpl implements EmployeeRepository {
private static final String DB_URL = "jdbc:mysql://localhost:3306/attendance";
private static final String DB_USER = "root";
private static final String DB_PASSWORD = "password";
@Override
public boolean isEmployeeIdDuplicated(String employeeId) {
String sql = "SELECT COUNT(*) FROM employees WHERE employee_id = ?";
try (Connection connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setString(1, employeeId);
try (ResultSet resultSet = preparedStatement.executeQuery()) {
if (resultSet.next()) {
return resultSet.getInt(1) > 0;
}
}
} catch (SQLException e) {
System.err.println("Failed to check duplicate employee ID: " + e.getMessage());
}
return false;
}
@Override
public void save(Employee employee) {
String sql = "INSERT INTO employees (employee_id, name, hire_date) VALUES (?, ?, ?)";
try (Connection connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setString(1, employee.getEmployeeId());
preparedStatement.setString(2, employee.getName());
preparedStatement.setString(3, employee.getHireDate());
preparedStatement.executeUpdate();
System.out.println("Employee registered successfully!");
} catch (SQLException e) {
System.err.println("Failed to save employee: " + e.getMessage());
}
}
}
/**
* 社員データを登録するドメインサービス
*/
public class EmployeeService {
private final EmployeeRepository employeeRepository;
public EmployeeService(EmployeeRepository employeeRepository) {
this.employeeRepository = employeeRepository;
}
/**
* 社員データを登録します
*/
public void registerEmployee(Employee employee) {
if (employeeRepository.isEmployeeIdDuplicated(employee.getEmployeeId())) {
System.err.println("Error: Employee ID " + employee.getEmployeeId() + " is already registered.");
return;
}
employeeRepository.save(employee);
}
}
スッキリしたドメインサービス
まず注目してほしいのが、EmployeeServiceクラスです。
リポジトリを採用する前と比較すると、コード量がかなり少なくなりました。
これにより、ソースコードの可読性が向上し、本来のドメインルールである、重複した社員IDは登録できないことがわかりやすくなりました。
データストアに依存しなくなったドメインサービス
データストアに対する操作はインターフェースを仲介することで、データストアに依存しなくなりました。
EmployeeServiceクラスのコンストラクタに注目してください。
引数として、EmployeeRepositoryインターフェースを受け取っています。
これにより、EmployeeRepositoryクラスは、データストアに対して、社員IDの重複チェック後、社員情報を登録することだけわかっていれば良いことになります。
つまり、データストアの種類が何であれEmployeeServiceクラスには影響しません。
データストアの種類が、MySQLでも、SQLServerでも、CSVファイルでも、EmployeeServiceクラスは動作することになります
単体テストが容易になったドメインサービス
データストアの操作ロジックが取り除かれたことにより、単体テストも簡単になりました。
EmployeeServiceクラスのコンストラクタではEmployeeRepositoryインターフェースを受け取っているため、モックが簡単になりました。
これにより、データベースのモックが不要になり本来のドメインルールである、重複した社員IDは登録できないことを重点的にテストしやすくなります。
また、仮にデータストアの種類が変更になった場合でも、EmployeeServiceクラスのテストコードを修正する必要がありません。
リポジトリを設計するコツ
リポジトリは、最小限の機能のみ提供する
リポジトリに定義する機能は、単純操作(CRUD)に限定したほうが良いです。
これには、以下の理由があります。
- シンプルで保守性が高い設計になる
- 役割の混在を防ぐ
リポジトリが単純な操作だけを提供することで、複雑なビジネスロジックをドメイン層やサービス層で管理でき、変更が容易になります。
また、リポジトリ内にビジネスルールを含めてしまうと、リポジトリの責務が曖昧になってしまいます。
悪い例として、バリデーションチェックをリポジトリ内で実施してしまうケースがあります。
// 悪い例 ビジネスルールが、リポジトリに書かれてしまっている
public class EmployeeRepositoryImpl implements EmployeeRepository {
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
@Override
public void save(Employee employee) {
// ビジネスロジック: 社員IDのバリデーション
if (employee.getEmployeeId() == null || !employee.getEmployeeId().matches("EMP[0-9]{4}")) {
throw new IllegalArgumentException("社員IDはEMP+4桁の数字である必要があります");
}
// ビジネスロジック: 名前のバリデーション
if (employee.getName() == null || employee.getName().isEmpty()) {
throw new IllegalArgumentException("社員名は空であってはなりません");
}
// ビジネスロジック: 入社日のバリデーション
try {
formatter.format(employee.getHireDate());
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("入社日はyyyy/MM/dd形式でなければなりません");
}
// データストアへの保存(ここでは省略)
}
}
上記のリポジトリには、ビジネスルールとして重要な社員ID、名前、入社日のバリデーションルールが書かれています。
これらのバリデーションチェックは、ドメインオブジェクト側で実施するようにしましょう。
単一責任原則に則り、リポジトリはデータストアへアクセスする操作だけに注力するようにしましょう
インターフェースを実装すること
ドメインオブジェクトとのやり取りにインターフェースを経由する理由は、ソフトウェア設計における柔軟性や保守性を高める目的があります。
リファクタリングをしたEmployeeServiceクラスを例にすると、
EmployeeRepositoryクラスは、データストアに対して、社員IDの重複チェック後、社員情報を登録することだけわかっていれば良いのです。
これにより、データ登録を行うEmployeeRepositoryImplクラスに修正が入っても
EmployeeServiceクラスは修正する必要がありません。
(データストアの種類が、データベースからCSVファイルに変わっても問題ないのです)
インターフェースを使うことで、クラス間の結合度を緩くすることができます。
ドメインオブジェクトとリポジトリの独立性が高まり、変更しやすい設計になります