読者です 読者をやめる 読者になる 読者になる

かとじゅんの技術日誌

技術の話をするところ

結構大事なアグリゲートについてもう少し考えてみる

ちと、小難しい話になります。DDDの話なんで、

コードで学ぶドメイン駆動設計入門 〜アグリゲート編〜 - じゅんいち☆かとうの技術日誌

でも取り上げたアグリゲートに関する考察です。どうあるべきかはちょっと分かっていません。これを読んでこうしたらよいのでは?と思う方、意見歓迎です。インターネットって一方だと面白くないので議論したいですね。

アグリゲートってライフサイクルの境界を扱うための設計パターンです。ライフサイクルで思い出すのはリポジトリとファクトリ。この二つのオブジェクトが扱うのがアグリゲートです。ライフサイクルは至るところに出てくるので、結構大事で、設計の根幹に影響するオブジェクトのひとつではないかと思います。

アグリゲートは、内部のエンティティやバリューオブジェクトを集約している境界で、その境界はエンティティであり、ルートエンティティと呼ばれる。

グローバルな同一性を持つエンティティ(集約ルート)

例えば、何度も登場している以下のような従業員エンティティは、グローバルな同一性を保証する識別子と、それ以外名前や属性を集約しています。つまり、アグリゲートです。Employeeのように、集約している外側のグローバルなエンティティを、集約ルートや、ルートエンティティと呼ぶらしい。ここでは集約ルートに統一します。

// Employeeはアグリゲートパターン。
public class Employee {
    private final UUID id; // グローバルな同一性を保証する識別子
    private String name; // VOを集約している
    private Department department; // VOを集約している

    public Employee(UUID id){
        this.id = id;
    }

    public UUID getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Department getDepartment() {
        return department;
    }

    public void setDepartment(Department department) {
        this.department = department;
    }

    // hashCode, equalsは省略
}

ローカルな同一性を持つローカルエンティティ

集約ルート内部だけで、同一性を保証する識別を持つローカルなエンティティもあります。なので、エンティティ=アグリゲートという訳ではないですね。
たとえば、以下のようなモデル。

// Carエンティティ(集約ルート)
public class Car implements Cloneable {
	private final String id; // グローバルな同一性を保証する識別子
	private Map<Position, Tire> tires = new HashMap<Position, Tire>();

	public Car(String id) {
		Validate.notNull(id);
		this.id = id;
	}

	public String getId() {
		return id;
	}

	public void addTire(Tire tire) {
		tires.put(tire.getLocalId(), tire);
	}

	public Set<Tire> getTires() {
		Set<Tire> result = new HashSet<Tire>();
		for (Tire t : tires.values()) {
			result.add(t.clone());
		}
		return result;
	}

	@Override
	public int hashCode() {
		return id.hashCode();
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (getClass() != obj.getClass()) {
			return false;
		}
		Car other = (Car) obj;
		return id.equals(other.id);
	}

	@Override
	public Car clone() {
		try {
			Car result = (Car) super.clone();
			Map<Position, Tire> old = result.tires;
			result.tires = new HashMap<Position, Tire>();
			Set<Entry<Position, Tire>> entrySet = old.entrySet();
			for (Entry<Position, Tire> entry : entrySet) {
				result.tires.put(entry.getKey(), entry.getValue().clone());
			}
			return result;
		} catch (CloneNotSupportedException e) {
			throw new Error(e);
		}
	}

}
// Tireローカルエンティティ
public class Tire implements Cloneable {
	private final Position localId; // ローカルな同一性を保証する識別子
	private String name;

	public Tire(Position localId) {
		this.localId = localId;
	}

	public Position getLocalId() {
		return localId;
	}

	@Override
	public int hashCode() {
		return localId.hashCode();
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (getClass() != obj.getClass()) {
			return false;
		}
		Tire other = (Tire) obj;
		return localId.equals(other.localId);
	}

	@Override
	public Tire clone() {
		try {
			return (Tire) super.clone();
		} catch (CloneNotSupportedException e) {
			throw new Error(e);
		}
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getName() {
		return name;
	}
}

そして、グローバルなエンティティ(集約ルート)はそもそもVMのライフサイクルを超えうる存在なので、永続化するための手段としてリポジトリが対応づくのが一般的。ローカルなエンティティやバリューオブジェクトは集約されるので、グローバルなエンティティに対するリポジトリが永続化するはず。
アグリゲートでライフサイクルを管理する境界として扱われます。なので、リポジトリで永続化されるときはひとまとめで扱われる。トランザクションもこの単位でなければなりません。

employeeRepository.store(employee); // nameやdepartmentも一緒に永続化される
carRepository.store(car); // tiresも一緒に永続化される

集約ルートが集約ルートの参照を持つ場合

集約ルート(グローバルなエンティティ)が集約ルート(グローバルなエンティティ)の参照を保持している場合が、結構ややこしいのかなと。
最初はこんなコードが思いつきましたが、集約ルートが集約ルートの参照を保持していることは、親子関係の参照とはわけが違う気がします。複製は「子供のことは、親がちゃんと不変条件をみてあげますよ」という意味合いがあると思うのです。だから、この場合は複製はよくないのでは?と

public class Car implements Cloneable {
	private final String id; // グローバルな同一性を保証する識別子
	private Map<Position, Tire> tires = new HashMap<Position, Tire>();
	private Engine engine;

