ddd

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Domain-Driven Design

领域驱动设计(Domain-Driven Design,DDD)

Strategic DDD

战略DDD

┌─────────────────┐     ┌─────────────────┐
│  Order Context   │────▶│ Payment Context  │
│  (core domain)   │     │  (supporting)    │
└────────┬────────┘     └─────────────────┘
┌─────────────────┐     ┌─────────────────┐
│ Inventory Context│     │ Notification Ctx  │
│  (supporting)    │     │  (generic)       │
└─────────────────┘     └─────────────────┘
┌─────────────────┐     ┌─────────────────┐
│  Order Context   │────▶│ Payment Context  │
│  (core domain)   │     │  (supporting)    │
└────────┬────────┘     └─────────────────┘
┌─────────────────┐     ┌─────────────────┐
│ Inventory Context│     │ Notification Ctx  │
│  (supporting)    │     │  (generic)       │
└─────────────────┘     └─────────────────┘

Tactical DDD (TypeScript)

战术DDD(TypeScript实现)

Entity

Entity

typescript
class Order {
  private constructor(
    readonly id: OrderId,
    private items: OrderItem[],
    private status: OrderStatus,
    private readonly createdAt: Date,
  ) {}

  static create(items: OrderItem[]): Order {
    if (items.length === 0) throw new DomainError('Order must have at least one item');
    return new Order(OrderId.generate(), items, OrderStatus.PENDING, new Date());
  }

  get total(): Money {
    return this.items.reduce((sum, item) => sum.add(item.subtotal), Money.zero('USD'));
  }

  confirm(): DomainEvent[] {
    if (this.status !== OrderStatus.PENDING) throw new DomainError('Can only confirm pending orders');
    this.status = OrderStatus.CONFIRMED;
    return [new OrderConfirmed(this.id, this.total)];
  }
}
typescript
class Order {
  private constructor(
    readonly id: OrderId,
    private items: OrderItem[],
    private status: OrderStatus,
    private readonly createdAt: Date,
  ) {}

  static create(items: OrderItem[]): Order {
    if (items.length === 0) throw new DomainError('Order must have at least one item');
    return new Order(OrderId.generate(), items, OrderStatus.PENDING, new Date());
  }

  get total(): Money {
    return this.items.reduce((sum, item) => sum.add(item.subtotal), Money.zero('USD'));
  }

  confirm(): DomainEvent[] {
    if (this.status !== OrderStatus.PENDING) throw new DomainError('Can only confirm pending orders');
    this.status = OrderStatus.CONFIRMED;
    return [new OrderConfirmed(this.id, this.total)];
  }
}

Value Object

Value Object

typescript
class Money {
  private constructor(readonly amount: number, readonly currency: string) {
    if (amount < 0) throw new DomainError('Amount cannot be negative');
  }

  static of(amount: number, currency: string): Money {
    return new Money(Math.round(amount * 100) / 100, currency);
  }

  static zero(currency: string): Money { return new Money(0, currency); }

  add(other: Money): Money {
    if (this.currency !== other.currency) throw new DomainError('Currency mismatch');
    return Money.of(this.amount + other.amount, this.currency);
  }

  equals(other: Money): boolean {
    return this.amount === other.amount && this.currency === other.currency;
  }
}
typescript
class Money {
  private constructor(readonly amount: number, readonly currency: string) {
    if (amount < 0) throw new DomainError('Amount cannot be negative');
  }

  static of(amount: number, currency: string): Money {
    return new Money(Math.round(amount * 100) / 100, currency);
  }

  static zero(currency: string): Money { return new Money(0, currency); }

  add(other: Money): Money {
    if (this.currency !== other.currency) throw new DomainError('Currency mismatch');
    return Money.of(this.amount + other.amount, this.currency);
  }

  equals(other: Money): boolean {
    return this.amount === other.amount && this.currency === other.currency;
  }
}

Domain Event

Domain Event

typescript
class OrderConfirmed implements DomainEvent {
  readonly occurredAt = new Date();
  constructor(readonly orderId: OrderId, readonly total: Money) {}
}
typescript
class OrderConfirmed implements DomainEvent {
  readonly occurredAt = new Date();
  constructor(readonly orderId: OrderId, readonly total: Money) {}
}

Repository (Port)

Repository (Port)

typescript
interface OrderRepository {
  findById(id: OrderId): Promise<Order | null>;
  save(order: Order): Promise<void>;
  nextId(): OrderId;
}
typescript
interface OrderRepository {
  findById(id: OrderId): Promise<Order | null>;
  save(order: Order): Promise<void>;
  nextId(): OrderId;
}

Application Service

Application Service

typescript
class ConfirmOrderUseCase {
  constructor(
    private orders: OrderRepository,
    private eventBus: EventBus,
  ) {}

  async execute(orderId: string): Promise<void> {
    const order = await this.orders.findById(OrderId.from(orderId));
    if (!order) throw new NotFoundError('Order', orderId);

    const events = order.confirm();
    await this.orders.save(order);
    await this.eventBus.publishAll(events);
  }
}
typescript
class ConfirmOrderUseCase {
  constructor(
    private orders: OrderRepository,
    private eventBus: EventBus,
  ) {}

  async execute(orderId: string): Promise<void> {
    const order = await this.orders.findById(OrderId.from(orderId));
    if (!order) throw new NotFoundError('Order', orderId);

    const events = order.confirm();
    await this.orders.save(order);
    await this.eventBus.publishAll(events);
  }
}

DDD Building Blocks

DDD构建块

Building BlockPurposeIdentity?Mutable?
EntityDomain object with identityYes (ID)Yes
Value ObjectImmutable descriptorNo (structural equality)No
AggregateConsistency boundaryRoot entity has IDYes (via root)
Domain EventSomething that happenedEvent IDNo
RepositoryPersistence abstractionN/AN/A
Domain ServiceLogic not belonging to entityN/AN/A
构建块(Building Block)用途是否有标识?是否可变?
Entity带唯一标识的领域对象是(ID)
Value Object不可变描述对象否(基于结构相等)
Aggregate一致性边界根实体拥有ID是(通过根实体)
Domain Event领域中发生的事件事件ID
Repository持久化抽象层不适用不适用
Domain Service不属于实体的业务逻辑不适用不适用

Anti-Patterns

反模式

Anti-PatternFix
Anemic domain model (logic in services)Put business logic in entities
Aggregate too largeKeep aggregates small, reference by ID
Exposing entity internalsUse methods that express domain intent
Cross-aggregate transactionsUse domain events for eventual consistency
Repository returning DTOsReturn domain objects, map in application layer
反模式修复方案
贫血领域模型(业务逻辑在服务中)将业务逻辑迁移至实体中
聚合过大保持聚合粒度较小,通过ID引用其他聚合
暴露实体内部细节使用表达领域意图的方法
跨聚合事务使用领域事件实现最终一致性
仓库返回DTO返回领域对象,在应用层进行映射

Production Checklist

生产环境检查清单

  • Bounded contexts identified and documented
  • Ubiquitous language in code matches business terms
  • Aggregates enforce invariants
  • Value objects for all descriptors (Money, Email, Address)
  • Domain events for cross-context communication
  • Repository pattern for persistence abstraction
  • 已识别并记录Bounded Context
  • 代码中的通用语言(Ubiquitous Language)与业务术语一致
  • 聚合能够强制执行业务不变量
  • 所有描述性类型均使用Value Object(如Money、Email、Address)
  • 使用Domain Event实现跨上下文通信
  • 使用Repository模式作为持久化抽象层