9
4

More than 1 year has passed since last update.

ドメイン駆動設計入門 Chapter 12 ドメインのルールを守る「集約」 - 集約とは を読んで

Last updated at Posted at 2022-02-20

4ヶ月ほど前から、ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本の輪読会に参加しています。昨年11月にChapter 2を担当したのに続き、「Chapter 12 ドメインのルールを守る『集約』」を担当することになりました。このため、自分なりに理解した内容と感想をまとめました。

注意:本記事は、あくまでも筆者の理解をまとめたものです。 正確な解説を知りたい場合は、書籍や別の記事などをご参照ください。

本書で取り扱われているサンプルコードのプログラミング言語はすべてC#が採用されています。しかし、筆者はC#に触れた経験がないため、本記事内におけるソースコードはすべてJavaを採用しました。

こんなコードはありませんか?

新しいクラスを作成したときに、「フィールドに対して、とりあえずgetterをつくる」ということをしていませんか。

Circleクラス
import java.util.ArrayList;
import java.util.List;

public class Circle {

    private final List<User> members = new ArrayList<>();

    /**
     * サークルのメンバー一覧を取得します
     * @return メンバーのリスト
     */
    public List<User> getMembers() {
      return members;
    }
}
サークルにメンバーを加入させる
circle.getMembers().add(member);

しかし、このCircleクラスの実装には、一つの問題があります。
「サークルにメンバーを加入させる」するだけなのに、メンバーの一覧を取得できるようになってしまっています。

その結果、たとえばアプリケーションのどこかに、下記のようなコードを書くことができてしまいます。

アプリケーションのどこかに書いてあるかもしれないコード
circle.getMembers().clear(); // メンバーが空になっているかもしれない
circle.getMembers().sort(); // メンバーの順番が変わっているかもしれない
circle.getMembers().set(index, user); // 一部のメンバーが置き換わっているかもしれない

言い換えると、上記のようなコードがどこかに存在しているかどうかを確認しながら、開発しなければいけません。
これでは開発スピードが落ちてしまいますね。では、どうすればよいのでしょうか。

解決策

以下のルールを守ることで解決ができます。

「外部からの集約に対する操作はすべて集約ルートを経由しておこなう」

・・・なんだか難しい表現ですね。しかし、この表現を分解すると、意外とシンプルな話です。

用語 説明
集約 ひとまとまりとして扱えるドメインオブジェクトの単位
集約ルート 集約のうち、外部から操作・参照する特定のオブジェクト

先ほどのコードで言うと、メンバーのリストそのものを外部から操作するのではなく、circleオブジェクトのみを操作するようにすればよいのです。

Circleクラス(修正後)
import java.util.ArrayList;
import java.util.List;

public class Circle {

    private final List<User> members = new ArrayList<>();

    /**
     * サークルにメンバーが加入します
     * @param user 加入するメンバー
     */
    public void join(User member) {
      members.add(member);
    }
}
サークルにメンバーを加入させる(修正後)
circle.join(member);

メソッドを呼び出すコードも、具体的な内部実装を読み上げるようなコードから、短くシンプルなものに変わりました。
修正前よりも読みやすくなったのではないでしょうか。

デメテルの法則

上記のルールは、オブジェクト指向プログラミングの設計におけるガイドラインである「デメテルの法則」でも表現できます。
デメテルの法則は、簡潔に「直接の友達とだけ話すこと」と要約されます。

上記のコードの例で言うと、circleの中のメンバーリストと話す(メンバーリストのメソッドを呼び出す)のではなく、circleとだけ話す(circleのメソッドだけを呼び出す)ということですね。

このルールが守られていれば、たとえばサークルのメンバーの加入に何らかの条件(メンバー数の上限など)を設定する場合でも、その条件に関するコードがCircleクラスの中だけで完結します。つまり、コードのメンテナンス性を高めることができます。

Javaのソースコード解析ツールであるPMDでは、デメテルの法則に違反しているコードを検出するルール「LawOfDemeter」が提供されています。

問題:リポジトリによるインスタンスの永続化がうまくいかない

しかし、上記のルールに従ってgetterを完全に非公開にすると、リポジトリによるインスタンスの永続化で困る可能性があります。

