战术模式映射 = L1 领域模型如何落到 L3 代码
解决什么问题:L1 战略层定义了 BC 和领域模型,但不知道怎么落到代码—— Tactical Mapping = 把战略层的"业务对象"翻译成代码层的"战术模式"—— Entity / Value Object / Domain Service / Domain Event / Aggregate / Repository。 一边是"业务怎么说话",一边是"代码怎么组织"——战术映射就是中间的桥。
它是什么
战术模式映射(Tactical Pattern Mapping) = L1 战略 DDD 模型到 L3 代码实现的"翻译规则"。
5 个核心战术模式(IDDD Ch 5-10):
| 战术模式 | 业务含义 | 代码形态 | 判定规则 |
|---|---|---|---|
| Entity(实体) | 有唯一身份 + 生命周期 | class + id + state | "对象有生命周期,需要追踪" |
| Value Object(值对象) | 无身份,按值判等 | immutable + equals | "对象只描述特征,无生命周期" |
| Domain Service(领域服务) | 无状态业务操作 | function / static method | "操作不属于任何实体" |
| Domain Event(领域事件) | 已发生的业务事实 | immutable event object | "业务上发生了某件事" |
| Aggregate(聚合) | 一致性边界 | root + children + invariant | "这些对象必须一起改" |
聚合根 + 仓储 = 战术模式的"边界封装":
- Aggregate Root = 一致性边界的入口
- Repository = 聚合的持久化抽象
IDDD Ch 10(Vernon)原话: "An Aggregate is a cluster of domain objects that can be treated as a single unit. The Aggregate Root is the only member that external clients interact with."
"Reference other Aggregates by identity only. Use eventual consistency for changes that span multiple Aggregates."
V2 视角(V2.7 B4 段 4 抽象 + B1 段 4 一致性边界): 战术模式 = 软件工程对"业务对象的工程容器"——给每个业务对象一个代码容器。 容器有 5 种选择(Entity/VO/Service/Event/Aggregate),每种对应不同的"对象类型"。 映射的关键不是"硬套所有模式",是"识别每种业务对象的类型,选最合适的容器"——这是 V2.5 强调的"分解的克制"。
关键属性
| 属性 | 值/含义 |
|---|---|
| 战略驱动 | 模式选择 = 战略层 BC + 领域逻辑的自然映射 |
| 类型化 | 每个业务对象都能归到 5 种模式之一 |
| 边界化 | 聚合 = 一致性边界,跨聚合用最终一致 |
| 小聚合 | 设计小聚合(性能 + 灵活),不是大聚合 |
| 身份引用 | 跨聚合用 ID(不是对象引用) |
| 不变量 | 聚合根保护真不变量 |
| 反模式 | 模式配齐 / 假不变量 / 贫血模型 / 大聚合 |
| 判错标志 | 模式与业务对象类型不符 / 跨聚合强一致 / 业务对象无法归类 |
怎么用
场景 1:5 种模式的选择流程
1. 这个对象有唯一身份 + 生命周期?
YES → Entity(实体)
NO ↓
2. 这个对象只描述特征,无生命周期?
YES → Value Object(值对象)
NO ↓
3. 这个对象是一个已发生的事实?
YES → Domain Event(领域事件)
NO ↓
4. 这个操作不属于任何实体?
YES → Domain Service(领域服务)
NO ↓
5. 这组对象必须一起改(真不变量)?
YES → Aggregate(聚合)
NO → 可能它们应该是独立的 Aggregate示例——订单领域:
Order → Entity(有 id + 生命周期:创建→支付→发货→签收)
OrderItem → Value Object(描述订单项的特征,无独立生命周期)
Money → Value Object(金额:amount + currency)
Address → Value Object(地址:province + city + district)
OrderCreated → Domain Event(订单已创建)
PlaceOrder → Domain Service(不属于 Order 实体,是"下单"这个动作)
Order + Items → Aggregate(订单 + 订单项必须一起改,聚合根 = Order)场景 2:Entity(实体)——有身份有生命周期
class Order:
"""实体 = 有身份 + 有生命周期 + 可变"""
def __init__(self, order_id: OrderId, customer_id: CustomerId):
self.id = order_id # 身份(不可变)
self.customer_id = customer_id
self.status = OrderStatus.PENDING # 状态(可变)
self.created_at = now()
# 判等:身份,不是属性
def __eq__(self, other):
return self.id == other.id
def __hash__(self):
return hash(self.id)
# 业务行为
def pay(self, amount: Money):
if self.status != OrderStatus.PENDING:
raise OrderNotPayable(self.id)
self.status = OrderStatus.PAID
# 发领域事件
return [OrderPaid(self.id, amount, now())]
def ship(self, tracking_number: str):
if self.status != OrderStatus.PAID:
raise OrderNotShippable(self.id)
self.status = OrderStatus.SHIPPED
return [OrderShipped(self.id, tracking_number, now())]实体的 3 个特征:
- 身份(Identity)——整个生命周期内 id 不变
- 连续性(Continuity)——状态可改但身份不变
- 判等用身份——不是用属性
场景 3:Value Object(值对象)——不可变 + 按值判等
@dataclass(frozen=True) # 不可变
class Money:
amount: Decimal
currency: str = "CNY"
def __add__(self, other):
if self.currency != other.currency:
raise CurrencyMismatch(self.currency, other.currency)
return Money(self.amount + other.amount, self.currency)
def __eq__(self, other):
return self.amount == other.amount and self.currency == other.currency
def __hash__(self):
return hash((self.amount, self.currency))
@dataclass(frozen=True)
class Address:
province: str
city: str
district: str
detail: str
postal_code: str
@dataclass(frozen=True)
class OrderItem:
product_id: ProductId
quantity: int
unit_price: Money
@property
def subtotal(self):
return Money(self.quantity * self.unit_price.amount, self.unit_price.currency)值对象的 3 个特征:
- 不可变(Immutable)——创建后不能改(要改 = 创建新的)
- 按值判等(Value Equality)——所有字段相等 = 相等
- 无副作用(Side-Effect-Free)——计算 / 比较都是纯函数
什么时候用值对象 vs 实体:
| 业务对象 | 选择 | 理由 |
|---|---|---|
| 金额 | Value Object | 100 元 = 100 元,无身份 |
| 地址 | Value Object | 北京中关村 = 北京中关村 |
| 订单项 | Value Object | 是订单的描述,无独立生命周期 |
| 订单 | Entity | 有 id + 状态可改 |
| 用户 | Entity | 有 id + 生命周期 |
场景 4:Domain Event(领域事件)——已发生的事实
@dataclass(frozen=True)
class OrderCreated:
"""事件 = 已发生的事实,不可变"""
order_id: OrderId
customer_id: CustomerId
items: list[OrderItem]
total_amount: Money
created_at: datetime # 事件发生时的时间
# 命名 = 过去时
# 时间 = 发生时间(不是处理时间)
# 内容 = 不可变快照
# 事件发布(保存到事件流)
class Order:
def place(self, cmd):
order = Order.create(cmd)
event = OrderCreated(...)
event_store.append(order.id, event) # 追加
return event事件的 5 个核心约束:
- 不可变——已发布不可改
- 过去时命名——Created / Updated / Cancelled
- 含时间戳——事件发生时间(不是处理时间)
- 含完整快照——包含"事件当时的所有信息"
- 自包含——订阅者不需要查询生产者
场景 5:Domain Service(领域服务)——不属于任何实体的操作
class TransferService:
"""转账服务 = 操作不属于 Account 实体(属于两个账户)"""
def transfer(self, from_account: Account, to_account: Account, amount: Money):
if from_account.balance < amount:
raise InsufficientFunds(from_account.id)
# 两个聚合的修改 → 必须用领域服务 + 跨聚合事件
from_account.withdraw(amount)
to_account.deposit(amount)
return [MoneyTransferred(from_account.id, to_account.id, amount)]什么时候用领域服务:
- 操作不属于任何实体(属于 2 个或多个)
- 操作无状态
- 操作涉及多种业务规则
反模式——贫血模型:
# ❌ 贫血模型(业务逻辑在 Service 里,Entity 只是数据)
class Order:
id = None
status = None
items = []
class OrderService:
def pay(self, order, amount):
if order.status != "PENDING":
raise ...
order.status = "PAID"
...
# ✅ 充血模型(业务逻辑在 Entity 里)
class Order:
def pay(self, amount):
if self.status != OrderStatus.PENDING:
raise OrderNotPayable(self.id)
self.status = OrderStatus.PAID
return [OrderPaid(self.id, amount, now())]场景 6:Aggregate(聚合)——一致性边界
class Order: # 聚合根
def __init__(self, order_id, customer_id):
self.id = order_id
self.customer_id = customer_id
self.items: list[OrderItem] = [] # 子对象
self.status = OrderStatus.PENDING
self.shipping_address: Address = None
def add_item(self, item: OrderItem):
# 真不变量保护:订单项数量 <= 50
if len(self.items) >= 50:
raise TooManyItems()
# 真不变量保护:每个订单项数量 > 0
if item.quantity <= 0:
raise InvalidQuantity()
self.items.append(item)
def remove_item(self, product_id: ProductId):
# 真不变量保护:不能全部移除
if len(self.items) <= 1:
raise CannotRemoveLastItem()
self.items = [i for i in self.items if i.product_id != product_id]
def ship_to(self, address: Address):
# 真不变量保护:必须有地址才能发货
if not address:
raise AddressRequired()
self.shipping_address = address
class OrderRepository:
"""仓储 = 聚合的持久化抽象,按聚合根存取"""
def save(self, order: Order):
# 整个聚合(Order + Items)一次持久化
self.db.save_order_with_items(order)
def find_by_id(self, order_id) -> Order:
return self.db.load_order_with_items(order_id)聚合的 4 条核心规则(IDDD Ch 10):
- Model true invariants in consistency boundaries——只把真不变量放在一致性边界内
- Design small aggregates——设计小聚合(性能 + 灵活)
- Reference other aggregates by identity——跨聚合用 ID 引用,不用对象引用
- Use eventual consistency outside the boundary——跨聚合用最终一致 + 事件
# ❌ 大聚合(Order 引用 Product 对象)
class Order:
items: list[OrderItem]
class OrderItem:
product: Product # 直接引用 Product 对象 → 跨聚合持锁
# ✅ 小聚合(Order 用 product_id 引用)
class OrderItem:
product_id: ProductId # 用 ID 引用,不持 Product 对象
# 需要 Product 信息时,单独查询
class Order:
def get_product_details(self):
return [self.product_repo.find_by_id(item.product_id) for item in self.items]常见误区
- "DDD 模式配齐"——错。业务只有 5 个对象就别抽 20 个类——模式配齐是安全感陷阱(V2.5 强调的"分解的克制")
- "贫血模型"——错。业务逻辑在 Service 里、Entity 只是数据 = 把 Entity 退化成数据表
- "大聚合"——错。一个聚合 = 一个表 = 一次事务 = 一次锁 = 性能差。大聚合 = 锁竞争 + 演化困难
- "假不变量"——错。开发者想象的"应该一致"不是真不变量。多个对象"应该一起改"但业务上可以分开 → 假不变量 = 切错聚合
- "强跨聚合一致"——错。跨聚合用最终一致 + 事件,不要用分布式事务
- "Value Object 应该有 setter"——错。值对象不可变,要改 = 创建新对象
- "Entity 应该继承 Value Object"——错。Entity 和 Value Object 是互斥的,不是一个继承另一个
- "Domain Service = Application Service"——错。Domain Service = 业务逻辑;Application Service = 用例编排
关键洞察
战术映射 = 把"业务怎么说话"翻译成"代码怎么组织"—— 业务上的"对象"用 Entity / Value Object 装;业务上的"事"用 Domain Event 装; 业务上的"动作"用 Domain Service 装;业务上的"边界"用 Aggregate 装。
V2.5 强调"分解的克制"——业务只有 5 个对象就别抽 20 个类。 战术映射的本质是识别每种业务对象的类型,不是"配齐模式显得专业"。
聚合是战术模式的灵魂——
- 聚合 = 一致性边界(决定事务粒度)
- 聚合 = 演化边界(决定哪些一起改)
- 聚合 = 团队边界(决定 1 个团队能拥有多少)
判断战术映射是否正确的标志:
- 每个业务对象都能归到 5 种模式之一
- 聚合是真不变量驱动的(不是数据库表驱动的)
- 跨聚合是最终一致(不是强一致)
- 模式选择服务于业务,不是为了"显得专业"
相关链接
- CQRS + Event Sourcing——战术模式的事件流化
- Saga——跨聚合的事务编排
- API Design——战术模式如何暴露为 API
- [V3 涌现作为] · 抽象——战术模式 = 抽象的业务容器
- [V3 涌现视域] · 认识对象——业务对象的 4 要素到代码
- [V3 涌现作为] · 分解——聚合是分解的一致性边界