0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScript・Pythonで実装するDDD戦術的パターン実践ガイド

0
Last updated at Posted at 2026-03-03

TypeScript・Pythonで実装するDDD戦術的パターン実践ガイド

DDD(ドメイン駆動設計)の戦術的パターンは、ビジネスロジックをコードに正確に反映するための設計手法です。この記事では、集約・値オブジェクト・リポジトリの3つの中核パターンを、TypeScriptとPythonの両方で実装しながら解説します。

この記事でわかること

  • 値オブジェクト・エンティティ・集約の違いと、それぞれの実装方法
  • Vaughn Vernonが提唱する集約設計の4つのルールと、違反時に起こる問題
  • TypeScript(Object.freeze + ファクトリメソッド)とPython(frozen dataclass)での値オブジェクト実装
  • リポジトリパターンによるドメイン層と永続化層の分離手法
  • 集約間の結果整合性をドメインイベントで実現する設計

対象読者

  • 想定読者: DDDに興味があるが実装経験の少ないソフトウェアエンジニア
  • 必要な前提知識:
    • TypeScript または Python の基礎文法
    • オブジェクト指向プログラミングの基本概念(クラス、インターフェース、継承)
    • RDB の基本操作(SELECT / INSERT / UPDATE)

MLエンジニアの方へ: DDDはML基盤のAPI設計やデータパイプラインにも応用できます。Pythonの @dataclass(frozen=True) はMLで馴染みのある記法なので、そこからDDDの概念に入りやすいでしょう。

結論・成果

