単純すぎるドメイン駆動設計Javaサンプルコード (1) DDDらしいコード


前述のコードをDDDらしく修正するうえで、最初に行うのはドメインの隔離です。

足し算アプリの中心となる部分は、もちろん足し算です。前述のコードの中では以下の部分が該当します。この部分をドメインとして扱うことになります。

int result = param1 + param2;

レイヤ化アーキテクチャ

前述のコードの処理内容を、以下の3層+ドメイン層のレイヤ化アーキテクチャで分割します。

  • UI層 (プレゼンテーション層と呼ばれることもあります)
  • ユースケース層 (アプリケーション層と呼ばれることもあります)
  • インフラストラクチャ層
  • ドメイン層

DDDとセットでよく使われるアーキテクチャには、オニオンアーキテクチャ、ヘキサゴナルアーキテクチャ、クリーンアーキテクチャなど複数ありますが、上記のような各層のネーミングや定義がそれぞれ微妙に異なります。ただ、ドメイン層を隔離する、即ち、ドメイン層は他のどの層にも依存しないようにする、というのが基本的な考え方だと思います。

UI層

この層では、UIに依存する処理を行ったうえで、実際の足し算については、後述のユースケース層のメソッドを呼び出します。

この例では、コマンドラインのUIとしているため、UIに依存する以下の処理を行います。

  • コマンドライン引数からの入力値の取得とパース
  • 結果を標準出力

コマンドライン引数の代わりに、ファイルやDBを入力としたり、WebAPIでリクエストを受け取ったり、画面から入力したり、のようにUIを変えたければ、この層を変更することになります。

public class DDDAddition01 {
    public static void main(String[] args) {
        // 引数チェック
        if (args.length != 2) {
            System.err.println("引数を2つ指定してください");
            System.exit(1);
        }
        int param1 = 0;
        int param2 = 0;
        try {
            param1 = Integer.parseInt(args[0]);
            param2 = Integer.parseInt(args[1]);
        } catch (NumberFormatException e) {
            System.err.println("引数には整数を指定してください");
            System.exit(1);
        }

        // アプリケーションサービスを生成し実行
        AdditionService service = new AdditionService();
        int result = service.execute(param1, param2);

        // 結果を表示
        System.out.printf("%s + %s = %s%n", param1, param2, result);
    }
}

ユースケース層

この層では、ユースケースごとにUI層から呼び出されるメソッドを定義します。

ここでは「足し算を行う」というユースケースに対応するexecuteメソッドを用意しました。

ユースケースが増えた場合(「足し算の履歴を表示する」等)は、それに対応するメソッドまたはサービスを追加していくことになります。

この層の役割は、ユースケースに応じてドメイン層をオブジェクトを操作することであり、具体的な処理内容はドメイン層のオブジェクトに委譲し、ユースケース層自身の処理は薄く保つように努めます。

public class AdditionService {

    public AdditionService(){}

    public int execute(int int1, int int2){
        // ドメインオブジェクトを生成
        final AdditionElement e1 = new AdditionElement(int1); // 値オブジェクト
        final AdditionElement e2 = new AdditionElement(int2); // 値オブジェクト

        // ドメインオブジェクトから結果を取得
        final AdditionElement result = e1.plus(e2); // 値オブジェクトのメソッド戻り値も値オブジェクト

        return result.getValue();
    }
}

ドメイン層

この層ではじめて、足し算という本アプリの中心となる部分を、コードによって表現します。

ここでは、「足し算の要素」というドメインモデルを AdditionElement というオブジェクトで表し、 AdditionElementplus メソッドで、「足し算の要素どうしを足すことができる」という振る舞いを表現しています。

なお、足し算の結果もまた「足し算の要素」として他の要素と足すことができるため、 plus メソッドの戻り値もまた AdditionElement 型としています。

public class AdditionElement {

    private final int value;

    public AdditionElement(int value) {
        this.value = value;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof AdditionElement)) return false;
        AdditionElement that = (AdditionElement) o;
        return value == that.value;
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }

    public int getValue() {
        return this.value;
    }

    public AdditionElement plus(AdditionElement element) {
        return new AdditionElement(this.value + element.getValue());
    }
}

この AdditionElement は、DDDにおいて「値オブジェクト」に分類されるモデルとなります。

値オブジェクトの特徴・条件はいくつかありますが、特に重要な特徴は、値オブジェクトは不変(immutable)であるということです。

不変であることが保証されることによってプログラムの保守性・安定性が高まります。例えば、「1」という値がどこかのタイミングで中身が「2」に変わってしまう可能性があったら(意味がわからないですね)、その値がどこでどう変わったか/変わっていないかを常に気にかけないといけなくなってしまいます。不変であれば、そのような余計な心配・確認が不要となります。

ここでは、 value プロパティにfinal修飾子をつけ、setterメソッドのような value を変更可能なメソッドも公開しないことによって、不変であることを保証しています。

また、本オブジェクトの等価性を定義するために、 equalshashCode メソッドをオーバーライドし、2つの AdditionElement インスタンスについて、 value が同じであれば同じ値とみなせることとしています。 この部分の実装有無は今回の例ではアプリの実動作には影響がありませんが、少なくともテストコード作成時には値の比較がしやすくなります。

インフラストラクチャ層

この時点では、インフラストラクチャ層とするような内容がないため割愛します。後述する仕様追加の対応の際に登場します。