Help us understand the problem. What is going on with this article?

DDD自分用覚え書き

はじめに

  • DDD本を読んで噛み砕いた理解をメモで残します(随時更新)。
  • 検証可能性から、なるべく意見は引用からにしています。
  • 実装例は、DDDの概念とは別にJavaの言語仕様から決めている面もあるかと思います。
  • コード例は、WunderlistのようなTodoアプリを想定しています。

途中雑感

  • domain modelとdomain serviceの説明を読んでいると、ビジネスルールを熟知することが問題の切り分けにつながる事だと強く感じる。
    • 銀行という施設が持つべき概念としての中枢処理(domain model, repository, factory)
      • 口座残高以上に出金できないだとか
      • 投資商品の金利がいくらだとか
    • 銀行員の業務(domain service)
      • 振替処理をするだとか、組戻処理をするだとか
    • 銀行員の生産性を上げるアプリケーションユースケース(application service)
      • 円や日本語でやり取りするとか、帳票をXMLで得るだとか
    • 一般的なシステムが持つインフラサービス(infra service)
      • 振り込み完了をメールで通知するだとか

んん、Todoアプリは例題が悪い気がしてきた。Todoの場合、入力するか削除するかしかなく、計算は殆どない。唯一コレクションオブジェジェクトが持つべき計算が、DueDateを昇順でソートしたり、Favorite(true)を集めて前方に、残りを後方に並べたり。Taskの消化率を計算で出したりくらい。Todo一つで何かを計算するような責務がない。

layer

+-- domain
|    |
|    +-- model
|    |    |
|    |    +-- Entity -- Aggregate
|    |    |
|    |    `-- Value Object
|    |
|    +-- Factory
|    |
|    +-- Repository ( command , query )
|    |
|    +-- Service
|    |
|    `-- ( shared )
|
+-- infrastructure - service
|    |                |
|    |                `-- ( mail sender )
|    |
|    `-- ( Repository Implementation )
|         |
|         +-- ( db I/O )
|         |
|         +-- ( file I/O )
|         |
|         `-- ( cache I/O )
|
+-- application -- Service
|
`-- interfaces
     |
     +-- ( controller )
     |    |
     |    `-- ( REST )
     |          |
     |          +-- ( XML )
     |          |
     |          `-- ( JSON )
     |
     +-- ( view )
     |    |
     |    `-- ( HTML )
     |
     `-- ( socket )


domain model

参考図書:

Entity

  • 特徴
    • ライフサイクルを通して不変の識別子を持っている。
    • 同一性は、識別子によって確認する。
    • 識別子以外の属性は可変である。
    • 原則、属性の操作はsetterではなく、ビジネスロジック経由である。
    • equals(), hashCode()は自分で実装する
Entity概要
// 識別子以外の属性は可変
//  このクラスでは属性同士に独立性があるので@Setterを指定している
//  属性同士に関連性が生まれた時、このクラスのビジネスロジックを経由して属性操作を行うこと
@Getter
@Setter
public final class Todo extends AbstractEntity<Todo> implements ValueObject {

    // (AbstractEntity::private final UUID identifier)
    private Title title;
    private DueDate dueDate;
    private TaskList taskList;
    private Memo memo;
    private Favorite favorite;

    public Todo(final EntityIdentifier<Todo> identifier, final Title title, final DueDate dueDate, final TaskList taskList, final Memo memo, final Favorite favorite) {
        super(identifier);
        Validate.notNull(title);
        Validate.notNull(dueDate);
        Validate.notNull(taskList);
        Validate.notNull(memo);
        Validate.notNull(favorite);

        this.title = title;
        this.dueDate = dueDate;
        this.taskList = taskList;
        this.memo = memo;
        this.favorite = favorite;
    }

    @Override
    public Todo clone() {
        return super.clone();
    }

    @Override
    public boolean equals(final Object that) {
        if (this == that) {
            return true;
        }
        if (that == null || getClass() != that.getClass()) {
            return false;
        }
        Todo o = (Todo) that;
        if(!entityIdentifier.equals(o.entityIdentifier)){
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        int result = Objects.hashCode(entityIdentifier);
        result = 31 * result;
        return result;
    }
}