public class UserRepository {
    public void save(User user) {
        // 永続化のために、Userオブジェクトから文字列値を取り出して、データモデルオブジェクトに変換
        UserDataDto dto = new UserDataDto();
        dto.setId(user.getUser().getValue());
        dto.setName(user.getName().getValue());

        // ...以下、保存処理
    }
}

上記のコードの user.getUser().getValue()user.getName().getValue() という箇所は、デメテルの法則に違反しています。しかし、そうでもしないと、リポジトリでは永続化のために必要な値が取り出せなさそうです。一体どうすればよいのでしょうか。

解決策(1):ドメインオブジェクト自身にデータモデルへの変換メソッドを作成する

筆者が過去に実施した解決策は、ドメインオブジェクト自身に、永続化のためのデータモデルへの変換メソッドを作成するというものでした。

ドメインオブジェクト自身に、永続化のためのデータモデルへの変換メソッドを作成する
public class User {
    private UserId id;
    private UserName name;

    // (略)

    /**
     * 永続化フレームワーク(リポジトリ)向けに、インスタンス自身をデータモデルオブジェクトに変換する
     * @return 永続化のためのデータモデルオブジェクト
     */
    public UserDto toDto() {
        UserDataDto dto = new UserDataDto();
        dto.setId(id.getValue());
        dto.setName(name.getValue());
        return dto;
    }
}

しかし、この方法は書籍では触れられていませんでした。おそらく、ドメインオブジェクトがデータモデルオブジェクトの存在を知っているという構造は、ドメイン駆動設計的には不適切なのだと思います。

解決策(2):通知オブジェクトを使う

書籍で解説されていたのは、「通知オブジェクトを使う」という方法でした。具体的には下記の3つの手順になります。

  • 通知インタフェースを追加
  • 通知インタフェースを実装した通知オブジェクトを追加
  • ドメインオブジェクトに、通知オブジェクトを受け取るメソッドを追加
通知インタフェース
public interface IUserNotification {
    IUserNotification id(UserId id);
    IUserNotification name(UserName name);
}
通知インタフェースを実装した通知オブジェクト
public class UserDataDtoBuilder implements IUserNotification {
    private UserId id;
    private UserName name;

    @Override
    public IUserNotification id(UserId id) {
        this.id = id;
        return this;
    }

    @Override
    public IUserNotification name(UserName name) {
        this.name = name;
        return this;
    }

    public UserDataDto build() {
        UserDataDto dto = new UserDataDto();
        dto.setId(id.getValue());
        dto.setName(name.getValue());
        return dto;
    }
}
通知オブジェクトを受け取るメソッド
public class User {
    private UserId id;
    private UserName name;

    // (略)

    /**
     * オブジェクトの内部データを通知する
     * @param 通知オブジェクト
     */
    public void notify(IUserNotification notification) {
        notification.id(id).name(name);
    }
}
変更後のリポジトリ
public class UserRepository {
    public void save(User user) {
        UserDataDtoBuilder builder = new UserDataDtoBuilder();
        user.notify(builder);
        UserDataDto dto = builder.build();

        // ...以下、保存処理
    }
}

しかし、解決策(1)などと比べて、コードの量が増えてしまうことがデメリットです。このデメリットを緩和するためには、関連するコードの自動生成ツールなどがあれば用意されていればよいという話でした。

感想

「外部からの集約に対する操作はすべて集約ルートを経由しておこなう」 というルールならびにデメテルの法則を守ることは、オブジェクト間の整合性を維持したり、コードの凝集性を改善するのに役立つということが理解できました。

しかし、リポジトリによる永続化のコードの問題に対する解決策(2)は、その長所をあまり理解できませんでした。

確かに解決策(2)のほうが、確かにドメイン駆動設計的には適切なコードです。しかし、コードの記述量が増えるというデメリットもあります。
このため、プロジェクトの開発環境によっては、教条主義的に特定の解決策にこだわらなくてもよいのではないか、と感じました。

なお、本Chapterの後半は「集約をどのように区切るか」などについて解説されています。できればこちらも、読んだ内容をもとに記事にしたいと思っています。

9
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
4