DDDの戦術的パターンを適用すると、以下の効果が報告されています。

  • Vaughn Vernonの調査によると、集約の約70%はルートエンティティと値オブジェクトのみで構成でき、残り30%も2〜3エンティティに収まる(Effective Aggregate Design Part I
  • MicrosoftのAzure Architecture Centerでは、マイクロサービス設計において「1トランザクション=1集約」の原則がスケーラビリティ向上に寄与すると報告されている
  • 値オブジェクトの導入により、バリデーションロジックの重複を排除し、バグの発生箇所を限定できる

値オブジェクトを実装する

値オブジェクトは、DDDの戦術的パターンの中で最も導入しやすいパターンです。Pythonの intstr のように「値そのもの」に意味があり、同じ値なら同一と見なすオブジェクトです。MLエンジニアにとっては、NumPyの配列比較(np.array_equal)に近い考え方です。

値オブジェクトの3つの特性

値オブジェクトには以下の特性があります。

特性 説明
不変性(Immutability) 一度作成したら変更できない Money(100, "JPY") は金額を後から変えられない
構造的等価性(Structural Equality) 全属性が同じなら同一と見なす Money(100, "JPY") == Money(100, "JPY")true
自己検証(Self-Validation) 不正な値では生成できない Money(-1, "JPY") はエラーになる

MLで例えると、torch.tensor([1, 2, 3])torch.tensor([1, 2, 3]) の値が等しいのと同じ発想です。IDで区別する「エンティティ」との違いは後述します。

TypeScriptでの値オブジェクト実装

TypeScriptでは Object.freeze とプライベートコンストラクタを組み合わせて不変性を実現します。

// value-object.base.ts
// 値オブジェクトの基底クラス

interface ValueObjectProps {
  [index: string]: unknown;
}

export abstract class ValueObject<T extends ValueObjectProps> {
  public readonly props: T;

  protected constructor(props: T) {
    // Object.freeze で不変性を保証
    this.props = Object.freeze(props);
  }

  // 構造的等価性: 全プロパティが一致すれば同一
  public equals(vo?: ValueObject<T>): boolean {
    if (vo === null || vo === undefined) return false;
    return JSON.stringify(this.props) === JSON.stringify(vo.props);
  }
}

具体的な値オブジェクト Money を実装してみましょう。

// money.ts
// 金額を表す値オブジェクト

interface MoneyProps {
  amount: number;
  currency: string;
}

export class Money extends ValueObject<MoneyProps> {
  // ファクトリメソッドでバリデーション付き生成
  static create(amount: number, currency: string): Money {
    if (amount < 0) {
      throw new Error("金額は0以上である必要があります");
    }
    if (!["JPY", "USD", "EUR"].includes(currency)) {
      throw new Error(`未対応の通貨: ${currency}`);
    }
    return new Money({ amount, currency });
  }

  get amount(): number {
    return this.props.amount;
  }

  get currency(): string {
    return this.props.currency;
  }

  // 値オブジェクトは新しいインスタンスを返す(不変性を維持)
  add(other: Money): Money {
    if (this.currency !== other.currency) {
      throw new Error("異なる通貨同士の加算はできません");
    }
    return Money.create(this.amount + other.amount, this.currency);
  }
}

なぜファクトリメソッド(static create)を使うのか:

  • コンストラクタを protected にすることで、バリデーションなしの生成を防止する
  • new Money(...) を直接呼べないため、不正な状態のオブジェクトが存在できない
  • Pythonの __init__ でバリデーションするのと同じ意図だが、TypeScriptでは慣習的にファクトリメソッドを使う

Pythonでの値オブジェクト実装

Pythonでは @dataclass(frozen=True) を使います。MLエンジニアには馴染みのある dataclassfrozen=True を付けるだけで不変性が得られます。

# value_objects.py
# Python 3.10+ を想定

from dataclasses import dataclass

@dataclass(frozen=True, slots=True)  # slots=True でメモリ効率改善
class Money:
    """金額を表す値オブジェクト"""
    amount: int
    currency: str

    def __post_init__(self) -> None:
        # frozen=True でも __post_init__ 内では object.__setattr__ で検証可能
        if self.amount < 0:
            raise ValueError("金額は0以上である必要があります")
        if self.currency not in ("JPY", "USD", "EUR"):
            raise ValueError(f"未対応の通貨: {self.currency}")

    def add(self, other: "Money") -> "Money":
        """新しいインスタンスを返す(不変性を維持)"""
        if self.currency != other.currency:
            raise ValueError("異なる通貨同士の加算はできません")
        return Money(amount=self.amount + other.amount, currency=self.currency)


# 使用例
price = Money(amount=1000, currency="JPY")
tax = Money(amount=100, currency="JPY")
total = price.add(tax)  # Money(amount=1100, currency="JPY")

# 構造的等価性(dataclass が自動生成)
assert Money(100, "JPY") == Money(100, "JPY")  # True

# 不変性(frozen=True により代入エラー)
# price.amount = 2000  # FrozenInstanceError

注意: frozen=True の dataclass では、__post_init__ 内で self.amount = ... のような代入はできません。バリデーションのみ行い、値の変換が必要な場合は object.__setattr__(self, 'amount', new_value) を使います。ただし、この手法は不変性の保証を弱めるため、ファクトリメソッド(@classmethod)での生成を推奨します。

よくある間違い: プリミティブ型をそのまま使う

値オブジェクトを使わずにプリミティブ型(string, number)をそのまま使うアンチパターンはプリミティブ執着(Primitive Obsession)と呼ばれます。

// ❌ アンチパターン: プリミティブ執着
function createOrder(
  userId: string,     // ← どんな文字列でも渡せてしまう
  productId: string,  // ← userId と取り違えても型エラーにならない
  price: number,      // ← 通貨が不明、負の値も渡せる
  email: string       // ← メールアドレスのフォーマット検証がない
): void { /* ... */ }

// ✅ 値オブジェクトを使う
function createOrder(
  userId: UserId,         // ← UserId 型でしか渡せない
  productId: ProductId,   // ← ProductId 型なので取り違えない
  price: Money,           // ← 通貨付き、負の値は生成時に弾かれる
  email: EmailAddress     // ← フォーマット検証済み
): void { /* ... */ }

制約: 値オブジェクトを多用するとクラス数が増え、シリアライゼーション(JSON変換)の実装が必要になります。ORMとの統合にも追加の設定が必要です。小規模プロジェクト(エンティティ10個以下)ではオーバーヘッドが利点を上回る場合があります。

エンティティと集約を設計する

エンティティは値オブジェクトとは異なり、一意のIDで識別されるオブジェクトです。MLで例えると、学習済みモデルの各バージョン(model-v1, model-v2)のように、内容が変わってもIDで同一性を追跡するものです。

エンティティと値オブジェクトの比較

比較項目 値オブジェクト エンティティ
同一性の判定 全属性の一致 IDの一致
可変性 不変 可変(IDは不変)
ライフサイクル なし(使い捨て) あり(生成→更新→削除)
Money, Email, Address User, Order, Product

集約とは何か

集約(Aggregate)は、エンティティと値オブジェクトをひとまとめにした「整合性の境界」です。集約の外部からは集約ルート(Aggregate Root)だけにアクセスし、内部のオブジェクトには直接触れません。

MLで例えると、MLパイプラインの「実験」(Experiment)が集約ルート、各「実行」(Run)が子エンティティ、ハイパーパラメータが値オブジェクトに相当します。外部からは実験ID経由でしかアクセスしないのと同じ構造です。

Vernon の集約設計4ルール

Vaughn Vernonは著書『実践ドメイン駆動設計』(通称「赤本」)で、以下の4つのルールを提唱しています(ArchiLab - Aggregate Design Rules)。

# ルール 説明 違反時の問題
1 真の不変条件を整合性境界でモデル化する 1つのトランザクションで必ず一貫性を保つべきデータだけを集約にまとめる 不要なデータまで含めて集約が肥大化する
2 小さい集約を設計する ルートエンティティ + 最小限の値オブジェクト/子エンティティ 集約が大きいとロック競合が発生し、スケーラビリティが低下する
3 他の集約はIDで参照する 直接のオブジェクト参照ではなく、IDで間接参照する 他の集約を変更する誘惑が生まれ、トランザクション境界を侵す
4 境界外では結果整合性を使う 集約間の整合性はドメインイベントで非同期に保つ 複数集約を1トランザクションで更新しようとして、デッドロックやパフォーマンス低下が起こる

Vernonの調査によると、約70%の集約はルートエンティティと値オブジェクトのみで構成でき、残り30%も2〜3エンティティに収まると報告されています(Effective Aggregate Design Part I)。

TypeScriptでの集約実装

ECサイトの注文(Order)を集約として実装してみましょう。

// order.ts
// 注文集約

// --- 値オブジェクト ---
class OrderId extends ValueObject<{ value: string }> {
  static create(value: string): OrderId {
    if (!value || value.length === 0) {
      throw new Error("OrderIdは空にできません");
    }
    return new OrderId({ value });
  }

  get value(): string {
    return this.props.value;
  }
}

class ProductId extends ValueObject<{ value: string }> {
  static create(value: string): ProductId {
    return new ProductId({ value });
  }
  get value(): string {
    return this.props.value;
  }
}

// --- 子エンティティ ---
class OrderLine {
  constructor(
    public readonly productId: ProductId,
    public readonly quantity: number,
    public readonly unitPrice: Money,
  ) {
    if (quantity <= 0) {
      throw new Error("数量は1以上である必要があります");
    }
  }

  get subtotal(): Money {
    return Money.create(
      this.unitPrice.amount * this.quantity,
      this.unitPrice.currency,
    );
  }
}

// --- ドメインイベント ---
interface DomainEvent {
  occurredOn: Date;
}

class OrderPlacedEvent implements DomainEvent {
  constructor(
    public readonly orderId: string,
    public readonly totalAmount: number,
    public readonly occurredOn: Date = new Date(),
  ) {}
}

// --- 集約ルート ---
class Order {
  private lines: OrderLine[] = [];
  private domainEvents: DomainEvent[] = [];

  private constructor(
    public readonly id: OrderId,
    private status: "draft" | "placed" | "cancelled",
  ) {}

  static create(id: OrderId): Order {
    return new Order(id, "draft");
  }

  // 集約ルート経由でのみ明細を操作(外部から直接 lines にアクセスさせない)
  addLine(productId: ProductId, quantity: number, unitPrice: Money): void {
    if (this.status !== "draft") {
      throw new Error("確定済みの注文には明細を追加できません");
    }
    // ビジネスルール: 1注文あたり最大20明細
    if (this.lines.length >= 20) {
      throw new Error("1注文あたりの明細数上限(20)を超えています");
    }
    this.lines.push(new OrderLine(productId, quantity, unitPrice));
  }

  place(): void {
    if (this.lines.length === 0) {
      throw new Error("明細が空の注文は確定できません");
    }
    this.status = "placed";
    // ドメインイベントを記録(後で発行)
    this.domainEvents.push(
      new OrderPlacedEvent(this.id.value, this.totalAmount.amount),
    );
  }

  get totalAmount(): Money {
    if (this.lines.length === 0) {
      return Money.create(0, "JPY");
    }
    return this.lines.reduce(
      (sum, line) => sum.add(line.subtotal),
      Money.create(0, this.lines[0].unitPrice.currency),
    );
  }

  // ドメインイベントの取得と消費
  pullDomainEvents(): DomainEvent[] {
    const events = [...this.domainEvents];
    this.domainEvents = [];
    return events;
  }
}

なぜ addLine を集約ルート経由にするのか:

  • 明細数の上限チェック(20件まで)は注文全体を見ないと判定できない
  • ステータスが draft のときだけ操作可能というビジネスルールを強制できる
  • 外部から order.lines.push(...) を直接呼ばせると、これらの不変条件が守れない

Pythonでの集約実装

# order.py
# 注文集約の Python 実装

from dataclasses import dataclass, field
from datetime import datetime
from typing import Protocol

@dataclass(frozen=True, slots=True)
class OrderId:
    value: str

    def __post_init__(self) -> None:
        if not self.value:
            raise ValueError("OrderIdは空にできません")

@dataclass(frozen=True, slots=True)
class ProductId:
    value: str

@dataclass(frozen=True, slots=True)
class OrderLine:
    product_id: ProductId
    quantity: int
    unit_price: Money

    def __post_init__(self) -> None:
        if self.quantity <= 0:
            raise ValueError("数量は1以上である必要があります")

    @property
    def subtotal(self) -> Money:
        return Money(
            amount=self.unit_price.amount * self.quantity,
            currency=self.unit_price.currency,
        )

# ドメインイベント
@dataclass(frozen=True)
class OrderPlacedEvent:
    order_id: str
    total_amount: int
    occurred_on: datetime = field(default_factory=datetime.now)

# 集約ルート(エンティティなので frozen=False)
class Order:
    def __init__(self, order_id: OrderId) -> None:
        self.id = order_id
        self._lines: list[OrderLine] = []
        self._status: str = "draft"
        self._domain_events: list[object] = []

    def add_line(
        self, product_id: ProductId, quantity: int, unit_price: Money
    ) -> None:
        if self._status != "draft":
            raise ValueError("確定済みの注文には明細を追加できません")
        if len(self._lines) >= 20:
            raise ValueError("1注文あたりの明細数上限(20)を超えています")
        self._lines.append(OrderLine(product_id, quantity, unit_price))

    def place(self) -> None:
        if not self._lines:
            raise ValueError("明細が空の注文は確定できません")
        self._status = "placed"
        self._domain_events.append(
            OrderPlacedEvent(
                order_id=self.id.value,
                total_amount=self.total_amount.amount,
            )
        )

    @property
    def total_amount(self) -> Money:
        if not self._lines:
            return Money(amount=0, currency="JPY")
        total = 0
        currency = self._lines[0].unit_price.currency
        for line in self._lines:
            total += line.subtotal.amount
        return Money(amount=total, currency=currency)

    def pull_domain_events(self) -> list[object]:
        events = list(self._domain_events)
        self._domain_events.clear()
        return events

    # エンティティの等価性: IDで判定
    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Order):
            return False
        return self.id == other.id

    def __hash__(self) -> int:
        return hash(self.id)

