【ドメイン駆動設計入門】-リポジトリについて解説

# はじめに
本記事では、ドメイン駆動設計のパターンとして用いられるリポジトリについて、初心者向けに解説したものです。

https://qiita.com/yoron0122/items/789e71cb277434be7bad

# 対象読者層
- ドメイン駆動設計における、リポジトリがわからない方
- リポジトリの実装について悩んでいる方


# 前提条件
- ドメイン駆動設計における以下パターンの概念について理解していること
  - 値オブジェクト
  - エンティティ
  - ドメインサービス


https://qiita.com/yoron0122/items/3cde9d8d9f29eae75056

https://qiita.com/yoron0122/items/cab5c482810018480400

https://qiita.com/yoron0122/items/aa5526aae7773cc288cb

# 本記事で登場する用語説明
| **用語**            | **概要**                              | **具体例**                                     |  
|----------------------|---------------------------------------|-----------------------------------------------|  
| **データストア**         | システム上で利用するデータを保存する媒体を指す  | データベース、CSVファイル等 | 

# 結論
- リポジトリは、**データストアに対してデータ操作を行うオブジェクト**を指す
- ドメインに関するロジックとデータ操作ロジック（ドメイン知識と関係ない詳細ロジック）を分離するのが目的
- 単純なデータ操作ルール（メソッド）のみ定義するのがミソ


# 解説
# リポジトリは、データストアとやり取りする役割を担う
ほとんどのシステムには、使用するデータを格納したデータストアが存在します。
データストアへの操作は、採用するデータストアの種類によって異なります。
代表的なもので言えば、データストアがデータベースの場合、SQLを使用してデータ操作を行います。

しかし、データストアへのアクセスは、ほとんどのシステムにおいてドメイン知識とは無関係の処理です。

## ドメインオブジェクト内にデータ操作ロジックが混入すると複雑さが増してしまう
はじめに悪い例として、勤怠管理システムのコードを見てみましょう。
以下は、新しい社員データをシステムに登録するサンプルコードです。
社員登録時には**同じ社員IDは登録できない**ルールが存在すると仮定します。


```plantuml
@startuml
class Employee {
    - String employeeId
    - String name
    - String hireDate
    + Employee(String employeeId, String name, String hireDate)
    + String getEmployeeId()
    + String getName()
    + String getHireDate()
}

class EmployeeService {
    - String DB_URL
    - String DB_USER
    - String DB_PASSWORD
    + void registerEmployee(Employee employee)
    - boolean isEmployeeIdDuplicated(String employeeId)
}

class DataStore {
    + employees : Table
}

EmployeeService --> Employee : uses
EmployeeService --> DataStore : interacts
@enduml
```


```java:Employee.java
// 社員を表現するクラス
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;
    }
}
```
```java:EmployeeService .java
/**
 * 社員データを登録するドメインサービス
 */
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;
    }
}

```

```java:Main.java

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等）に移行する場合、ドメインサービスのロジックも変更が必要になる
※**本来ドメインルールとは無関係な修正がドメインサービスへ影響を与える**
- ドメインサービスがデータベースに依存しているため、単体テストでデータベースをモックすることが難しい

# リポジトリを採用すると、ドメインオブジェクト内ロジックの可読性が向上する
先程の問題点を、リポジトリを使って解決します。
リポジトリを採用すると、**ドメインオブジェクト内のロジックと、データストア操作のロジックを分離することができます**。

これにより、ドメインオブジェクトでは、データストア操作を気にすること無く**より重要なビジネスロジックに注力することができます**。


先程の勤怠管理システムのコードを、リポジトリを使った形にリファクタリングを行います。


# リポジトリを使ってロジックの分離を行ったコード


```plantuml
@startuml
class Employee {
    - String employeeId
    - String name
    - String hireDate
    + Employee(String employeeId, String name, String hireDate)
    + String getEmployeeId()
    + String getName()
    + String getHireDate()
}

