Interactive

Domain層

ビジネスルールと不変条件をカプセル化する層です。弊社ではすべてのドメインオブジェクトをimmutableとして実装し、データとロジックを同じクラスにまとめます。

Entityの実装

データとロジックを分離せず、クラス内にカプセル化します。状態を変更するメソッドは新しいインスタンスを返します。

export class UserEntity {
  constructor(props: {
    id: string
    email: EmailEntity
    name: string
  }) {
    Object.assign(this, props)
    Object.freeze(this)
  }

  withEmail(email: EmailEntity): UserEntity {
    if (this.email.equals(email)) {
      throw new Error('同じメールアドレスには変更できません')
    }
    return new UserEntity({ ...this, email })
  }
}

値オブジェクト

ビジネス的な意味を持つ値はプリミティブ型ではなく値オブジェクトとして表現します。コンストラクタでバリデーションを行い、不正な値の生成を防ぎます。

export class MoneyEntity {
  constructor(props: {
    amount: number
    currency: string
  }) {
    if (props.amount < 0) {
      throw new Error('金額は0以上である必要があります')
    }
    Object.assign(this, props)
    Object.freeze(this)
  }

  add(other: MoneyEntity): MoneyEntity {
    if (this.currency !== other.currency) {
      throw new Error('異なる通貨の加算はできません')
    }
    return new MoneyEntity({
      amount: this.amount + other.amount,
      currency: this.currency
    })
  }
}

集約ルート

集約ルートは整合性を保ちたいデータのまとまりをEntityとして定義したものです。同じテーブルに対しても、守るべき不変条件が異なれば別の集約ルートを作ります。

例えば「組織」に対して、メンバー管理と課金管理では守るべき整合性が異なります。メンバーの追加では上限人数との整合性を、プラン変更では契約状態との整合性を保証する必要があるため、それぞれ別の集約ルートにします。

// メンバー管理の集約ルート
export class OrganizationMemberEntity {
  constructor(props: {
    id: string
    name: string
    memberCount: number  // JOINしてフラットに
    maxMembers: number
  }) {
    Object.assign(this, props)
    Object.freeze(this)
  }

  canAddMember(): boolean {
    return this.memberCount < this.maxMembers
  }
}

// 課金管理の集約ルート
export class OrganizationBillingEntity {
  constructor(props: {
    id: string
    plan: string
    stripeCustomerId: string
    expiresAt: Date
  }) {
    Object.assign(this, props)
    Object.freeze(this)
  }

  canUpgrade(): boolean {
    return this.plan === 'free'
  }
}

Entityはフラットに保つ

Entityのプロパティはプリミティブ型や値オブジェクトとし、他のEntityを入れ子にしません。関連データはJOINしてフラットに展開します。集約ルートごとに必要なデータだけを持つことで、構造がシンプルになりRepository層での変換も容易になります。

ドメインサービス

複数のエンティティにまたがる処理はドメインサービスとして実装します。メソッド名はexecuteで統一します。

export class TransferService {
  execute(props: {
    from: Location
    to: Location
    item: Item
    quantity: Quantity
  }) {
    const updatedFrom = props.from.withDecrease(
      props.item, props.quantity
    )
    const updatedTo = props.to.withIncrease(
      props.item, props.quantity
    )
    return { from: updatedFrom, to: updatedTo }
  }
}