之前参与的一个澳洲金融项目,被客户吐槽我们架构做的过于“老旧”,希望我们用DDD的思想实现。我个人觉得node项目,不适合用DDD。不仅处处别扭而且代码复杂。不过这里我还是梳理一下在Node中如何实现DDD。方便大家快速理解DDD

问题:贫血模型的问题

先看原来的代码,典型贫血模型(Anemic Model):

// ❌ 贫血模型:数据和逻辑分离
class Order {
  id: string;
  userId: string;
  items: OrderItem[];
  status: string;
  totalAmount: number;
}
 
class OrderService {
  async cancelOrder(orderId: string) {
    const order = await this.repo.findById(orderId);
    
    // 业务逻辑散落在 Service 层
    if (order.status === 'shipped') {
      throw new Error('已发货订单不能取消');
    }
    // ...
    
    order.status = 'cancelled';
    await this.repo.save(order);
    await this.refundService.refund(order);
    await this.inventoryService.restore(order.items);
  }
}

假设三个月后,同样的取消逻辑出现在 5 个地方,每个地方规则略有不同,改一个功能要翻 10 个文件。

充血模型(Rich Domain Model):把逻辑塞回实体

DDD 的核心思想:让数据和行为待在一起

// ✅ 充血模型:实体自己管理状态变更
class Order {
  private constructor(
    private id: string,
    private status: OrderStatus,
    // ...
  ) {}
  
  // 业务逻辑封装在实体内
  cancel(): void {
    if (this.status === OrderStatus.Shipped) {
      throw new DomainError('已发货订单不能取消');
    }
    this.status = OrderStatus.Cancelled;
  }
}

现在取消逻辑只在一个地方,其他代码调用 order.cancel() 就行。

下面我们不讲理论,直接上代码,看一个完整的 DDD 怎么用,以及他的问题。

DDD 四层架构

graph LR
    UI("&nbsp;&nbsp;&nbsp;User Interface Layer&nbsp;&nbsp;&nbsp;<br/>「接口层」")
    UI_NOTE["💡 职责:接收 HTTP 请求、返回响应、参数校验、错误处理<br/>📦 代码:Controller、DTO、Middleware"]
    
    APP("&nbsp;&nbsp;&nbsp;Application Layer&nbsp;&nbsp;&nbsp;<br/>「应用层」")
    APP_NOTE["💡 职责:编排用例流程、调度领域对象、事务管理、发布领域事件<br/>📦 代码:UseCase、Command、EventHandler"]
    
    DOMAIN("&nbsp;&nbsp;&nbsp;Domain Layer&nbsp;&nbsp;&nbsp;<br/>「领域层」")
    DOMAIN_NOTE["💡 职责:核心业务逻辑、状态变更、业务规则校验「不依赖外部」<br/>📦 代码:Entity、Value Object、Domain Service、Domain Event"]
    
    INFRA("&nbsp;&nbsp;&nbsp;Infrastructure Layer&nbsp;&nbsp;&nbsp;<br/>「基础设施层」")
    INFRA_NOTE["💡 职责:数据库访问、外部 API 调用、消息队列、缓存、文件存储<br/>📦 代码:Repository 实现、Mapper、HTTP Client、Queue Client"]
    
    UI --> APP
    APP --> DOMAIN
    DOMAIN -.->|依赖倒置| INFRA
    
    UI -.- UI_NOTE
    APP -.- APP_NOTE
    DOMAIN -.- DOMAIN_NOTE
    INFRA -.- INFRA_NOTE
    
    style UI fill:#dbeafe,stroke:#1e40af,stroke-width:3px
    style APP fill:#bfdbfe,stroke:#3b82f6,stroke-width:3px
    style DOMAIN fill:#fef3c7,stroke:#f59e0b,stroke-width:3px
    style INFRA fill:#f3f4f6,stroke:#6b7280,stroke-width:3px
    
    style UI_NOTE fill:#f0f9ff,stroke:#bae6fd,stroke-width:1px,text-align:left
    style APP_NOTE fill:#eff6ff,stroke:#bfdbfe,stroke-width:1px,text-align:left
    style DOMAIN_NOTE fill:#fffbeb,stroke:#fde68a,stroke-width:1px,text-align:left
    style INFRA_NOTE fill:#fafafa,stroke:#e5e7eb,stroke-width:1px,text-align:left

依赖方向:上层依赖下层,Domain Layer 是核心,不依赖任何外部(依赖倒置原则)。

1. User Interface Layer(接口层)