ハマりポイント: 集約ルート(Order)は状態が変わるため frozen=True にできません。一方、子エンティティ(OrderLine)や値オブジェクト(Money)は不変にしましょう。「何を可変にするか」の判断基準は「ライフサイクルの中で状態遷移があるか」です。

リポジトリパターンで永続化を分離する

リポジトリパターンは、ドメイン層が永続化の詳細(どのDBを使うか、SQLの書き方など)を知らなくて済むようにするパターンです。MLエンジニアにとっては、MLflowの MlflowClient がモデルの保存先(ローカル/S3/GCS)を抽象化するのと同じ発想です。

リポジトリの基本原則

リポジトリの基本ルールは以下の3つです。

  1. 集約ルートごとに1つ: OrderRepositoryOrder 集約全体を保存/取得する。OrderLine 単独のリポジトリは作らない
  2. インターフェースはドメイン層に定義: 実装はインフラ層に置き、依存関係を逆転させる(DIP: 依存関係逆転の原則)
  3. ドメインオブジェクトを返す: DBのレコードではなく、集約(Order)を返す

TypeScriptでのリポジトリ実装

// order-repository.ts
// リポジトリのインターフェース(ドメイン層に定義)

export interface OrderRepository {
  findById(id: OrderId): Promise<Order | null>;
  save(order: Order): Promise<void>;
  nextId(): OrderId;
}
// prisma-order-repository.ts
// Prisma ORM を使ったリポジトリ実装(インフラ層)

