単純すぎるドメイン駆動設計Javaサンプルコード (2) 仕様追加その1


前述のDDDらしいコードの作成では、手続き型設計のコードをレイヤ化アーキテクチャで分割し、足し算アプリの中心部分である足し算の処理を、ドメイン層を隔離しました。

見方によっては、むしろ記述量やクラス数が増えて、わかりにくくなった印象を持たれるかもしれません。それは今回の題材がDDDを取り入れるには単純すぎるのが原因です。

ドメインがより複雑になったり、仕様の追加・変更が何度も発生すると、DDDの効果が出てくるでしょう。

例として、実はこの足し算アプリには以下の要件があることが新たに判明したとします。

  • このアプリは小学校の算数の教材として使用するものであり、正の整数のみを扱うものとする

このとき、手続き型設計の場合とDDDの場合で、どのような違いが生まれるかを見ていきます。

手続き型設計の場合

例えば、以下のように修正することになりそうです。安易にチェック処理を追加しました。

public class TransactionAddition02 {

    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);
        }
        // チェック処理を追加
        if (param1 < 0 || param2 < 0) {
            System.err.println("負の数は扱いません");
            System.exit(1);
        }

        // 処理実行
        int result = param1 + param2;

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

DDDの場合

ドメイン層

ドメインモデルの AdditionElement に事前条件を追加します。

public class AdditionElement {

    private final int value;

    public AdditionElement(int value) {
        // 事前条件を追加
        if (value < 0) {
            throw new IllegalArgumentException("負の数は扱いません:" + 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());
    }
}

「足し算の要素は負の数を扱わない(0または正の整数である)」というドメイン知識を表現しています。

「足し算の要素」というドメインモデルに、ドメイン知識が凝集していることがポイントです。これにより「足し算の要素」の振る舞いや条件が、このクラスさえ読めばわかるようになります。これはコードの規模が大きくなるほど、可読性として重要になってきます。

なお、今回の仕様変更では、ドメイン層以外の層は何も変更する必要がありません、と言いたかったところですが、 AdditionElement が例外を投げるようになったので、そのハンドリングのみ以下のように追加しました。

ユースケース層

public class AdditionService {

    public AdditionService(){}

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

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

            return result.getValue();
        // 例外ハンドリングを追加
        } catch (IllegalArgumentException e) {
            throw new AdditionServiceException(e);
        }
    }
}

UI層

public class DDDAddition02 {

    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);
        }

        // アプリケーションサービスを生成・実行
        int result = 0;
        try {
            AdditionService service = new AdditionService();
            result = service.execute(param1, param2);
        // 例外ハンドリングを追加
        } catch (AdditionServiceException e) {
            System.err.println("足し算サービスでエラーが発生しました");
            e.printStackTrace();
            System.exit(1);
        }

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