💡

複雑さを解消しようと思ったら複雑になった件

2023/03/09に公開

初めまして、所謂ベンチャーでフロントエンドエンジニアをやっております。白色と申します。
今回はDDD(ドメインモデル貧血症)のお話をしようと思います。

今回実装したコード
https://github.com/fuuki12/ddd_typescript

引用
https://scrapbox.io/kawasima/ドメインモデル貧血症

ドメインモデル貧血症とは

ドメイン層っぽいデータをもつクラスを作りながらも、フィールドのGetter/Setterだけをつものを指す。そうしちゃうとドメインオブジェクトに業務知識が実装されず、それを使う側に任されることになる。

複雑さを解消してみる

  • 複雑なコード(貧血モデル)
 public class Task {
     @Getter @Setter
     private Long id;
     @Getter @Setter
     private TaskStatus taskStatus;
     @Getter @Setter
     private String name;
     @Getter @Setter
     private LocalDate dueDate;
     @Getter @Setter
     private int postponeCount;
     public static final int POSTPONE_MAX_COUNT = 3;
 }
  • ドメイン知識を実装したモデル
 public class Task {
     @Getter
     private Long id;
     @Getter
     private final String name;
     @Getter
     private LocalDate dueDate;
     private TaskStatus taskStatus;
     private int postponeCount;
     public static final int POSTPONE_MAX_COUNT = 3;
 
     public Task(String name, LocalDate dueDate) {
         if (name == null || dueDate == null) {
             throw new IllegalArgumentException("必須項目が設定されていません");
         }
         this.name = name;
         this.dueDate = dueDate;
         this.taskStatus = TaskStatus.UNDONE;
         this.postponeCount = 0;
     }
 
     public void postpone() {
         if (postponeCount >= POSTPONE_MAX_COUNT) {
             throw new IllegalArgumentException("最大延期回数を超過しています");
         }
         dueDate = dueDate.plusDays(1L);
         postponeCount++;
     }
 
     public void done() {
         this.taskStatus = TaskStatus.DONE;
     }
 
     public boolean isUndone() { return this.taskStatus == TaskStatus.UNDONE; }
     public boolean canPostpone() { return this.postponeCount < POSTPONE_MAX_COUNT; }
 }

こちらの通りオブジェクトに業務のふるまいを持たせることでドメインモデルの複雑さは解消できます。
ですが、以下をご覧ください

  • 3つの状態掛け合わせ
    タスクステータス
    タスクの優先度
    タスクの延期回数

このようなmodelが状態ごとに作成される

export class DoneLowerPriorityTask implements DoneTask {
  private readonly _id: number;
  private readonly _name: string;
  private readonly _priority: string;
  private readonly _postponeCount: number;
  private readonly _status: string;
}

確かにドメインモデルは綺麗になったのですが、Factoryに複雑さが集中してしまいました。
今回の場合状態が3つでこの複雑さなのですが、4つ5つと増えていく毎にさらにドメインオブジェクトへの変換処理が複雑になっていきます。

export class TaskFactory {
  /**
   * パターンに応じてDBの値からタスクを再生成する
   * MEMO: ここに複雑性が集中している
   *
   * @param id
   * @param name
   * @param priority
   * @param postponeCount
   * @param status
   * @return
   */

  public static reconstructUndoneTask(
    id: number,
    name: string,
    priority: string,
    postponeCount: number,
    status: string
  ) {
    if (priority == "high") {
      return UndoneHighPriorityTask.reconstruct(
        id,
        name,
        priority,
        postponeCount,
        status
      );
    } else if (priority == "middle" && postponeCount == 3) {
      return UndoneMiddlePriorityDeadlineTask.reconstruct(
        id,
        name,
        priority,
        postponeCount,
        status
      );
    } else if (priority == "low" && postponeCount < 3) {
      return UndoneLowPriorityDeadlineTask.reconstruct(
        id,
        name,
        priority,
        postponeCount,
        status
      );
    } else {
      throw new Error();
    }
  }

まとめ

  • ドメインモデルは状態毎にクラスを用意するのではなく、必要不必要を判断した上で切り分けを行うことが大事
  • このアプローチではインフラモデルからドメインモデル変換処理が複雑になっていく

Discussion