DDD モデルの型定義 - jmolecules -

はじめに

この記事は、以下の記事の補足説明です。

blog1.mammb.com


集約ルートの指定と型制約

エンティティが、別の集約ルートに属するエンティティとコンポジションを構成してはならない。例えば以下のような注文(集約ルート)と注文明細があった場合、

@Entity
public class Order {
    @OneToMany
    private List<LineItem> items;
    // ...
}

@Entity
public class LineItem {
    // ...
}

外部の集約ルートである顧客エンティティが注文明細の参照を持ってはならない。

@Entity
public class Customer {
    @ManyToOne
    private LineItem lineItem;
}

これを防ぐために、jmolecules では AggregateRootEntity インターフェースに以下のような型引数が設けられる。

public interface Entity<T extends AggregateRoot<T, ?>,                                             ID>
                extends Identifiable<ID> {
}
public interface AggregateRoot<T extends AggregateRoot<T, ID>,
                               ID extends Identifier>
                        extends Entity<T, ID> {
}

これにより、LineItem の属する集約ルートは、(AggregateRoot である)Order であることを明示することができる(AggregateRoot ではないエンティティは集約ルートとして指定できない型制約にもなっている)。

public class Order implements AggregateRoot<Order, OrderId> { ... }
public class LineItem implements Entity<Order, Long> { ... }

以下のような Customer を定義した場合は、単体テストなどでリフレクションを使用することで他の集約ルート配下のエンティティを参照している場合にテストを失敗とすることが可能となる。

public class Customer implements AggregateRoot<Customer, CustomerId> {
    private LineItem lineItem;
}


リポジトリに対する制約

DDD では、集約ルートに対してリポジトリを提供し、その他のエンティティに対してリポジトリを提供しない。

jmolecules では以下のリポジトリインターフェースを提供しており、対象のエンティティが AggregateRoot であることを型制約として課している。

public interface Repository<T extends AggregateRoot<T, ID>,
                            ID extends Identifier> {
}


外部の集約ルート参照に対する制約

OrderCustomer がいずれも集約ルートであった場合、以下のように OrderCustomer の参照を持つべきではない。

@Entity
public class Order {
    @Id
    private Long id;
    @OneToMany
    private List<LineItem> lineItems;
    @ManyToOne
    private Customer customer;
}

外部の集約ルートを参照する場合、以下のようなバリューオブジェクトを用意し

@Embeddable
public class CustomerId {
    @Column(name = "customer_id")
    private Long customerId;
}

以下のように CustomerId を保持することが推奨される。

@Entity
public class Order {
    @Id
    private Long id;
    @OneToMany
    private List<LineItem> lineItems;
    @Embedded
    private CustomerId customerId;
}

jmolecules の AggregateRoot では、ID値は ID extends Identifier のようになっており、Identifier の実装であることが課せられる(Identifier 単純なマーカーインターフェース)。

public interface AggregateRoot<T extends AggregateRoot<T, ID>,
                               ID extends Identifier>
                        extends Entity<T, ID> {
}


集約ルートの Customer は以下のような定義となり、

public class Customer implements AggregateRoot<Customer, CustomerId> {
    @Embedded @Id
    private CustomerId id;
    // ...
}

@Embeddable
public record CustomerId(UUID id) implements Identifier {}

Order 側では CustomerId をフィールド値として保持する。

public class Order implements AggregateRoot<Order, OrderId> {
    // ...
    @Embedded
    private CustomerId customer;
    // ...
}

直接 Customer の参照を持つことを防ぐには、こちらも単体テストなどでリフレクションを使い、フィールドの型が AggregateRoot のサブタイプの場合にテストを失敗とすることが可能となる。

外部の集約ルートの参照には Association を使うことができる。Association については以下を参照。

blog1.mammb.com