接收请求、返回响应,不写业务逻辑。

// presentation/controllers/OrderController.ts
class OrderController {
  constructor(
    private cancelOrderUseCase: CancelOrderUseCase
  ) {}
  
  async cancelOrder(req: Request, res: Response) {
    const { orderId } = req.params;
    const userId = req.user.id;
    
    try {
      // 这里是useCase, 可以将复杂的逻辑, 拆分不同的use case中。
      await this.cancelOrderUseCase.execute({ orderId, userId });
      res.json({ success: true });
    } catch (error) {
      if (error instanceof DomainError) {
        res.status(400).json({ error: error.message });
      } else {
        res.status(500).json({ error: 'Internal error' });
      }
    }
  }
}

2. Application Layer(应用层)

编排用例流程,调度 Domain 和 Infrastructure。

// application/use-cases/CancelOrderUseCase.ts
class CancelOrderUseCase {
  constructor(
    private orderRepo: IOrderRepository,
    private paymentService: IPaymentService,
    private inventoryService: IInventoryService,
    private eventBus: IEventBus
  ) {}
  
  async execute(command: CancelOrderCommand): Promise<void> {
    // 1. 加载聚合根
    const order = await this.orderRepo.findById(command.orderId);
    if (!order) {
      throw new NotFoundError('订单不存在');
    }
    
    // 2. 权限检查
    if (order.userId !== command.userId) {
      throw new ForbiddenError('无权操作');
    }
    
    // 3. 执行领域逻辑
    order.cancel();
    
    // 4. 持久化
    await this.orderRepo.save(order);
    
    // 5. 触发副作用(退款、恢复库存)
    await this.paymentService.refund(order.id, order.totalAmount);
    await this.inventoryService.restoreStock(order.items);
    
    // 6. 发布领域事件
    await this.eventBus.publish(new OrderCancelledEvent(order.id));
  }
}

3. Domain Layer(领域层)

核心业务逻辑,不依赖外部。

// domain/entities/Order.ts
export class Order {
  private constructor(
    private readonly id: OrderId,
    private status: OrderStatus,
    // ...
  ) {}
  
  // 工厂方法:创建新订单
  static create(userId: UserId, items: OrderItem[]): Order {
    if (items.length === 0) {
      throw new DomainError('订单至少包含一个商品');
    }
    return new Order(OrderId.generate(), userId, items, OrderStatus.Pending, new Date());
  }
  
  // 领域行为
  cancel(): void {
    if (!this.canBeCancelled()) {
      throw new DomainError(`订单状态为 ${this.status},不能取消`);
    }
    this.status = OrderStatus.Cancelled;
  }
  
  private canBeCancelled(): boolean {
    return this.status === OrderStatus.Pending || this.status === OrderStatus.Paid;
  }
}
 
// domain/value-objects/Money.ts
export class Money {
  private constructor(
    private readonly amount: number,
    private readonly currency: string
  ) {
    if (amount < 0) throw new DomainError('金额不能为负数');
  }
  
  add(other: Money): Money {
    if (this.currency !== other.currency) {
      throw new DomainError('不同币种不能相加');
    }
    return new Money(this.amount + other.amount, this.currency);
  }
  
  // ...
}

4. Infrastructure Layer(基础设施层)

数据库、外部 API、消息队列等实现。

// infrastructure/repositories/OrderRepository.ts
class OrderRepository implements IOrderRepository {
  constructor(private prisma: PrismaClient) {}
  
  async findById(id: string): Promise<Order | null> {
    const raw = await this.prisma.order.findUnique({
      where: { id },
      include: { items: true }
    });
    
    if (!raw) return null;
    
    // 将数据库记录转换为领域对象
    return OrderMapper.toDomain(raw);
  }
  
  async save(order: Order): Promise<void> {
    const raw = OrderMapper.toPersistence(order);
    await this.prisma.order.upsert({ where: { id: raw.id }, create: raw, update: raw });
  }
}
 
// infrastructure/mappers/OrderMapper.ts
class OrderMapper {
  static toDomain(raw: any): Order {
    // 从数据库格式转换为领域对象
    return Order.reconstitute(
      OrderId.from(raw.id),
      UserId.from(raw.userId),
      raw.items.map(OrderItemMapper.toDomain),
      OrderStatus[raw.status],
      // ...
    );
  }
  
  static toPersistence(order: Order): any {
    // 从领域对象转换为数据库格式
    return {
      id: order.id.value,
      userId: order.userId.value,
      status: order.status,
      // ...
    };
  }
}