import { PrismaClient } from "@prisma/client";

export class PrismaOrderRepository implements OrderRepository {
  constructor(private readonly prisma: PrismaClient) {}

  async findById(id: OrderId): Promise<Order | null> {
    // 集約全体を1クエリで取得(N+1問題を回避)
    const record = await this.prisma.order.findUnique({
      where: { id: id.value },
      include: { lines: true },  // 子エンティティも一緒に取得
    });
    if (!record) return null;
    return this.toDomain(record);
  }

  async save(order: Order): Promise<void> {
    const data = this.toPersistence(order);
    // Upsert で冪等性を確保
    await this.prisma.order.upsert({
      where: { id: order.id.value },
      create: data,
      update: data,
    });
  }

  nextId(): OrderId {
    // UUIDv4 で一意なIDを生成
    return OrderId.create(crypto.randomUUID());
  }

  // --- マッパー(ドメイン ↔ 永続化の変換) ---
  private toDomain(record: OrderRecord): Order {
    // DB レコードからドメインオブジェクトを再構築
    const order = Order.reconstruct(
      OrderId.create(record.id),
      record.status,
      record.lines.map(
        (l) =>
          new OrderLine(
            ProductId.create(l.productId),
            l.quantity,
            Money.create(l.unitPrice, l.currency),
          ),
      ),
    );
    return order;
  }