  • 同一性を使ってオブジェクトを追跡したい(=ライフサイクルを通して不変の識別子を持たせる)
  • Entityの同一性とは
    • Javaの==演算子で比較する同一性とも違う
      • この同一性の仕組みは他のアプリケーションドメインではほとんど意味をなさない。(Evans No.2348-2349)
      • メモリ上で同一性を実現する仕組みであり、メモリの外に出てしまうと同一性がなくなってしまう
    • 同一性は、エンティティの持つ巧妙で意味のある属性であり、プログラミング言語の持つ自動化された機能には引き継げないのである。(Evans No.2349-2350)
    • ライフサイクルを通じた連続性を持ち、アプリケーションのユーザにとって重要な区別が属性から独立してなされるもの(Evans No.2339-2340)
  • 同一性=identityに基づく等価性、等価性=価値の等しさ
  • 通常は実体という意味で使われるエンティティでもない。(Evans No.2339)
  • ケースによってはEntityだったりValueObjectだったり
    • 単語の概念上に区切りが有るわけではなく、モデルの中での扱われ方で決める
    • 通販会社のソフトウェアでは、クレジットカードを確認し、小包の宛先とするのに住所が必要である。しかし、同居人も同じ会社に注文したとしても、両者が同じ場所に住んでいると気づくことは重要でない。この場合、住所は値オブジェクトである。(Evans No.2500-2502)
    • 郵便サービスのソフトウェアは、配達経路を体系化するために、国は地方、都市、郵便区、街区からなる階層形式となり、これは個々の住所にまで至る。これらの住所オブジェクトは、階層における親から郵便番号を導き出すのであり、郵便サービスが郵便区を割り当てなおすことにしたら、その中のすべての住所が一緒に移動する。ここでは、住所はエンティティだ。(Evans No.2503-2506
  • 識別子を不変とすることで同一性が担保されている。よって属性は書き換えられる
  • あるオブジェクトが属性ではなく同一性によって識別されるのであれば、モデルでこのオブジェクトを定義する際には、その同一性を第一とすること。(Evans No.2363-2364)

Value Object

  • 特徴
    • 属性は不変である。
    • Listなどに内包されるオブジェクトも不変である。
    • 属性を内部から書き換えるList#add()などの利用後は、自オブジェクトを新規作成して返す
    • 等価性は、属性を突合して確認する。
    • 場合によっては、属性を可変とすることもある。
      • [注意] factoryの項を参照
    • equals(), hashCode()は自分で実装する
ValueObject概要
// ---- 基本はこれ --------------------------

// 属性は不変
@Getter
public final class Task implements ValueObject {
    private final String value;
    private final Boolean done;

    public Task(final String value) {
        Validate.notNull(value);

        this.value = value;
        this.done = false;
    }

    public Task(final String value, final Boolean done) {
        Validate.notNull(value);
        Validate.notNull(done);

        this.value = value;
        this.done = done;
    }

    @Override
    public boolean equals(final Object that) {
        if (this == that) {
            return true;
        }
        if (that == null || getClass() != that.getClass()) {
            return false;
        }
        Task o = (Task) that;
        if (!value.equals(o.value)) {
            return false;
        }
        if (!done.equals(o.done)) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        int result = Objects.hashCode(value);
        result = 31 * result + Objects.hashCode(done);
        return result;
    }
}



// ---- 子にValue Objectを持つ --------------------------

// 属性は不変
//  しかし、属性がListなどのオブジェクトの場合は工夫が必要。
public final class TaskList implements ValueObjectList<TaskList, Task> {
    private final List<Task> taskList;

    public TaskList() {
        taskList = Collections.unmodifiableList(new ArrayList<>());
    }

    public TaskList(final List<Task> list) {
        Validate.notNull(list);

        // unmodifiableList()の理由は、
        // 参照渡しで返すList<Task>の中身を書き換えられないようにし、
        // 書き換えたい場合はコンストラクタの利用を強制したいから。
        // 要素の中までfinal宣言は効かないので、宣言的な意味でもこの位置で利用している。
        taskList = Collections.unmodifiableList(list);
    }

/*
  Collections.unmodifiableList()を使う理由は、
  使わない場合は下記テストコードが通らず、属性不変を担保できないから。

  使う場合は taskList.set(0, after) でUnsupportedOperationExceptionが発生するので、
  要素を書き換えたい場合は、ArrayList<>(taskList) をTaskListのコンストラクタに渡すことで、
  要素を書き換えつつ属性は不変(=コンストラクタ利用なので属性の違う別オブジェクト)を担保できる。

        TaskList target = new TaskList();

        Task before = new Task("牛乳を買う", false);
        target.add(before);
        assertEquals(1, target.getList().size());

        // 取り出したリストの1つ目を別のオブジェクトで上書き
        List<Task> taskList = target.getList();
        Task after = new Task("牛乳を売る", true);
        taskList.set(0, after);

        // 上書き前に確保した変数beforeに上書き分が反映されないこと
       assertEquals(before.getFavorite(), target.getList().get(0).getFavorite());
 */

    private static void accept(final Task w) {
    }

    public Optional<Task> find(final Task searchTask) {
        Validate.notNull(searchTask);

        Optional<Task> ret = Optional.empty();

        for (Task existTask : getList()) {
            if (existTask.equals(searchTask)) {
                ret = Optional.of(existTask);
                break;
            }
        }

        return ret;
    }

    public List<Task> getList() {
        return taskList;
    }

    public TaskList add(final Task... t) {
        Arrays.stream(t).forEach(Validate::notNull);

        List<Task> result = new ArrayList<>(taskList);
        Arrays.stream(t).forEach(
                v -> find(v).ifPresentOrElse(
                        TaskList::accept,
                        () -> result.add(v)
                )
        );

        return new TaskList(result);
    }

/*
  自分自身を生成しなおして返している理由は、
  内部属性がCollections#unmodifiableList()で不変化されているので変更できない。

  注意点としては、List#add(Object):booleanのつもりで使うと、thisは更新されないので新しい値を受け取れない。
*/

    public TaskList remove(final Task... t) {
        Arrays.stream(t).forEach(Validate::notNull);

        List<Task> result = new ArrayList<>(taskList);
        Arrays.stream(t).forEach(
                v -> find(v).ifPresent(result::remove)
        );

        return new TaskList(result);
    }

    @Override
    public boolean equals(final Object that) {
        if (this == that) {
            return true;
        }
        if (that == null || getClass() != that.getClass()) {
            return false;
        }
        TaskList o = (TaskList) that;
        if (!taskList.equals(o.taskList)) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        int result = Objects.hashCode(taskList);
        result = 31 * result;
        return result;
    }
}
  • 原則、属性やその中身は書き換えない。使い捨てるべし。
    • 顧客データの中の同姓同名の人物2名について、Value Objectでは同姓同名なら1つのオブジェクトを2人が共有してもいいとされる。しかし、、
    • 片方の名前が変更された場合、もう一方の人の名前も変わってしまうかもしれない!これを防いで、オブジェクトを安全に共有するためには、オブジェクトは不変でなければならない。これはつまり、完全に置き換える以外、変更することはできないということだ。(Evans No.2533-2535)
  • 場合によっては可変としても良い
    • 値の更新が頻繁な時、オブジェクトの生成や破棄のコストが高い時、インスタンスの置き換えでクラスタリングに問題が出る場合、値を共有しない場合(クラスタリングの改良、その他 技術的な理由の場合も含む)は、可変を許してもよい。(Kato blog)
  • 等価性は全ての属性を突合して担保する
    • Evansには明示的な記述がない・・
  • 値オブジェクトがインスタンス化される際に表現しようとするのは、何であるかだけが問題となり、誰であるか、あるいはどれであるかは問われないような設計の要素である。(Evans No.2483-2484)
  • 値オブジェクトは、他のオブジェクトが組み合わされてできていることもある。(Evans No.2490)
    • 窓(Entity)と窓様式(ValueObject)と様式建材(ValueObject)の関係
    • ※属性が増えてくると一部属性を書き換えたい時コストが爆発する。不変オブジェクトの欠点と言われる。
      • 対処法はValueObjectBuilder が実装されている。
      • DDDにbuilderの概念は無いが、具体的な生成過程を隠蔽するという意味ではfactoryの横に並べてもよさそう。

factory

  • 特徴
    • Entityの生成を請負うオブジェクト
    • メソッドをstaticとするかはコストによって判断する
    • Entityの生成が容易な場合は、作られない場合もある
    • 可変ValueObjectを含むEntityの複製(clone)を請負うこともある
Factory概要
public class TodoFactory implements EntityFactory<Todo> {

    @Override
    public Todo create() {
        return create("");
    }

    @Override
    public Todo create(final EntityIdentifier<Todo> identifier) {
        Validate.notNull(identifier);

        Title t = new Title("");
        DueDate d = new DueDate();
        TaskList l = new TaskList();
        Memo m = new Memo();
        Favorite f = new Favorite();

        return new Todo(identifier, t, d, l, m, f);
    }

    /**
     * {@link Title}を指定して{@link Todo}作成する
     *
     * @param title
     * @return
     */
    public Todo create(final String title) {
        Validate.notNull(title);

        EntityIdentifier<Todo> identifier = new DefaultEntityIdentifier<>(Todo.class, UUID.randomUUID());
        Title t = new Title(title);
        DueDate d = new DueDate();
        TaskList l = new TaskList();
        Memo m = new Memo();
        Favorite f = new Favorite();

        return new Todo(identifier, t, d, l, m, f);
    }
}
  • リポジトリとファクトリは、それ自体はドメインに由来しない(Evans No.3069-3070)
  • ドメインモデルを生成するオブジェクト
    • 焦点をライフサイクルの始まりに合わせ、ファクトリを使用して、複雑なオブジェクトや集約を生成したり再構成したりする。(Evans No.3066-3067)
  • 生成が容易でバリエーションが無い時は用意しなくても良い。
  • ドメインモデルの複製を任せる場合もある
    • cloneメソッドはシャローコピーなので、そこで可変オブジェクトなどを考慮してディープコピーすることを忘れたり、非finalによる初期化漏れの心配もあります。(Kato blog)
      • オーバーライドメソッドを組み込んだcloneメソッドの欠陥の話から出てきている
      • ドメインモデルのcloneメソッドで複雑な複製をしようとするとSRPに違反するだろう、という指摘も兼ねて。
      • が、 『そのケースでない場合』 かつ 『ValueObjectが全て不変オブジェクト』 なら、考えなくてもいい。不変のValueObjectは要素単位の変更を許可していないし、setterを許可していない。よって、『値変更=新しいオブジェクト生成』なので、cloneがシャローコピーであっても値変更後はディープコピー時と同じ結果になる。
      • 『可変のValueObjectを作っている場合』 は考慮すべき点。

repository

  • リポジトリとファクトリは、それ自体はドメインに由来しない(Evans No.3069-3070)
  • ドメインモデルを永続化するオブジェクト
    • リポジトリがライフサイクルの中期と終わりに対応して、永続化されたオブジェクトにアクセスする手段を提供しつつ、アクセスに伴って必要となる巨大なインフラストラクチャをカプセル化する。(Evans No.3067-3069)
  • ドメインモデルを復元するオブジェクト
    • 具体的に何ページ?

aggregation (集約)

  • 集約が区切るスコープによって、不変条件が維持されなければならない範囲が示される。(Evans No.3073-3074)
  • ドメインモデルを合成したオブジェクト
    • 具体的に何ページ?
  • トランザクション境界
    • 具体的に何ページ?

domain service

  • サービスとは、モデルにおいて独立したインタフェースとして提供される操作で、エンティティと値オブジェクトのようには状態をカプセル化しない。(Evans No.2632-2634)
  • 純粋にクライアントに対して何が実行できるかという観点から定義される(Evans No.2636-2637)
  • 引数と結果はドメインオブジェクトであるべき(Evans No.2641)
  • 優れたサービスには3つの特徴がある。(Evans No.2645-2648)
    1. 操作がドメインの概念に関係しており、その概念がエンティティや値オブジェクトの 自然な一部ではない
    2. ドメインモデルの他の要素の観点 からインタフェースが定義されている。
    3. 操作に 状態がない
  • 技術的なサービスにはビジネスに関する意味が一切ないはずである。(Evans No.2673)
    • 銀行の業務用語をモデリングした話から
    • つまり、ビジネスに関する意味がある振る舞いはドメインモデルに寄せるべきだ、という
  • 複数のドメインモデルを扱ってビジネスロジックを実現する場合
    • 資金をある口座から別の口座に振り替える機能はドメインサービスである。(Evans No.2680-2681)
  • ドメインモデルが持つビジネスロジックに相応しくない場合
    • 口座オブジェクトに「振替」の操作を入れては扱いにくくなる。この操作は2つの口座と何らかのグローバルなルールを伴っているからだ(Evans No.2684-2685)
  • ただし、なるべくドメインサービスを作成しないこと
    • 具体的に何ページ?

application service

  • ドメインモデルを利用したアプリケーションのユースケースを実現する場合
    • 例えば、銀行業アプリケーションが、我々が分析できるよう取引を変換してスプレッドシートファイルにエクスポートできるとすると、そのエクスポートはアプリケーションサービスである。銀行業ドメインにおいて「ファイル形式」には意味がなく、ビジネスルールも関係してはいない。(Evans No.2677-2680)
  • 非常に薄い
    • レイヤーを縦断しないという意味かな。上の記述を見るとスプレッドシートを生成するとか全然薄くないので。
    • 具体的に何ページ?

infrastructure service

  • 文献で論じられているサービスのほとんどは純粋に技術的なものであり、こうしたサービスはインフラストラクチャ層に属する。(Evans No.2662-2663)
  • 口座残高が一定限度額を下回ると、顧客に電子メールを送信するかもしれない。この電子メールシステムはインタフェースによってカプセル化され、場合によっては、代わりの通知手段もそこに含まれるかもしれない。このインタフェースが、インフラストラクチャ層のサービスである(Evans No.2664-2667)

interfaces

  • GET/POST/PUT/DELETE その他諸々受けるところ

- socket接続口だったりもここじゃないか?

positrium
テスター業を卒業してSEになるべく派遣で活動中。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away