先日、DevLOVEで発表した「コードで学ぶドメイン駆動設計入門」ですが、入門としながらも難しかったかもしれません。モデリングの話は難しい方の話題なので仕方ないのですが、できるだけわかりやすく補足するブログを書いてみたいと思います。
まず、レイヤードアーキテクチャの話ですが、こちらのスライドを参照してください。
DEVLOVE HangarFlight で話したスライド&ソースコード - じゅんいち☆かとうの技術日誌
平たく言うと、そのアプリケーションが解決する問題の領域がドメインです。ドメインとそれ以外のものをごっちゃにしないようにしたのが、ドメイン駆動設計だと考えればよいと思います。
一般的に業務システムでは、対象業務がドメインに成り得ますので、ドメインは業務に準えて語られることが多いと思います。ドメインに登場する概念をユビキタス言語*1として定義し、モデルに落としこむというのが設計の大まかな流れです。
また、業務システム以外の開発ツールにもドメインはあります。例えば、コンパイラのドメインは何でしょうか。コンパイル、つまり「ある命令を構文解析や意味解析などを用い、別の命令(機械語など)に変換すること」に関する問題の領域です。それ以外の画面にコンパイルの進捗を表示したり、ファイルにコンパイル結果を出力するのは、直接関係のないドメイン以外の責務と捉えます。上記のエントリで紹介しているschema-generatorも「スキーマの生成」に関する問題の領域がドメインです。
つまるところ、アプリケーションは何かしらのドメインのために存在しています。「アプリケーションの本懐はドメインにあり」と言っても過言ではないということです。
エンティティ
まず、ドメイン層のオブジェクトから考えていきますが、ここではイメージしやすいように実際のソースを示しながら解説します。
ドメイン層のオブジェクトに含まれるエンティティ*2と、バリューオブジェクトから解説します。
/** * エンティティを表すインターフェイス。 */ public interface Entity<T extends Entity<T>> extends Cloneable { /** * エンティティの識別子を取得する。 * * @return {@link EntityIdentifier} */ EntityIdentifier<T> getIdentifier(); /** * エンティティの{@link #getIdentifier() 識別子}を用いて、このエンティティの同一性を比較する。 * * @param that 比較対象オブジェクト * @return 同じ識別子を持つ場合は{@code true} */ boolean equals(Object that); /** * このエンティティのハッシュコードを返す。 * <p>Effective Java 第二版 項目9に従い、equalsメソッドを * オーバーライドするときは必ずhashCodeメソッドもオーバーライドする。</p> * * @return ハッシュコード */ int hashCode(); /** * このエンティティの複製を生成する。 * * @return このエンティティの複製。 */ T clone(); }
エンティティは、識別=見分けることが目的のモデルなので、EntityIdentifier型のidentifierプロパティを、見分けるための”身元”(アイデンティティとも言う)を表す識別子として持ちます。その他の属性には関心がないため、インターフェイスとしては規定しません。また、エンティティの同一性判断はequalsメソッドで行い、識別子が同一かどうかで判断されます。
たとえば、以下のような上記のEntityインターフェイスを実装した製品(Product)エンティティがある場合に、p1とp2のインスタンスで識別子以外の属性が異なるのに、equalsメソッドではtrueを返します。これは同じ識別子であるため同一と判定されます。
// 第一引数が製品の識別子、第二引数が製品の価格 Product p1 = new Product(1L, 100); // implements Entity<Product> Product p2 = new Product(1L, 105); // implements Entity<Product> p1.equals(p2); // trueとなるのがエンティティ。 // つまり、p1.getIndentifier().equals(p2.getIndentifier())が成り立つかどうかで同一性を判定している。
この例でいう価格の属性、つまり識別子以外の属性に関心がない理由は、エンティティの目的が識別だけにあるからです。
DDDの原書に「5歳の頃の私と同じだろうか」という下りがありますが、これは年齢や身長などの属性は変化していくが、私という個人としてのアイデンティティは変わらないという意味です。たとえば、夏目漱石は、生まれたときから「夏目漱石」というアイデンティティは存在し、この世から亡くなった後も「夏目漱石」というアイデンティティは存在し続けるのです。この例でもわかるように、実はエンティティはライフサイクルを持つオブジェクトです。ライフサイクルをDDDでどのように扱うかは、別のエントリにします。
身元といえば、日常でよくある「本人確認」という行為です。本人確認は、便宜上、名前や住所などの複数の属性が、完全に一致するかどうかで判定します。しかし、本来、このような属性は、ライフサイクルの途中でイベントが発生し変化していくものです。たとえば、引越しで住所変更や結婚で苗字が変わるなど。つまり、属性は流転*3すると言えます。
少し乱暴なことを言うと、同じマンションの部屋に同姓同名の人が引越してきた場合、属性で識別する方法だと見誤ってしまう(識別の間違い)可能性もあります。このような事は稀なので、実用上問題がないかもしれません。しかし、多くの情報を扱い、高い論理性を問われるソフトウエアでは、流転していく属性を頼りに識別するのは危うい方法だとわかります。それは、オブジェクトの識別を間違い、致命的なデータ汚染を招くことを意味します。少し説明が長くなりましたが、このような理由で、エンティティでは属性で識別せずに、一意となる識別子で識別するのです。
エンティティでは、識別子以外の属性には関心がありませんが、ドメインオブジェクトとして必要な属性を自由に定義できます。たとえば、以下のような顧客(Customer)というエンティティがあるとしたら、顧客名としてStringのnameプロパティを持つのが自然でしょう。
public final class Customer implements Entity<Customer> { @Override public EntityIdentifier<Customer> getIdentifier(){ ... } // setterを定義してはならない。不変属性でなければならない。 @Override public boolean equals(Object that){ ... } @Override public int hashCode(){ ... } @Override public Customer clone(){ ... } // 識別子以外の属性を定義する public String getName(){ ... } public void setName(String name){ ... } // setterがある可変属性 }
エンティティの識別子は、ライフサイクルを通じて不変(変わらないこと)でなければならないので、setterなど識別子を書き換えるメソッドは定義しないほうがよいでしょう。
final classとしている理由は、実装継承を許すことで継承の階層が深くなることを防止する意図があります。継承の階層を深くすると、コードが複雑になる傾向だからです。また、エンティティとしての同一性を担保するために、getIdentifierやequals, hashCodeの振る舞いを変更できないようしています。場合によってはこれらのメソッドだけfinalメソッドにしてもよいでしょう。
上記のCustomerのnameプロパティはsetNameメソッドがあるため、可変属性です。値はライフサイクルの途中で変化します。この属性が変わっても、Customerの識別子であるidentifierプロパティが不変であれば、このエンティティはいつでも識別することができます。
エンティティの識別子と可変属性はよい組み合わせですが、可変であることは別の問題を引き起こします。いわゆる 可変と共有の問題です。可変属性を持つオブジェクトである「可変オブジェクト」は、アプリケーション内のあちこちで共有すると、不具合などで意図しない属性の更新が発生した場合に、その原因を特定することが困難です。詳しくは、Effective Java 第二版 項目15あたりを参照。
エンティティは、ドメインの状態を保持する非常に重要なオブジェクトであり、アプリケーション内で共有されるオブジェクトなので、この問題とは切っても切れません。では、どのように解決するとよいでしょうか。典型的な解決方法はEntityにcloneを実装することです。cloneによって複製したインスタンスを共有するようにすればよいでしょう。
public final class Customer implements Entity<Customer> { ... @Override public Customer clone(){ try { return (Customer) super.clone(); } catch (CloneNotSupportedException e) { throw new Error("clone not supported"); } } ... } public static void main(String[] args){ Customer c = new Customer("kato"); Customer c1 = convertUpperCase(c); // 文字列が更新されてしまう。 Customer c2 = convertUpperCase(c.clone()); // クローンしたインスタンスなので更新されても問題はない。 } // 顧客名を大文字表現に変換するメソッド private static Customer convertUpperCase(Customer customer){ customer.setName(customer.getName().toUpperCase()); return customer; }
補足ですが、equals, hashCodeは、java.lang.Objectで実装されているメソッドであるため、実装漏れでもコンパイルエラーになりません。これは次に紹介するValueObjectの例でも同様ですので、注意してください。
バリューオブジェクト
次は、バリューオブジェクトです。
/** * バリューオブジェクトを表すインターフェイス。 */ public interface ValueObject { /** * 全てのプロパティの等価性を用いて、このバリューオブジェクトの等価性を比較する。 * * @param that 比較対象オブジェクト * @return 等価の場合は{@code true} */ boolean equals(Object that); /** * このエンティティのハッシュコードを返す。 * * @return ハッシュコード */ int hashCode(); }
インターフェイスで定義されているメソッドは、equalsとhashCodeだけです。
若干話しが逸れますが、前述した「可変オブジェクト」とは反対に属性がライフサイクルを通じて不変なのが、「不変オブジェクト」です。バリューオブジェクトは「不変オブジェクト」であることが推奨されています。StringやBigDecimalは、このインターフェイスは実装していませんが、バリューオブジェクトです。
バリューオブジェクトでは、保持しているすべての属性が等価かどうかで、そのバリューオブジェクトの等価を判定します。hashCodeをなぜ実装するかは、Effective Java 第二版 項目9を参照してください。
たとえば、下記のような従業員のエンティティでは、従業員の名前に、人の名前(firstNameとlastName)を表すバリューオブジェクト PersonNameクラスを用いています。PersonNameクラスのequalsメソッドでは、firstNameとlastNameの、それぞれの属性が等価かどうかを判定し、すべての属性が等価であればequalsメソッドはtrueを返します。hashCodeメソッドは、それぞれの属性のハッシュコードを用いて、PersonNameのハッシュコードを算出して返します。
// 従業員を表すエンティティの実装 public final class Employee implements Entity<Employee> { private final EntityIdentifier<Employee> identifier; private PersonName name; public Employee(EntityIdentifier<Employee> identifier, PersonName name) { Validate.notNull(identifier); Validate.notNull(name); this.identifier = identifier; this.name = name; } @Override public EntityIdentifier<Employee> getIdentifier() { return identifier; } @Override public Entity clone() { try { return (Entity) super.clone(); } catch (CloneNotSupportedException e) { throw new Error("clone not supported"); } } @Override public int hashCode() { return identifier.hashCode(); } @Override public boolean equals(Object o) { if (this == that) { return true; } if (that == null || o instanceof Entity == false) { return false; } return identifier.equals(((Entity) o).getIdentifier()); } public PersonName getName() { return name; } public void setName(PersonName name) { this.name = name; } } // 人の名前を表すバリューオブジェクトの実装 public final class PersonName implements ValueObject { private final String firstName; private final String lastName; /** * インスタンスを生成する。 * * @param firstName 名 * @param lastName 氏 */ public PersonName(String firstName, String lastName) { Validate.notNull(firstName); Validate.notNull(lastName); this.firstName = firstName; this.lastName = lastName; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } PersonName that = (PersonName) o; if (firstName.equals(that.firstName) == false) { return false; } if (lastName.equals(that.lastName) == false) { return false; } return true; } @Override public int hashCode() { int result = firstName.hashCode(); result = 31 * result + lastName.hashCode(); return result; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } }
次は、エンティティの識別子であるEntityIdentifierとそのデフォルト実装であるDefaultEntityIdentifierです。
/** * エンティティの識別子を表すバリューオブジェクト。 * * @param <T> エンティティの型。コンパイル時だけ利用。 */ public interface EntityIdentifier<T extends Entity<T>> extends ValueObject { /** * この識別子のカインドを取得する。 * <p>通常はエンティティのFQCNが格納されている</p> * * @return カインド */ String getKind(); /** * この識別子を{@link UUID}に変換する。 * * @return {@link UUID} */ UUID toUUID(); } /** * {@link EntityIdentifier}のデフォルト実装。 * * @param <T> エンティティの型。コンパイル時のみ利用。 */ public final class DefaultEntityIdentifier<T extends Entity<T>> implements EntityIdentifier<T> { private String kind; private UUID uuid; /** * インスタンスを生成する。 * * @param entityClass エンティティクラス。カインドにはFQCNが設定される。 * @param uuid UUID * @return {@link DefaultEntityIdentifier} */ public DefaultEntityIdentifier(Class<T> entityClass, UUID uuid) { this(entityClass.getName(), uuid); } /** * インスタンスを生成する。 * * @param kind カインド * @param uuid {@link UUID} */ public DefaultEntityIdentifier(String kind, UUID uuid) { Validate.notNull(kind); Validate.notNull(uuid); this.kind = kind; this.uuid = uuid; } @Override public String getKind() { return kind; } @Override public UUID toUUID() { return uuid; } @Override public int hashCode() { int result = kind != null ? kind.hashCode() : 0; result = 31 * result + (uuid != null ? uuid.hashCode() : 0); return result; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || o instanceof DefaultEntityIdentifier == false) { return false; } DefaultEntityIdentifier that = (DefaultEntityIdentifier) o; if (kind.equals(that.kind) == false) { return false; } if (uuid.equals(that.uuid) == false) { return false; } return true; } }
EntityIdentifierの正体はUUIDですが、それ以外に識別の情報が必要になった場合はUUIDの型では不十分である為、ここではエンティティの識別子専用の型を用意しています。ここでは、コンパイル時の型であったり、カインドというプロパティでクラス名を取っています。デフォルトの実装で事足りない場合は、独自の実装を作るとよいでしょう。
これでひと通り必要なインターフェイスとクラスは登場したのですが、Entityインターフェイスで定義されているメソッドは、以下のような抽象クラスに骨格の実装を前もって定義することが可能です。実際にEntityを実装する際は、AbstractEntityを継承するとよいでしょう。
/** * {@link Entity}の骨格実装。 * * @author j5ik2o */ public abstract class AbstractEntity<T extends Entity<T>> implements Entity<T> { private final EntityIdentifier<T> identifier; protected AbstractEntity(EntityIdentifier<T> identifier) { Validate.notNull(identifier); this.identifier = identifier; } @Override public EntityIdentifier<T> getIdentifier() { return identifier; } @Override public T clone() { try { return (T) super.clone(); } catch (CloneNotSupportedException e) { throw new Error("clone not supported"); } } @Override public int hashCode() { return identifier.hashCode(); } @Override public boolean equals(Object that) { if (this == that) { return true; } if (that == null || that instanceof Entity == false) { return false; } return identifier.equals(((Entity) that).getIdentifier()); } }
// AbstractEntityを継承して実装したEmployeeクラス public final class Employee extends AbstractEntity<Employee> { private PersonName name; public Employee(EntityIdentifier<Employee> identifier, PersonName name) { super(identifier); Validate.notNull(name); this.name = name; } public PersonName getName() { return name; } public void setName(PersonName name) { this.name = name; } }
ひとまず、このエントリではここまで。
ここでは、ドメインといっても、属性だけで振る舞いがないただのデータクラスとしての側面しか触れていません。次は振る舞いとサービスの話をします。
補足:
schema-generatorでもここで紹介したインターフェイスと同等のインターフェイスがあります。ほぼ同じ仕様なのですが、再帰的ジェネリックス(T extends EntityやT extends ValueObject)を使い、sameIdentityAsやsameValueAsの引数に具体的な実装型を指定できるようにしています。これはこれで便利なのですが、その為だけにTの型パラメータを利用するのは面倒かもしれないという考え方に基づき採用していません。
https://github.com/tricreo/schema-generator/blob/master/src/main/java/jp/tricreo/schemagenerator/domain/model/Entity.java
https://github.com/tricreo/schema-generator/blob/master/src/main/java/jp/tricreo/schemagenerator/domain/model/ValueObject.java
補足の補足:
EntityIdentifier
JiemamyではEntityRefというクラスを採用していますが、私のほうでは識別子にその役割を持たせる方針になっています。要は、依存関係の爆発を防ぐには参照を直接保持するのではなく、識別子を持って、後に紹介するリポジトリから間接的に参照を取得する戦略のほうが有利ということです。詳しくは別のエントリで。
補足の補足の補足:
ここではあまり振る舞いについて言及していません。サービスと一緒に語った方がよいと思ったからです。次のサービスのエントリも一緒に読んでもらったほうがよいと思います。
あわせて読みたい
コードで学ぶドメイン駆動設計入門 〜振る舞いとサービス編〜 - じゅんいち☆かとうの技術日誌
コードで学ぶドメイン駆動設計入門 〜ファクトリ編〜 - じゅんいち☆かとうの技術日誌
コードで学ぶドメイン駆動設計入門 〜リポジトリ編〜 - じゅんいち☆かとうの技術日誌
コードで学ぶドメイン駆動設計入門 〜アグリゲート編〜 - じゅんいち☆かとうの技術日誌