  private toPersistence(order: Order): OrderCreateInput {
    return {
      id: order.id.value,
      status: order.status,
      totalAmount: order.totalAmount.amount,
      currency: order.totalAmount.currency,
      lines: {
        // 明細は洗い替え(delete → create)で簡潔に
        deleteMany: {},
        create: order.getLines().map((l) => ({
          productId: l.productId.value,
          quantity: l.quantity,
          unitPrice: l.unitPrice.amount,
          currency: l.unitPrice.currency,
        })),
      },
    };
  }
}

なぜ upsert を使うのか:

  • 冪等性の確保: 同じ save を2回呼んでもエラーにならない
  • リトライ時の安全性: ネットワーク障害で再送されても、データの不整合が起きない
  • 外部I/Oに対して冪等性を設計に含めるのは、分散システムの基本原則

Pythonでのリポジトリ実装

# order_repository.py
# リポジトリインターフェース(ドメイン層)

from abc import ABC, abstractmethod

class OrderRepository(ABC):
    """注文集約のリポジトリインターフェース"""

    @abstractmethod
    def find_by_id(self, order_id: OrderId) -> Order | None:
        ...

    @abstractmethod
    def save(self, order: Order) -> None:
        ...

    @abstractmethod
    def next_id(self) -> OrderId:
        ...
# sqlalchemy_order_repository.py
# SQLAlchemy を使ったリポジトリ実装(インフラ層)

import uuid
from sqlalchemy.orm import Session

class SqlAlchemyOrderRepository(OrderRepository):
    def __init__(self, session: Session) -> None:
        self._session = session

    def find_by_id(self, order_id: OrderId) -> Order | None:
        # joinedload で N+1 問題を回避
        record = (
            self._session.query(OrderModel)
            .options(joinedload(OrderModel.lines))
            .filter(OrderModel.id == order_id.value)
            .one_or_none()
        )
        if record is None:
            return None
        return self._to_domain(record)

    def save(self, order: Order) -> None:
        record = self._to_persistence(order)
        self._session.merge(record)  # merge で upsert 相当
        self._session.flush()

    def next_id(self) -> OrderId:
        return OrderId(value=str(uuid.uuid4()))

    def _to_domain(self, record: OrderModel) -> Order:
        order = Order(order_id=OrderId(value=record.id))
        for line in record.lines:
            order.add_line(
                product_id=ProductId(value=line.product_id),
                quantity=line.quantity,
                unit_price=Money(amount=line.unit_price, currency=line.currency),
            )
        return order

    def _to_persistence(self, order: Order) -> OrderModel:
        return OrderModel(
            id=order.id.value,
            status=order._status,
            lines=[
                OrderLineModel(
                    product_id=line.product_id.value,
                    quantity=line.quantity,
                    unit_price=line.unit_price.amount,
                    currency=line.unit_price.currency,
                )
                for line in order._lines
            ],
        )