	public Car(String id, Engine engine) {
		Validate.notNull(id);
		Validate.notNull(engine);
		this.id = id;
		setEngine(engine); // 複製
	}

	public String getId() {
		return id;
	}

	public void addTire(Tire tire) {
		tires.put(tire.getLocalId(), tire);
	}

	public Set<Tire> getTires() {
		Set<Tire> result = new HashSet<Tire>();
		for (Tire t : tires.values()) {
			result.add(t.clone());
		}
		return result;
	}

// hashCode, equalsメソッドは省略

	@Override
	public Car clone() {
		try {
			Car result = (Car) super.clone();
			result.engine = engine.clone(); // 複製
			Map<Position, Tire> old = result.tires;
			result.tires = new HashMap<Position, Tire>();
			Set<Entry<Position, Tire>> entrySet = old.entrySet();
			for (Entry<Position, Tire> entry : entrySet) {
				result.tires.put(entry.getKey(), entry.getValue().clone());
			}
			return result;
		} catch (CloneNotSupportedException e) {
			throw new Error(e);
		}
	}
	public void setEngine(Engine engine) {
		this.engine = engine.clone(); // 不変条件を維持するために複製を取り込む
	}

	public Engine getEngine() {
		return engine.clone(); // 不変条件を維持するために複製を返す
	}
}

集約ルートはグローバルな同一性を持っていて、不変条件は集約ルート単位で管理されるべきと考えたら、複製しないという解釈なのかもしれないと思った。

public class Car implements Cloneable {
	private final String id; // グローバルな同一性を保証する識別子
	private Map<Position, Tire> tires = new HashMap<Position, Tire>();
	private Engine engine; // これは集約とは違う

	public Car(String id, Engine engine) {
		Validate.notNull(id);
		Validate.notNull(engine);
		this.id = id;
		this.engine = engine; // そのまま参照を持つ
	}

	public String getId() {
		return id;
	}

	public void addTire(Tire tire) {
		tires.put(tire.getLocalId(), tire);
	}

	public Set<Tire> getTires() {
		Set<Tire> result = new HashSet<Tire>();
		for (Tire t : tires.values()) {
			result.add(t.clone());
		}
		return result;
	}

// hashCode, equalsメソッドは省略

	@Override
	public Car clone() {
		try {
			Car result = (Car) super.clone();
			Map<Position, Tire> old = result.tires;
			result.tires = new HashMap<Position, Tire>();
			Set<Entry<Position, Tire>> entrySet = old.entrySet();
			for (Entry<Position, Tire> entry : entrySet) {
				result.tires.put(entry.getKey(), entry.getValue().clone());
			}
			return result;
		} catch (CloneNotSupportedException e) {
			throw new Error(e);
		}
	}

	public void setEngine(Engine engine) {
		this.engine = engine;
	}

	public Engine getEngine() {
		return engine;
	}
}

この場合のリポジトリの責務は以下のようになると思います。CarRepositoryはengineを永続化しないはず。Carのengineは永続化対象外のtransient的な扱いかなと。

CarRepository carRepository = new CarRepository();
carRepository.store(car); // Engineの永続化は行わない
EngineRepository engineRepository = new EngineRepository();
engineRepository.store(car.getEngine()); // EngineはEngineRepositoryで永続化する

まぁ、これだと以下のようなエンティティの参照を取得する時に困ってしまうので、

CarRepository carRepository = new CarRepository();
Car car = carRepository.resolve(製造番号); // engineはどうすんのさ?

CarRepository内部で、EngineRepository.resolve(エンジンの製造番号) に委譲すればよいかもしれない。
以下はオンメモリのリポジトリなので、cars.put(car.getId(), car)するとcar.engineも一緒に保存されてしまうのですが、永続化対象外と考えてEngineRepositoryに委譲する設計にしてみました。CarとEngineの関連を保存するマップengineIdsを持っていますが、リポジトリはステートフルなんでよいかなと思っています。

public class CarRepository {

	private final EngineRepository engineRepository;
	private final Map<String, Car> cars = new HashMap<String, Car>();
	private final Map<String, String> engineIds = new HashMap<String, String>(); // CarとEngineの関連を保存するマップ

	public CarRepository(EngineRepository engineRepository) {
		this.engineRepository = engineRepository;
	}

	public Car resolve(String id) {
		Engine engine = engineRepository.resolve(engineIds.get(id)); // Engineを読み込む
		Car car = cars.get(id).clone(); // DBならここでselectして、new Car(id, engine)のようなコードになる。
		car.setEngine(engine); // car.engineに設定する
		return car;
	}

	public void store(Car car) {
		cars.put(car.getId(), car.clone()); // car.engineは保存されないという前提。DBならDaoにinsert or update
		engineIds.put(car.getId(), car.getEngine().getId()); // 関連を保存
		engineRepository.store(car.getEngine()); // EngineRepositoryにcar.engineの永続化を委譲
	}

	// 他のメソッドは省略

}

DDDとしてどういう風に考えるのが正しいかな。。難しいですね。という感じで考えてみました。他によい案あれば歓迎。

並行処理環境で考えると、CarはEngineの不変条件を考慮しないので、スレッドセーフにするには若干注意が必要な側面がありますね。たぶん、ひとつのスレッドの単位で各エンティティが独立したインスタンスであればスレッドセーフにできるはずなので、次回はその点を考えてみます。

追記:
Car#cloneでTireが暴露していたので修正。