正在加载,请稍候…

领域驱动设计:实战指南

在真实项目中应用DDD概念。通过TypeScript示例学习实体、值对象、聚合、仓储和领域服务。

领域驱动设计:实战指南

领域驱动设计:实战指南

DDD 让你的代码结构与它所建模的业务领域保持一致。

核心构建块

领域驱动设计:实战指南插图

值对象

由属性定义的不可变对象。

class Money {
  private constructor(
    private readonly amount: number,
    private readonly currency: string
  ) {
    if (amount < 0) throw new Error('Amount cannot be negative');
    if (!['USD', 'EUR', 'GBP'].includes(currency)) {
      throw new Error('Invalid currency');
    }
  }

  static of(amount: number, currency: string): Money {
    return new Money(amount, currency);
  }

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

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

  toString() { return `${this.amount} ${this.currency}`; }
}

class Email {
  private constructor(private readonly value: string) {}

  static of(email: string): Email {
    if (!/^[^@]+@[^@]+\.[^@]+$/.test(email)) {
      throw new Error('Invalid email format');
    }
    return new Email(email.toLowerCase());
  }

  toString() { return this.value; }
}

实体

具有身份且随时间持续存在的对象。

abstract class Entity<T> {
  constructor(protected readonly id: T) {}

  equals(other: Entity<T>): boolean {
    return this.id === other.id;
  }
}

class UserId {
  constructor(readonly value: string) {}
}

class User extends Entity<UserId> {
  private email: Email;
  private name: string;

  private constructor(id: UserId, email: Email, name: string) {
    super(id);
    this.email = email;
    this.name = name;
  }

  static create(email: string, name: string): User {
    const id = new UserId(crypto.randomUUID());
    return new User(id, Email.of(email), name);
  }

  changeEmail(newEmail: string): void {
    this.email = Email.of(newEmail);
  }
}

领域驱动设计:实战指南插图

聚合

以根实体为核心的实体集群,用于维护不变量。

class Order extends Entity<OrderId> {
  private items: OrderItem[] = [];
  private status: OrderStatus = OrderStatus.PENDING;

  addItem(productId: ProductId, quantity: number, price: Money): void {
    if (this.status !== OrderStatus.PENDING) {
      throw new Error('Cannot add items to non-pending order');
    }
    const existing = this.items.find(i => i.productId.equals(productId));
    if (existing) {
      existing.increaseQuantity(quantity);
    } else {
      this.items.push(new OrderItem(productId, quantity, price));
    }
  }

  confirm(): void {
    if (this.items.length === 0) {
      throw new Error('Cannot confirm empty order');
    }
    this.status = OrderStatus.CONFIRMED;
  }

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

仓储

interface OrderRepository {
  findById(id: OrderId): Promise<Order | null>;
  findByUserId(userId: UserId): Promise<Order[]>;
  save(order: Order): Promise<void>;
  delete(id: OrderId): Promise<void>;
}

class PostgresOrderRepository implements OrderRepository {
  constructor(private db: Database) {}

  async findById(id: OrderId): Promise<Order | null> {
    const row = await this.db.query(
      'SELECT * FROM orders WHERE id = $1',
      [id.value]
    );
    return row ? OrderMapper.toDomain(row) : null;
  }

  async save(order: Order): Promise<void> {
    const data = OrderMapper.toPersistence(order);
    await this.db.query(
      'INSERT INTO orders ... ON CONFLICT (id) DO UPDATE ...',
      [data]
    );
  }
}

领域驱动设计:实战指南插图

领域服务

class OrderPricingService {
  constructor(
    private readonly discountRepository: DiscountRepository,
    private readonly taxService: TaxService
  ) {}

  async calculateTotal(order: Order, userId: UserId): Promise<Money> {
    const subtotal = order.subtotal;
    const discount = await this.discountRepository.findForUser(userId);
    const discounted = discount ? discount.apply(subtotal) : subtotal;
    const tax = await this.taxService.calculate(discounted);
    return discounted.add(tax);
  }
}

领域事件

interface DomainEvent {
  readonly occurredAt: Date;
  readonly eventType: string;
}

class OrderConfirmed implements DomainEvent {
  readonly occurredAt = new Date();
  readonly eventType = 'OrderConfirmed';

  constructor(
    readonly orderId: string,
    readonly userId: string,
    readonly total: Money
  ) {}
}

DDD 在复杂领域中回报丰厚,尤其是当业务规则成为竞争优势时。