核心概念实战

Entity vs Value Object

Entity(实体):有唯一标识,生命周期中可变。

// ✅ Entity:订单有 ID,状态会变
class Order {
  private id: OrderId; // 唯一标识
  private status: OrderStatus; // 可变状态
  
  equals(other: Order): boolean {
    return this.id.equals(other.id); // 通过 ID 判断相等
  }
}
 
// ✅ Value Object:金额通过数值判断相等
class Money {
  private readonly amount: number; // 不可变
  
  equals(other: Money): boolean {
    return this.amount === other.amount && this.currency === other.currency;
  }
  
  // 返回新对象,不修改自己
  add(other: Money): Money {
    return new Money(this.amount + other.amount, this.currency);
  }
}

判断标准

  • 需要追踪生命周期?→ Entity
  • 只关心值本身?→ Value Object
  • 示例:用户(Entity)、邮箱地址(Value Object)、订单(Entity)、收货地址(Value Object)

Aggregate(聚合根)

问题:Order 和 OrderItem 谁管谁?

// ❌ 错误:外部直接修改 OrderItem
const order = await repo.findById('123');
order.items[0].quantity = 10; // 绕过了 Order 的校验逻辑
await repo.save(order);

解决:Order 是聚合根,OrderItem 是聚合内部对象。

// ✅ 正确:通过 Order 修改 Item
class Order {
  private items: OrderItem[] = [];
  
  addItem(productId: string, quantity: number, price: Money): void {
    // Order 控制添加逻辑
    if (this.status !== OrderStatus.Pending) {
      throw new DomainError('只有待支付订单才能添加商品');
    }
    
    const existing = this.items.find(i => i.productId === productId);
    if (existing) {
      existing.increaseQuantity(quantity);
    } else {
      this.items.push(new OrderItem(productId, quantity, price));
    }
  }
}
 
// OrderItem 只能通过 Order 访问
class OrderItem {
  constructor(
    public readonly productId: string,
    private quantity: number,
    public readonly price: Money
  ) {}
  
  // 包内可见,外部不能直接调用
  increaseQuantity(amount: number): void {
    this.quantity += amount;
  }
}

聚合边界划分原则

  1. 一个聚合一个 Repository
  2. 聚合内部对象不暴露给外部
  3. 跨聚合通过 ID 引用,不直接持有对象
// ✅ 正确:Order 引用 User 的 ID
class Order {
  private userId: UserId; // 只存 ID,不存 User 对象
}
 
// ❌ 错误:Order 直接持有 User
class Order {
  private user: User; // 跨聚合边界了
}

Repository 模式

Repository 是领域层和基础设施层的桥梁。

// domain/repositories/IOrderRepository.ts
export interface IOrderRepository {
  findById(id: string): Promise<Order | null>;
  findByUserId(userId: string): Promise<Order[]>;
  save(order: Order): Promise<void>;
  delete(id: string): Promise<void>;
}

关键点

  1. 接口定义在 Domain 层
  2. 实现在 Infrastructure 层
  3. 只暴露领域对象,不暴露数据库细节
// ❌ 错误:泄露数据库细节
interface IOrderRepository {
  executeQuery(sql: string): Promise<any>;
  getPrismaClient(): PrismaClient;
}
 
// ✅ 正确:只暴露领域操作
interface IOrderRepository {
  findById(id: string): Promise<Order | null>;
  save(order: Order): Promise<void>;
}

Domain Service vs Application Service

Domain Service:处理多个实体协作的领域逻辑。

// domain/services/PricingService.ts
class PricingService {
  calculateDiscount(order: Order, coupon: Coupon): Money {
    // 复杂定价逻辑:涉及 Order 和 Coupon 两个聚合
    if (!coupon.canApplyTo(order)) return Money.zero();
    
    return coupon.type === 'percentage' 
      ? order.totalAmount.multiply(coupon.value / 100)
      : Money.from(coupon.value);
  }
}
 
// application/use-cases/ApplyCouponUseCase.ts
class ApplyCouponUseCase {
  constructor(
    private orderRepo: IOrderRepository,
    private pricingService: PricingService // 注入 Domain Service
  ) {}
  
  async execute(command: ApplyCouponCommand): Promise<void> {
    const order = await this.orderRepo.findById(command.orderId);
    const coupon = await this.couponRepo.findByCode(command.couponCode);
    
    // 调用 Domain Service
    const discount = this.pricingService.calculateDiscount(order, coupon);
    order.applyDiscount(discount);
    
    await this.orderRepo.save(order);
  }
}

