【日記】DDD的な設計完全に理解したメモ【プログラミング】

マサカリ避け

本読んでないです。
経験則です。
DDDチョットデキル人になりたいです。

前書き

「大量の相関チェック(しかもDBアクセスなど層をまたぐ処理や複数オブジェクト間の相関チェックを含む)とその後の登録が発生する状況」を想定して書きます。
言語はJava(SpringBoot)想定です。

最近作っているプロダクトで、DDD的な考えに触れて自分が考えたことを設計に取り込んだ経験から書きます。

ドメインを見極める

ドメインとは、複雑になりそうな処理内容を切り出したものです。
想定した状況の場合、まずバリデーション処理は物凄く複雑になるでしょう。

この「複雑な何か」、「まともにやりあったら複雑すぎてコードと自分が爆発四散しそうなもの」がドメインです。
ここではドメインが明確になったものとして話を続けます(でもDDDで一番むずかしいのはドメインの見極めだとは思います)。

実現する

次に実際の処理の作り方です。

まず書いてみる

どんな処理であっても、何を実現するのか、そのために何が必要なのかを理解するために努力していれば、「何かこうやったらうまくいきそう」という想像ができると思います。
そのぼんやりした想像を、図に書いたり、コードに書いたり、何でもいいから書き出してみて、その上で分からないことが有れば聞いたりはっきりさせていきながら書いたり捨てたりしていくのが良いでしょう。
そうして「処理とフィールドの塊」を作っていきます。

今回はドメインの持つべきフィールドの内容が以下の3クラスにまとまったものとして話を進めます(ゲッターや内部処理等は省略)。
内容は、DomainRootがDomainChildとDomainNodeを持っているというものです。

public class DomainRoot {
    private final DomainChild domainChild;
    private final DomainNode domainNode;
}
public class DomainChild {
    private final ChildValueObject childValueObject;
}
public class DomainNode {
    private final NodeValueObject nodeValueObject;
}

そうして全体の構造をはっきりさせていくと、今度はドメイン内部で用いる処理が3種類に分けられるようになります。

  • 単体のオブジェクト内で処理が完結する内容
  • DomainRoot内の情報をDomainChildやDomainNodeが必要とする内容
  • レイヤーをまたぎ、ドメインオブジェクト外に処理を書く必要がある内容

この内、単体のオブジェクト内で完結しない内容について具体的な実装のコツを書きます。

単体のオブジェクト内で完結しない内容をどう書くか

個人的には、以下3つの方法があると思っています。

  • 関数型インターフェースで処理を受け取る
  • インターフェースを用意してその実装を受け取る
  • abstractクラスを作り、その実装を受け取る

この内まず試すべきなのが関数型インターフェースで処理を受け取る方法で、数が多かったり、上の方のクラスで継承するなどの理由が有ればインターフェースを用意してその実装を受け取る方法に順次シフトするのが良いと思います。

abstractクラスを使うやり方は、Javaでは多重継承ができないなどの制約が有り、そもそも要求する内容はフィールド等を要求しないため、やらない方がいいと思います。

DomainRoot内の情報をDomainChildやDomainNodeが必要とする内容

DomainChild.childValueObjectの検証にDomainNode.nodeValueObjectの内容が必要な場合を、 関数型インターフェースで処理を受け取る例で書きます。

public class DomainChild {
    private final ChildValueObject childValueObject;
    // 外部に公開しないように保持する
    // この関数をコンストラクタで渡してもらう
    private final Predicate<ChildValueObject> predicate;

    // バリデーション関数
    private boolean isValid() {
        return predicate.test(childValueObject);
    }
}

ここで要求される関数を、DomainRoot側では以下のように実装します。

public class DomainRoot {
    private final DomainChild domainChild;
    private final DomainNode domainNode;

    public DomainRoot() {
        // 引数や途中の処理は省略

        this.domainChild = new DomainChild(
                childValueObject,
                it -> /* domainNodeの絡むような相関チェック */;
        )
    }
}

重要なのは、外の内容を必要とする(DomainChild)側は「引数を渡したら検証結果が返ってくる」ような関数を要求することです。
これによって、DomailChildは外の処理を知らずにそれが正しいかを検証することができるようになります。
また、この相関チェックは本質的にルートが知っておくべき処理なので、それをDomainRootに書くこともできています。

レイヤーをまたぎ、ドメインオブジェクト外に処理を書く必要がある内容

レイヤーをまたぎ、ドメインオブジェクト外に処理を書く必要がある内容を、インターフェースを用意してその実装を受け取る例で書きます。

具体的には、以下のように実装します(前節で追記した内容は書くと邪魔になるので排除しています)。

public class DomainChild {
    public interface Delegates {
        HogeDto getHogeDto(Integer childId);
    }

    private final Delegates delegates; // 移譲はコンストラクタで受け取る
    private final ChildValueObject childValueObject;
}
public class DomainNode {
    public interface Delegates {
        FugaDto getFugaDto(Integer nodeId);
    }

    private final Delegates delegates; // 移譲はコンストラクタで受け取る
    private final NodeValueObject nodeValueObject;
}
public class DomainRoot {
    // ルート側では特にインターフェースを要求しなかった場合を想定
    public interface Delegates extends DomainChild.Delegates, DomainNode.Delegates {
    }

    private final DomainChild domainChild;
    private final DomainNode domainNode;

    public DomainRoot(Delegates delegates) {
        domainChild = new /* delegatesを渡す */;
        domainNode = new /* delegatesを渡す */;
    }
}

これによって、「ドメインに対してその他処理が依存している状態」を作り出すことができます。
補足として、今回は「このオブジェクトはDBから取ってくるものだ」ということを強調するために『なんたらDto』というような名前を付けていますが、これに関しては必要な情報をドメイン層に定義した上でそれをDBから取ってくるという形としなければ、「ドメインに対してその他処理が依存している状態」にはなりません。

なぜこう書くときれいだと言えるのか

ここまで長々と書いてきましたが、今書いたコツは一見するとかなり複雑なことをやっているように見えます。
なぜこう書くときれいだと言えるのか、自分なりの答えは以下3点にまとまります。

  • どのドメインオブジェクトでも「そのオブジェクトの中でやるべきこととそうでないこと」が明確に分かれている
  • どのドメインオブジェクトでもDBなど「Javaの外側の世界」/「具体的な実装」を意識せずに実装できている
  • どのドメインオブジェクトも使い方・使われ方が明示されているため、それに従うだけで処理を生やしていくことができる

細かく言い出すとキリが有りませんが、対象の処理が複雑なのにこれらが成り立っていない部分の有る設計は大体どこかで破綻すると思います。

まとめ

まとまってませんが、とりあえずこれを意識して設計することで、以下のような利点が有ると思っています。

  • 外側の変更に強いシステムを作ることができる
  • 「どこに何を書くか問題」がドメイン別に整理されることでスッキリする
  • ドメインファーストで設計することで、複雑さを切り離して楽になることができる