よくある問題と解決方法

問題 原因 解決方法
N+1 クエリ問題 集約の子エンティティを遅延ロードしている include(Prisma)/ joinedload(SQLAlchemy)で一括取得
集約の再構築が複雑 コンストラクタがバリデーション付きのため、DBからの復元時にエラーになる reconstruct 用のファクトリメソッドを別途用意する
トランザクション境界が不明確 リポジトリ内でトランザクションを管理してしまう ユースケース層で begin/commit を管理し、リポジトリは save のみ担当
テストが書きにくい リポジトリがDBに依存している インメモリ実装(Map / dict ベース)をテスト用に用意する

制約: リポジトリパターンは、ORMが提供するクエリ機能(JOINの最適化、バッチ処理など)を直接使えなくなるトレードオフがあります。読み取り専用の複雑なクエリには、CQRS(Command Query Responsibility Segregation)を組み合わせて、リポジトリは書き込み側のみに使う方法が実用的です。

集約間の結果整合性をドメインイベントで実現する

Vernonの4つ目のルール「境界外では結果整合性を使う」を実装するには、ドメインイベントが鍵になります。注文が確定したら在庫を減らす、という例で見てみましょう。

// place-order-use-case.ts
// ユースケース層(アプリケーションサービス)

export class PlaceOrderUseCase {
  constructor(
    private readonly orderRepo: OrderRepository,
    private readonly eventBus: EventBus,
  ) {}

  async execute(orderId: string): Promise<void> {
    // 1. 集約を取得
    const order = await this.orderRepo.findById(OrderId.create(orderId));
    if (!order) throw new Error("注文が見つかりません");

    // 2. ビジネスロジックを実行
    order.place();

    // 3. 集約を保存(1トランザクション = 1集約)
    await this.orderRepo.save(order);

    // 4. ドメインイベントを発行(結果整合性)
    const events = order.pullDomainEvents();
    await this.eventBus.publishAll(events);
  }
}

最初は order.place() の中で直接在庫を減らそうと考えるかもしれませんが、これはVernonのルール1・3に違反します。 注文集約が在庫集約のオブジェクト参照を持つと、2つの集約を1トランザクションで更新する誘惑が生まれ、ロック競合やスケーラビリティ低下の原因になります。

トレードオフ: 結果整合性を採用すると、注文確定と在庫減少の間にタイムラグが生じます。この間に別の注文で在庫が0になる可能性があります。これを許容するか、補償トランザクション(在庫不足時に注文をキャンセル)を設計するかは、ビジネス要件によります。

2026年の新しいアプローチ: Dynamic Consistency Boundaries

従来の集約は「クラス単位で固定された境界」でしたが、Sara Pellegriniが提唱するDynamic Consistency Boundaries(DCB)では、操作ごとに動的に境界を定義します(EventSourcingDB - DDD Back to Basics)。

DCBは「この操作で整合性が必要なデータは何か」をイベントストリームから動的に判断するアプローチで、集約クラスに縛られない柔軟性があります。ただし、まだ新しい概念であり、実績のあるプロダクション事例は限定的です。

まとめと次のステップ

まとめ:

  • 値オブジェクト: 不変・構造的等価性・自己検証の3特性を持つ。TypeScriptでは Object.freeze + ファクトリメソッド、Pythonでは @dataclass(frozen=True) で実装する
  • 集約: Vernonの4ルール(真の不変条件、小さく、IDで参照、結果整合性)に従って設計する。集約の70%はルートエンティティ + 値オブジェクトだけで構成できる
  • リポジトリ: 集約ルートごとに1つ作成し、ドメイン層にインターフェース、インフラ層に実装を置く。N+1問題とトランザクション境界に注意する
  • ドメインイベント: 集約間の結果整合性を実現する。1トランザクション = 1集約の原則を守り、集約間の連携はイベント経由で行う

次にやるべきこと:

  • 自分のプロジェクトで最も複雑なビジネスルールを持つエンティティを1つ選び、集約として再設計してみる
  • 値オブジェクトの導入は小さく始める: まず EmailAddressUserId のようなID系から
  • リポジトリのテストは、インメモリ実装(Map / dict)を使って高速に回す

参考


注意: この記事はAI(Claude Code)により自動生成されました。内容の正確性については複数の情報源で検証していますが、実際の利用時は公式ドキュメントもご確認ください。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?