Domain Event(领域事件)

当订单取消时,需要退款、恢复库存、发送通知。不要在 order.cancel() 里直接调用这些服务,用事件解耦。

// domain/events/OrderCancelledEvent.ts
export class OrderCancelledEvent {
  constructor(
    public readonly orderId: string,
    public readonly userId: string,
    public readonly cancelledAt: Date
  ) {}
}
 
// domain/entities/Order.ts
class Order {
  private domainEvents: DomainEvent[] = [];
  
  cancel(): void {
    if (!this.canBeCancelled()) throw new DomainError('订单不能取消');
    
    this.status = OrderStatus.Cancelled;
    
    // 记录领域事件
    this.addDomainEvent(new OrderCancelledEvent(this.id.value, this.userId.value, new Date()));
  }
  
  getDomainEvents(): DomainEvent[] { return [...this.domainEvents]; }
  clearDomainEvents(): void { this.domainEvents = []; }
  private addDomainEvent(event: DomainEvent): void { this.domainEvents.push(event); }
}
 
// application/use-cases/CancelOrderUseCase.ts
class CancelOrderUseCase {
  async execute(command: CancelOrderCommand): Promise<void> {
    const order = await this.orderRepo.findById(command.orderId);
    order.cancel();
    await this.orderRepo.save(order);
    
    // 发布事件
    const events = order.getDomainEvents();
    for (const event of events) {
      await this.eventBus.publish(event);
    }
    order.clearDomainEvents();
  }
}
 
// application/event-handlers/OrderCancelledHandler.ts
class OrderCancelledHandler {
  async handle(event: OrderCancelledEvent): Promise<void> {
    await this.paymentService.refund(event.orderId);
    await this.inventoryService.restoreStock(event.orderId);
  }
}

完整目录结构

src/
├── presentation/           # User Interface Layer
│   ├── controllers/
│   ├── middlewares/
│   └── dtos/
├── application/            # Application Layer
│   ├── use-cases/
│   ├── event-handlers/
│   └── interfaces/        # 接口定义(Repository、外部服务)
├── domain/                 # Domain Layer
│   ├── entities/
│   ├── value-objects/
│   ├── services/
│   ├── events/
│   └── errors/
└── infrastructure/         # Infrastructure Layer
    ├── repositories/
    ├── mappers/
    ├── external-services/
    └── database/

DDD 的实际缺点

DDD 真的 那么好吗?

Node.js/Express 不适合 DDD

DDD 诞生于 Java 生态,天然契合强 OOP 语言。在 Node.js 里写 DDD,处处别扭。

// ❌ Node.js:class 语法像"后妈养的"
class Order {
  private id: OrderId;  // TypeScript 的 private 是编译时检查,运行时还是 public
  private status: OrderStatus;
  // ...
}

Node.js 的问题

  1. class 不是一等公民:JavaScript 本质是原型链,class 只是语法糖
  2. 没有真正的 private:运行时一切都是 public,private 只是 TypeScript 编译时检查
  3. 依赖注入复杂:Java 有 Spring,Node.js 要自己搭 DI 容器(InversifyJS/TypeDI 或者用 nestJS)
  4. ORM 不够成熟:Java 有 Hibernate,Node.js 的 TypeORM 性能差、Prisma 不返回 class instance

Node.js 更适合函数式风格

// ✅ 函数式:更贴合 JavaScript 思维
type Order = {
  id: string;
  status: OrderStatus;
  items: OrderItem[];
};
 
const cancelOrder = (order: Order): Result<Order, DomainError> => {
  if (order.status === 'shipped') {
    return err(new DomainError('已发货订单不能取消'));
  }
  return ok({ ...order, status: 'cancelled' });
};
 
// 使用
const order = await getOrder(orderId);
const result = cancelOrder(order);
 
if (result.isOk) {
  await saveOrder(result.value);
}

所以 Node.js 做 DDD 要付出额外成本,不如 Java 那样自然。

还是太复杂

团队新人看代码:「为什么一个取消订单要写 7 个文件?」

- OrderController.ts         # 接收请求
- CancelOrderCommand.ts       # DTO
- CancelOrderUseCase.ts       # 用例
- Order.ts                    # 实体
- IOrderRepository.ts         # 接口
- OrderRepository.ts          # 实现
- OrderMapper.ts              # 映射

贫血模型只要 2 个文件:Controller + Service。

过度设计风险

不是所有业务都需要 DDD。我们有个配置管理模块,就是 CRUD,用 DDD 写了 200 行,普通 MVC 只要 50 行。