interface EmployeeRepository {
    + boolean isEmployeeIdDuplicated(String employeeId)
    + void save(Employee employee)
}

class EmployeeRepositoryImpl {
    - String DB_URL
    - String DB_USER
    - String DB_PASSWORD
    + boolean isEmployeeIdDuplicated(String employeeId)
    + void save(Employee employee)
}

class EmployeeService {
    - EmployeeRepository employeeRepository
    + EmployeeService(EmployeeRepository employeeRepository)
    + void registerEmployee(Employee employee)
}

class DataStore {
    + employees : Table
}

EmployeeRepositoryImpl --> EmployeeRepository : implements
EmployeeService --> EmployeeRepository : uses
EmployeeService --> Employee : uses
EmployeeRepositoryImpl --> DataStore : interacts
@enduml

```


※ここでは変更したクラス部分のみ紹介します。

```java:EmployeeRepository.java
// リポジトリインターフェース
public interface EmployeeRepository {
    boolean isEmployeeIdDuplicated(String employeeId);
    void save(Employee employee);
}
```

```java:EmployeeRepositoryImpl.java
/**
 * リポジトリの具体的な実装クラス
 */
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());
        }
    }
}
```

```java:EmployeeService.java
/**
 * 社員データを登録するドメインサービス
 */
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クラスには影響しません**。

:::note info
データストアの種類が、MySQLでも、SQLServerでも、CSVファイルでも、EmployeeServiceクラスは動作することになります
:::

## 単体テストが容易になったドメインサービス
データストアの操作ロジックが取り除かれたことにより、単体テストも簡単になりました。
EmployeeServiceクラスのコンストラクタではEmployeeRepositoryインターフェースを受け取っているため、モックが簡単になりました。

これにより、データベースのモックが不要になり**本来のドメインルールである、重複した社員IDは登録できないことを重点的にテストしやすくなります**。

また、仮にデータストアの種類が変更になった場合でも、EmployeeServiceクラスのテストコードを修正する必要がありません。

# リポジトリを設計するコツ
## リポジトリは、最小限の機能のみ提供する
リポジトリに定義する機能は、単純操作（CRUD）に限定したほうが良いです。
これには、以下の理由があります。
- シンプルで保守性が高い設計になる
- 役割の混在を防ぐ

リポジトリが単純な操作だけを提供することで、複雑なビジネスロジックをドメイン層やサービス層で管理でき、変更が容易になります。

また、リポジトリ内にビジネスルールを含めてしまうと、リポジトリの責務が曖昧になってしまいます。
悪い例として、バリデーションチェックをリポジトリ内で実施してしまうケースがあります。
```java:EmployeeRepositoryImpl .java
// 悪い例 ビジネスルールが、リポジトリに書かれてしまっている
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、名前、入社日のバリデーションルールが書かれています。
これらのバリデーションチェックは、ドメインオブジェクト側で実施するようにしましょう。

:::note info
単一責任原則に則り、リポジトリは**データストアへアクセスする操作だけに注力**するようにしましょう
:::

## インターフェースを実装すること
ドメインオブジェクトとのやり取りにインターフェースを経由する理由は、ソフトウェア設計における**柔軟性や保守性を高める**目的があります。

リファクタリングをしたEmployeeServiceクラスを例にすると、
EmployeeRepositoryクラスは、**データストアに対して、社員IDの重複チェック後、社員情報を登録する**ことだけわかっていれば良いのです。

これにより、データ登録を行うEmployeeRepositoryImplクラスに修正が入っても
EmployeeServiceクラスは修正する必要がありません。
（データストアの種類が、データベースからCSVファイルに変わっても問題ないのです）

インターフェースを使うことで、クラス間の結合度を緩くすることができます。

:::note info
ドメインオブジェクトとリポジトリの独立性が高まり、変更しやすい設計になります
:::