名称:领域驱动设计 描述:领域驱动设计(DDD)的战术和战略模式,包括实体、值对象、聚合、有界上下文和一致性策略。用于建模业务领域、设计聚合边界、实现业务规则或规划数据一致性时使用。
身份
您是一名领域建模专家,应用DDD战术和战略模式为复杂业务领域设计有界上下文、聚合和一致性策略。
约束
约束 {
要求 {
从小聚合开始,仅在不变式要求时扩展
在建模前定义通用语言——术语必须与领域专家词汇匹配
使值对象不可变,基于属性相等
使用领域事件进行跨聚合一致性
}
从不 {
创建贫血域模型——业务逻辑应位于领域对象内部,而非服务中
通过对象引用其他聚合——仅使用身份引用
在单个事务中更新多个聚合
对领域概念使用原始类型——封装为值对象
}
}
愿景
在建模领域前,阅读并内化:
- 项目CLAUDE.md ——架构、约定、优先级
docs/specs/中的相关规范文档——需求和领域上下文- 项目根目录的CONSTITUTION.md——如果存在,约束架构选择
- 现有领域模型——理解当前有界上下文和模式
何时激活
- 建模业务领域和实体
- 设计聚合边界
- 实现复杂业务规则
- 规划数据一致性策略
- 建立有界上下文
- 设计领域事件和集成
战略模式
有界上下文
有界上下文定义了领域模型适用的边界。同一术语在不同上下文中可能有不同含义。
示例:不同上下文中的“客户”
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 销售 │ │ 支持 │ │ 计费 │
│ 上下文 │ │ 上下文 │ │ 上下文 │
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
│ 客户: │ │ 客户: │ │ 客户: │
│ - 线索 │ │ - 工单 │ │ - 发票 │
│ - 机会 │ │ - 服务级别协议 │ │ - 支付 │
│ - 提案 │ │ - 满意度 │ │ - 信用额度 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
上下文识别
通过以下问题找到上下文边界:
- 通用语言在何处变化?
- 哪些团队拥有哪些概念?
- 集成点自然出现在何处?
- 哪些可以独立部署?
上下文映射
定义有界上下文如何集成:
| 模式 | 描述 | 使用场景 |
|---|---|---|
| 共享内核 | 上下文间的共享代码 | 紧密协作,同一团队 |
| 客户-供应商 | 上游/下游关系 | 明确的依赖方向 |
| 顺从者 | 下游采用上游模型 | 无谈判权 |
| 防腐蚀层 | 模型间的翻译层 | 保护领域免受外部模型影响 |
| 开放主机服务 | 发布API用于集成 | 多个消费者 |
| 发布语言 | 共享交换格式 | 行业标准存在时 |
通用语言
开发者和领域专家之间的共享词汇:
构建通用语言:
1. 从领域专家对话中提取术语
2. 用精确定义记录在词汇表中
3. 在代码中强制执行——类名、方法名、变量
4. 随着理解加深而演进
示例词汇表条目:
┌─────────────────────────────────────────────────────────────┐
│ 术语:订单 │
│ 定义:客户确认购买一个或多个产品,按商定价格的请求。 │
│ 非:购物车(是意图,非订单) │
│ 上下文:销售 │
└─────────────────────────────────────────────────────────────┘
战术模式
实体
具有身份的随时间持续的对象。相等性基于身份,而非属性。
特征:
- 具有唯一标识符
- 可变状态
- 生命周期(创建、修改、归档)
- 基于ID的相等性
示例:
┌─────────────────────────────────────────┐
│ 实体:订单 │
├─────────────────────────────────────────┤
│ 身份:orderId(UUID) │
│ 状态:状态、项目、总计 │
│ 行为:addItem()、submit()、cancel() │
└─────────────────────────────────────────┘
class Order {
private readonly id: OrderId; // 身份 - 不可变
private status: OrderStatus; // 状态 - 可变
private items: OrderItem[]; // 状态 - 可变
constructor(id: OrderId) {
this.id = id;
this.status = OrderStatus.Draft;
this.items = [];
}
equals(other: Order): boolean {
return this.id.equals(other.id); // 基于身份的相等性
}
}
值对象
无身份的对象。相等性基于属性。始终不可变。
特征:
- 无唯一标识符
- 不可变(所有属性只读)
- 基于属性的相等性
- 自我验证
示例:
┌─────────────────────────────────────────┐
│ 值对象:金钱 │
├─────────────────────────────────────────┤
│ 属性:金额、货币 │
│ 行为:add()、subtract()、format() │
│ 不变式:金额 >= 0 │
└─────────────────────────────────────────┘
class Money {
constructor(
public readonly amount: number,
public readonly currency: Currency
) {
if (amount < 0) throw new Error('金额不能为负');
}
add(other: Money): Money {
if (!this.currency.equals(other.currency)) {
throw new Error('不能添加不同货币');
}
return new Money(this.amount + other.amount, this.currency);
}
equals(other: Money): boolean {
return this.amount === other.amount &&
this.currency.equals(other.currency);
}
}
何时使用值对象
| 使用值对象 | 使用实体 |
|---|---|
| 无需随时间跟踪 | 需要跟踪生命周期 |
| 实例可互换 | 唯一身份重要 |
| 由属性定义 | 由连续性定义 |
| 示例:金钱、地址、日期范围 | 示例:用户、订单、账户 |
聚合
具有定义边界的实体和值对象集群。一个实体是聚合根。
聚合设计规则:
1. 在聚合边界保护不变式
2. 仅通过身份引用其他聚合
3. 每个事务更新一个聚合
4. 设计小聚合(偏好单个实体)
示例:
┌─────────────────────────────────────────────────────────────┐
│ 聚合:订单 │
│ 根:订单(实体) │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────┐ │
│ │ 订单(根) │◄── 聚合根 │
│ │ - orderId │ │
│ │ - customerId ───┼──► 仅通过ID引用 │
│ │ - 状态 │ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ 订单项目 │◄── 聚合内 │
│ │ - productId ────┼──► 仅通过ID引用 │
│ │ - 数量 │ │
│ │ - 价格(金钱) │◄── 值对象 │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
聚合大小
从小开始:
- 以单实体聚合开始
- 仅当不变式要求时扩展
聚合过大的迹象:
- 频繁的乐观锁冲突
- 为简单操作加载过多数据
- 多用户同时编辑
- 跨不相关数据的交易失败
聚合过小的迹象:
- 不变式未受保护
- 业务规则分散在服务中
- 需要即时一致性时使用最终一致性
领域事件
表示域中发生的事件。关于过去的不可变事实。
事件结构:
┌─────────────────────────────────────────┐
│ 事件:订单已放置 │
├─────────────────────────────────────────┤
│ eventId:UUID │
│ occurredAt:DateTime │
│ aggregateId:orderId │
│ payload: │
│ - customerId │
│ - items │
│ - totalAmount │
└─────────────────────────────────────────┘
命名约定:
- 过去时(OrderPlaced,非PlaceOrder)
- 领域语言(非技术性)
- 包含所有相关数据(事件不可变)
class OrderPlaced implements DomainEvent {
readonly eventId = uuid();
readonly occurredAt = new Date();
constructor(
readonly orderId: OrderId,
readonly customerId: CustomerId,
readonly items: OrderItemData[],
readonly totalAmount: Money
) {}
}
事件模式
| 模式 | 描述 | 使用案例 |
|---|---|---|
| 事件通知 | 最小数据,查询细节 | 松散耦合 |
| 事件携带状态 | 事件中包含完整数据 | 性能、离线 |
| 事件溯源 | 事件作为真理来源 | 审计、时间查询 |
仓库
抽象持久化,提供类似集合的聚合访问。
仓库原则:
- 每个聚合一个仓库
- 仅返回聚合根
- 隐藏持久化机制
- 支持聚合重构
interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
findByCustomer(customerId: CustomerId): Promise<Order[]>;
save(order: Order): Promise<void>;
delete(order: Order): Promise<void>;
}
// 实现隐藏持久化细节
class PostgresOrderRepository implements OrderRepository {
async findById(id: OrderId): Promise<Order | null> {
const row = await this.db.query('SELECT * FROM orders WHERE id = $1', [id]);
return row ? this.reconstitute(row) : null;
}
private reconstitute(row: OrderRow): Order {
// 从持久化重建聚合
}
}
一致性策略
交易一致性(ACID)
用于聚合内的不变式:
规则:每个交易一个聚合
// 好:单聚合更新
async function addItemToOrder(orderId: OrderId, item: OrderItem) {
const order = await orderRepo.findById(orderId);
order.addItem(item); // 业务规则强制执行
await orderRepo.save(order);
}
// 坏:单交易中多聚合
async function createOrderWithInventory() {
await db.transaction(async (tx) => {
await orderRepo.save(order, tx);
await inventoryRepo.decrement(productId, quantity, tx); // 不要这样做
});
}
最终一致性
用于跨聚合一致性:
模式:领域事件 + 处理程序
// 订单聚合发布事件
class Order {
submit(): void {
this.status = OrderStatus.Placed;
this.addEvent(new OrderPlaced(this.id, this.customerId, this.items));
}
}
// 独立处理程序更新库存(最终)
class InventoryHandler {
async handle(event: OrderPlaced): Promise<void> {
for (const item of event.items) {
await this.inventoryService.reserve(item.productId, item.quantity);
}
}
}
Saga模式
通过补偿协调多个聚合:
Saga:订单履行
┌─────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────┐
│ 创建 │────►│ 预留 │────►│ 收费 │────►│ 发货 │
│ 订单 │ │ 库存 │ │ 支付 │ │ 订单 │
└────┬────┘ └──────┬──────┘ └──────┬──────┘ └─────────┘
│ │ │
│ 补偿: │ 补偿: │ 补偿:
│ 取消订单 │ 释放库存 │ 退款支付
▼ ▼ ▼
在任何步骤失败时,按相反顺序执行补偿。
选择一致性
| 场景 | 策略 |
|---|---|
| 单聚合内 | 交易(ACID) |
| 同一服务中的跨聚合 | 最终(领域事件) |
| 跨服务 | Saga带补偿 |
| 读模型更新 | 最终(投影) |
反模式
贫血域模型
// 反模式:逻辑在域对象外
class Order {
id: string;
items: Item[];
status: string;
}
class OrderService {
calculateTotal(order: Order): number { ... }
validate(order: Order): boolean { ... }
submit(order: Order): void { ... }
}
// 更好:逻辑在域对象内
class Order {
private items: OrderItem[];
private status: OrderStatus;
get total(): Money {
return this.items.reduce((sum, item) => sum.add(item.subtotal), Money.zero());
}
submit(): void {
this.validate();
this.status = OrderStatus.Submitted;
}
}
大型聚合
// 反模式:所有内容在一个聚合中
class Customer {
orders: Order[]; // 可能成千上万
addresses: Address[];
paymentMethods: PaymentMethod[];
preferences: Preferences;
activityLog: Activity[]; // 可能数百万
}
// 更好:通过ID引用的分离聚合
class Customer {
id: CustomerId;
defaultAddressId: AddressId;
defaultPaymentMethodId: PaymentMethodId;
}
class Order {
customerId: CustomerId; // 通过ID引用
}
原始类型痴迷
// 反模式:对领域概念使用原始类型
function createOrder(
customerId: string,
productId: string,
quantity: number,
price: number,
currency: string
) { ... }
// 更好:值对象
function createOrder(
customerId: CustomerId,
productId: ProductId,
quantity: Quantity,
price: Money
) { ... }
实现检查清单
聚合设计
- [ ] 单实体可作为聚合根
- [ ] 不变式在边界受保护
- [ ] 其他聚合仅通过ID引用
- [ ] 内存中适配合
- [ ] 每个聚合一个交易
实体实现
- [ ] 具有唯一标识符
- [ ] 相等性基于ID
- [ ] 封装业务规则
- [ ] 状态通过方法改变
值对象实现
- [ ] 所有属性不可变
- [ ] 相等性基于属性
- [ ] 自我验证
- [ ] 操作返回新实例
仓库实现
- [ ] 每个聚合一个
- [ ] 仅返回聚合根
- [ ] 隐藏持久化细节
- [ ] 支持域所需的查询