// ❌ 过度设计:简单 CRUD 用 DDD
class ConfigEntity {
  constructor(
    private id: ConfigId,
    private key: ConfigKey,
    private value: ConfigValue
  ) {}
  
  updateValue(newValue: string): void {
    this.value = ConfigValue.from(newValue);
  }
}
 
// ✅ 够用就好:简单场景用简单方案
const config = await prisma.config.update({
  where: { key: 'theme' },
  data: { value: 'dark' }
});

聚合边界难划分

Order 和 Payment 是两个聚合还是一个?

// 方案 A:Order 聚合包含 Payment
class Order {
  private payment: Payment; // Payment 是聚合内部对象
}
 
// 方案 B:Order 和 Payment 是两个聚合
class Order {
  private paymentId: string; // 只存 ID,跨聚合引用
}

我们最初选了方案 A,后来发现支付要接入第三方系统,改成方案 B 重构了一周。

性能问题

DDD 追求纯领域对象,但有时需要跨多个聚合查询。

// ❌ 低效:加载 100 个 Order 实体,再遍历计算
async getTopCustomers(): Promise<Customer[]> {
  const orders = await this.orderRepo.findAll();
  const grouped = groupBy(orders, o => o.userId);
  return sortBy(grouped, g => g.totalAmount).slice(0, 10);
}

CQRS 解决方案

DDD 纯粹主义者会说「用 CQRS(Command Query Responsibility Segregation,命令查询职责分离)」。核心思想:写操作用领域模型,读操作用查询模型

// 写操作(Command):用完整的领域模型
class CancelOrderUseCase {
  async execute(command: CancelOrderCommand): Promise<void> {
    const order = await this.orderRepo.findById(command.orderId);  // 加载完整聚合根
    order.cancel();  // 领域逻辑
    await this.orderRepo.save(order);
  }
}
 
// 读操作(Query):直接查询数据库,返回 DTO
class GetTopCustomersQuery {
  async execute(): Promise<TopCustomerDTO[]> {
    // 直接写 SQL,不加载领域对象
    return prisma.$queryRaw`
      SELECT u.id, u.name, COUNT(o.id) as order_count, SUM(o.total_amount) as total_spent
      FROM users u LEFT JOIN orders o ON u.id = o.user_id
      WHERE o.status = 'completed'
      GROUP BY u.id ORDER BY total_spent DESC LIMIT 10
    `;
  }
}

CQRS 的目录结构

src/
├── application/
│   ├── commands/           # 写操作
│   │   ├── CancelOrderCommand.ts
│   │   └── CancelOrderHandler.ts
│   └── queries/            # 读操作
│       ├── GetTopCustomersQuery.ts
│       └── GetOrderDetailQuery.ts
├── domain/                 # 只用于写操作
│   └── entities/Order.ts
└── infrastructure/
    ├── repositories/       # 写操作用
    │   └── OrderRepository.ts
    └── query-services/     # 读操作用
        └── OrderQueryService.ts

大量的Mapper胶水代码

每个实体都要写 toDomaintoPersistence,重复劳动。

class OrderMapper {
  static toDomain(raw: any): Order { /* 50 行 */ }
  static toPersistence(order: Order): any { /* 50 行 */ }
}
 
class OrderItemMapper {
  static toDomain(raw: any): OrderItem { /* 30 行 */ }
  static toPersistence(item: OrderItem): any { /* 30 行 */ }
}

DDD Lite:简化DDD

既然DDD 太复杂了,我们能不能做一些简化,降低复杂度,只用 DDD 的核心部分?

  • ✅ 充血模型(Entity 包含业务逻辑)
  • ✅ Value Object(关键值对象)
  • ✅ Repository 接口
  • ❌ 不用 Application Service(直接在 Controller 里编排)
  • ❌ 不用 Domain Event
  • ❌ 不用复杂的 Mapper(Prisma 的 model 直接当 Entity 用, 需要扩展model, 默认 Prisma 是 贫血模型)

总结

DDD 不是银弹,它解决的是复杂业务建模的问题,DDD 适合的场景

  • ✅ 使用 Java/C# 这种强 OOP 语言
  • ✅ 业务规则复杂(订单、库存、支付联动)
  • ✅ 需要长期维护(不是一次性项目)
  • ✅ 团队有学习意愿和 OOP 经验

代码是写给人看的,顺手比正确更重要, 不要为了 DDD 而 DDD,选择适合团队和项目的架构才是正道。