领域驱动设计 — 模式与实践 是做什么的?
何时使用 DDD
使用场景:
- 超越CRUD的复杂业务逻辑
- 拥有不同术语的多个领域专家
- 领域模型是竞争优势
- 企业级应用
避免使用场景:
- 简单的CRUD应用
- 黑客马拉松/MVP(过度设计)
- 没有领域专家可用
战略设计
限界上下文
DDD中最重要的单一概念。限界上下文是一个边界,在该边界内定义并适用一个特定的模型。
示例 — 电子商务:
[订单上下文] [支付上下文] [物流上下文]
- 订单 - 支付 - 货运
- 订单项 - 交易 - 追踪编号
- 客户(引用) - 退款 - 地址
- 地址(值对象) - 发票 - 承运商
Customer 在每个上下文中含义不同:
- 订单上下文:姓名、邮箱、配送偏好
- 支付上下文:账单信息、支付方式
- 支持上下文:工单历史、满意度评分
上下文映射图
[订单] ←→ [支付] # 合作关系
[订单] → [物流] # 客户-供应商
[订单] → [遗留CRM] # 防损层
战术设计
实体
具有标识的对象。两个属性相同但ID不同的实体被视为不同实体。
class User {
readonly id: UserId;
name: string;
email: Email; // 值对象
}
值对象
由其属性定义而非标识的对象。不可变。
class Email {
constructor(readonly value: string) {
if (!isValidEmail(value)) throw new InvalidEmailError(value);
}
equals(other: Email): boolean {
return this.value === other.value;
}
}
聚合
由单个根实体(聚合根)统领的实体和值对象集群。所有访问都通过根进行。
class Order { // 聚合根
private items: OrderItem[] = [];
addItem(product: ProductRef, quantity: number): void {
// 业务逻辑在这里,不在服务中
if (quantity <= 0) throw new InvalidQuantityError();
this.items.push(new OrderItem(product, quantity));
}
get total(): Money {
return this.items.reduce((sum, item) => sum.add(item.subtotal), Money.zero());
}
}
聚合规则:
- 仅通过ID引用其他聚合
- 每个事务一个聚合
- 聚合之间采用最终一致性
领域事件
领域中发生的、领域专家关心的事件。
class OrderPlaced implements DomainEvent {
constructor(
readonly orderId: OrderId,
readonly customerId: CustomerId,
readonly total: Money,
readonly occurredAt: Date
) {}
}
领域服务
不属于实体或值对象的业务逻辑。
class PricingService {
calculatePrice(order: Order, customer: Customer, promotions: Promotion[]): Money {
// 涉及多个聚合的复杂定价逻辑
}
}
仓储
数据访问的抽象。每个聚合根对应一个仓储。
interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
save(order: Order): Promise<void>;
delete(id: OrderId): Promise<void>;
}
目录结构 (DDD)
src/
├── modules/
│ └── orders/ # 限界上下文
│ ├── domain/
│ │ ├── entities/
│ │ │ └── order.ts # 聚合根
│ │ ├── value-objects/
│ │ │ └── money.ts
│ │ ├── events/
│ │ │ └── order-placed.ts
│ │ ├── services/
│ │ │ └── pricing.ts
│ │ └── repositories/
│ │ └── order.repository.ts # 接口
│ ├── application/
│ │ ├── commands/
│ │ │ └── place-order.ts
│ │ ├── queries/
│ │ │ └── get-order.ts
│ │ └── handlers/
│ │ └── place-order.handler.ts
│ └── infrastructure/
│ ├── persistence/
│ │ └── order.repository.impl.ts # 实现
│ └── api/
│ └── orders.controller.ts
整洁架构依赖规则
在DDD中,依赖关系必须严格向内指向:
[ 框架与驱动 (Web, DB, UI) ]
└──▶ [ 接口适配器 (Controllers, Gateways) ]
└──▶ [ 应用层 (Use Cases, CQRS Handlers) ]
└──▶ [ 领域层 (Entities, Value Objects) ]
- 领域层对ORM(如Prisma、TypeORM)、HTTP框架(如Express、NestJS)或外部SDK 零依赖。
- 仓储在领域层/应用层定义为接口,在基础设施层实现。
领域事件 vs 集成事件
- 领域事件:表示单个限界上下文内部的状态变更。
- 直接在聚合根内部引发(如
order.addItem(...)引发OrderItemAdded)。 - 在事务提交前进程内分发。
- 直接在聚合根内部引发(如
- 集成事件:跨限界上下文边界发布,用于与其他服务通信。
- 通过事务性发件箱模式分发给消息代理。
- 必须使用带有版本控制的向后兼容模式。
防损层 (ACL)
当消费来自外部限界上下文或第三方供应商API(如Stripe、Salesforce)的数据时:
- 绝对不要将外部领域模型直接导入你的领域。
- 在基础设施层创建一个 ACL转换器/适配器,将外部DTO转换为你自己的值对象和实体。
反模式
- [失败] 贫血领域模型 —— 实体只有getter/setter,所有逻辑在服务中
- [失败] 大聚合 —— 聚合应该小,专注于不变量
- [失败] 跨聚合事务 —— 使用最终一致性
- [失败] 处处DDD —— 仅在复杂性足以证明合理的地方使用DDD
- [失败] ORM实体泄漏到领域 —— 领域实体不得依赖
@Entity()或ORM装饰器
代码示例
详细代码示例请参见 EXAMPLES.md。
验证清单
在完成任务前的审查阶段需要验证的内容。
常见错误
需要明确避免的反模式和事项。请参见 TROUBLESHOOTING.md。
集成说明
此技能如何与其他技能交互。