りゅうじの学習blog

学習したことをアウトプットしていきます。

6/4 ドメイン駆動設計 Chapter12

集約とは

  • オブジェクト指向プログラミングでは複数のオブジェクトがまとめられ、一つの意味を持ったオブジェクトが構築される
    • オブジェクトのグループには維持されべき不変条件が存在する
      • オブジェクトのデータ変更の操作を無制限に受け入れてしまうと不変条件を維持するのが難しくなるので、秩序が必要!
        • 集約により、不変条件を維持する単位として切り出し、オブジェクトの操作に秩序をもたらします!
  • 集約には、境界とルートが存在する
    • 境界とは、集約に何が含まれるかを定義するためのもの
    • ルートは集約に含まれる特定のオブジェクトのこと
      • 外部からの集約に対する操作は全て、集約ルートを経由して行われる
        • 集約の境界内のオブジェクトを外部にさらけ出さないことで、集約内の不変条件を維持できるようにしている
  • 集約は難しい概念ではなく、これまでに出てきた、UserやCircleといったオブジェクトは集約にあたる

12-1-1 集約の基本的構造

集約は関連するオブジェクト同士を線で囲う境界として定義される

Userの集約を図で言うとこのようになります

  • 集約の外部から境界内のオブジェクトを操作してはいけない
    • 操作するための直のインターフェースは集約ルートと呼ばれるオブジェクトに限定される
      • これにより、集約内部の不変条件は維持される

例として、UserNameを直接操作していいのは、集約ルートであるUserのみです

ユーザ名の変更はUserオブジェクトに依頼する形で変更しなくてはならない

const userName = new UserName('NewName');

// NG
user.name = userName;

// OK
uesr.changeName(userName);
  • それぞれやっていることは同じだが、メソッドだと引数の値をnullチェックなどの確認が行える

12-1-2 オブジェクトの操作に関する基本的な原則

  • オブジェクト同士が無秩序にメソッドを呼び出し合うと、不変条件を維持するのが難しくなる

デメテルの法則

メソッドを呼び出すオブジェクトは次の4つに限定される

例として、車を運転するときにタイヤに対して直接命令しないのと同じで、オブジェクトのフィールドに直接命令するのではなく、それを保持するオブジェクトに対して命令を行い、フィールドは保持しているオブジェクト自身が管理するべきだということ。

"オブジェクトはその直接の友人のみと交流すべきであり、それ以外のオブジェクトとは交流すべきではない"という原則を表します。これは、オブジェクト間の結合度を低く保つことで、変更の影響を局所化し、システムの保守性を向上させることを目指しています。

以下に、TypeScriptでのデメテルの法則違反の例と、それを改善した例を示します。

悪い例

class User {
  userName: UserName;
  // ...
}

class UserName {
  value: string;
  // ...
}

class UserPrinter {
  printUserName(user: User) {
    console.log(user.userName.value);
  }
}
  • UserPrinterクラスがUserクラスの内部のUserNameクラスの詳細(valueプロパティ)まで知っているため、デメテルの法則に違反しています。

 良い例

class User {
  userName: UserName;
  // ...

  getUserNameValue(): string {
    return this.userName.value;
  }
}

class UserName {
  value: string;
  // ...
}

class UserPrinter {
  printUserName(user: User) {
    console.log(user.getUserNameValue());
  }
}
  • UserクラスにgetUserNameValueメソッドを追加し、UserPrinterクラスはこのメソッドを通じてユーザー名を取得します。これにより、UserPrinterクラスはUserクラスの内部詳細(UserNameクラスのvalueプロパティ)を知る必要がなくなり、デメテルの法則が守られます。

12-3 集約の大きさと操作の単位

  • トランザクションデータをロックする
    • 集約が大きくなると比例してロックする範囲も大きくなってしまう
      • 処理が失敗する可能性を高めてしまう
        • 集約はなるべく小さく保つべきであり、大きくなってしまったのら境界線を見つめ直すチャンスです

12-4 言葉との齟齬をなくす

サークルのメンバーは30名までというルールのisFullメソッドがあった場合の例

class Circle {
  constructor(private owner: User, private members: User[]) {}
  public isFull(): boolean {
    return this.members.length >= 29;
  }
}
  • このコードは間違っていない、membersとは他にサークルのオーナーのユーザが別のフィールドで保持されているため、30から1を引いて29という数で比較している
    • コードには問題がないが、言葉の齟齬が生まれる、開発者が読んだときに誤解を招く
class Circle {
  constructor(private owner: User, private members: User[]) {}
  public isFull(): boolean {
    return this.members.length >= 30;
  }
  public membersLength(): number {
    return this.members.length + 1;
  }
}
  • 言葉の齟齬をなくすために、メンバーの数を数えるメソッドを追加するといい

参考

ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本