战术设计 (Tactical Design)
概述
战术设计是在 战略设计 确定的限界上下文内部,对具体业务场景进行精细化建模的工程手段。它关注"怎么写代码"才能让业务规则稳定、可维护、可演进。
核心特征:
- 限定在单一限界上下文内 ——不跨上下文讨论
- 面向模型和代码 ——产出物是类、聚合、接口
- 工程师的主要战场 ——开发日常工作的核心
核心问题:
1. 哪些对象有身份,哪些只是值? → 实体 vs 值对象
2. 哪些对象必须一起变更? → 聚合
3. 谁负责对象的创建? → 工厂
4. 对象如何持久化? → 仓储
5. 不属于任何对象的业务逻辑放哪里? → 领域服务
6. 如何解耦聚合间的协作? → 领域事件
战术设计 vs 战略设计
战略设计 ──▶ 战术设计 ──▶ 代码实现
(宏观) (微观) (工程)
│ │ │
├─ 领域 ├─ 实体 ├─ Class / Struct
├─ 子域 ├─ 值对象 ├─ Immutable Object
├─ 限界上下文 ├─ 聚合根 ├─ Aggregate Root
└─ 上下文映射 ├─ 工厂 ├─ Factory
├─ 仓储 ├─ Repository Impl
├─ 领域服务 ├─ Service Class
└─ 领域事件 └─ Event + Handler
战术设计的七大构件
Eric Evans 在蓝皮书中给出的原始战术模式包括 实体、值对象、聚合、工厂、仓储、服务(含领域服务)。领域事件虽未在原书的"模型驱动设计构造块"章节正式列出,但 Evans 在后续会议演讲与 Vaughn Vernon 的 IDDD 中已将其纳入战术设计的核心构件。本文采用 7 件套的常见划分:
⚠️ 按需选用,不为标准而引入:七大构件是工具箱,不是"DDD 项目必须全部齐备"的检查清单。简单的 CRUD 类子域、原型期项目,使用其中 2–3 件(如实体 + 仓储)足矣;只有核心子域、业务规则密集的场景才需要工厂、领域服务、领域事件等"重武器"全部上场。复杂度匹配是 DDD 落地的根本判据。
1. 实体 (Entity)
定义:拥有唯一标识和生命周期的业务对象。
典型:User、Order、Subscription
判别:通过 ID 判断相等,而非属性值相等。
代码形态(四种血液模型):
▸ 失血模型:只有字段,业务逻辑外泄 ❌ 不推荐
▸ 贫血模型:数据 + 核心业务规则 ✅ 工程首选
▸ 充血模型:数据 + 所有业务(含持久化) ⚠️ 少用
▸ 胀血模型:还掺入授权、事务等横切关注点 ❌ 禁用
2. 值对象 (Value Object)
定义:没有身份、通过属性值判断相等、不可变的对象。
典型:Money、Email、Address、DateRange
判别:不需要"哪个",只关心"是什么"。
分类:
▸ 单一属性值对象:Email、Phone(只有一个值)
▸ 多属性值对象:Money(amount + currency)、Address(province + city + street)
价值:封装领域概念,消除"原始类型偏执"。
3. 聚合与聚合根 (Aggregate & Aggregate Root)
定义:一组必须一起变更的实体和值对象的集合。
入口:聚合根是外部访问聚合的唯一入口。
事务:一个事务只修改一个聚合。
设计原则:
▸ 尽量小的聚合(高内聚)
▸ 聚合间通过 ID 引用,不通过对象引用
▸ 聚合内强一致,聚合间最终一致
范围:聚合 < 限界上下文 < 子域 < 领域
4. 工厂 (Factory)
定义:封装复杂对象 / 聚合的创建过程,保证创建时不变量满足。
形态:
▸ 独立工厂类:复杂创建,依赖外部服务
▸ 聚合根静态工厂方法:常规场景首选
▸ 值对象工厂方法:Money.yuan(100)
原则:构造即合法,工厂要防止非法对象诞生。
5. 仓储 (Repository)
定义:聚合的持久化抽象,为领域层提供类似集合的接口。
分层:
▸ 接口定义在领域层(依赖倒置)
▸ 实现在仓储层 / 基础设施层
▸ Mapper / ORM 是仓储的具体实现手段
使用约束:
▸ 一个聚合根一个仓储
▸ 返回领域对象,不返回数据库实体
▸ 保存的是完整聚合
6. 领域服务 (Domain Service)
定义:封装不属于任何单个实体或值对象的领域逻辑。
特征:
▸ 无状态
▸ 纯业务,不管事务、不调基础设施
▸ 位于领域层
判断标准:
如果逻辑不自然属于任何一个实体 → 用领域服务
优先把逻辑放在实体中,领域服务是补充
7. 领域事件 (Domain Events)
定义:领域中已发生且值得关注的事实。
命名:过去时态(OrderPlaced、PaymentCompleted)。
用途:
▸ 聚合间解耦通信
▸ 跨限界上下文协作
▸ 实现最终一致性(配合 Saga / Outbox)
▸ 审计 / 事件溯源的原材料
七大构件的协作关系
┌───────────────────────────────────────────────────────────────┐
│ 一个限界上下文 │
│ │
│ ┌──────────────┐ │
│ 创建 ──▶ │ 工厂 │ │
│ └──────┬───────┘ │
│ ▼ │
│ ┌──────────────┐ 包含 │
│ │ 聚合根(Entity) │──────────▶ [实体] [值对象] │
│ └──────┬───────┘ │
│ │ 发布 │
│ ▼ │
│ ┌──────────────┐ │
│ │ 领域事件 │──────────▶ 其他聚合 / 上下文 │
│ └──────────────┘ │
│ ▲ │
│ │ 持久化 │
│ ┌──────┴───────┐ │
│ │ 仓储 │ │
│ └──────────────┘ │
│ ▲ │
│ │ 调用 │
│ ┌──────┴───────┐ │
│ 跨聚合 ──▶│ 领域服务 │ │
│ └──────────────┘ │
└───────────────────────────────────────────────────────────────┘
战术设计落地:典型用例流程
以 RabbitAdvisors 用户订阅专栏 为例:
1. 应用服务接收命令 SubscribeCommand
│
▼
2. 领域服务 SubscriptionDomainService.subscribe()
│
├─▶ 查专栏(通过防腐层 ColumnFacadeClient)
│
├─▶ 工厂 OrderFactory.create(reader, column) 创建订单聚合
│
├─▶ 支付(通过 PaymentClient)→ Payment 聚合
│
├─▶ Subscription.fromPayment(payment, column) 工厂方法创建订阅聚合
│
└─▶ 订阅根发布 SubscriptionCreated 领域事件
│
▼
3. 仓储 SubscriptionRepository.save() 持久化
│
▼
4. 应用服务发布事件 → 其他上下文订阅
│
├─▶ 成长体系上下文:增加积分
└─▶ 签约分佣上下文:累计到作者佣金
战术设计常见误区
❌ 贫血模型 + 万能 Service
→ 实体只剩 getter/setter,业务逻辑全在 Service
→ 丢失了 DDD 最有价值的充血模型思想
❌ 聚合过大
→ 一个聚合包含 User + Order + Product + Payment
→ 事务冲突严重,聚合间耦合度高
❌ 聚合之间直接对象引用
→ order.customer.updateAddress() 这样调用
→ 应该用 customerId 查找然后独立操作
❌ 工厂只是构造函数的"套壳"
→ 没有增加任何业务不变量校验
→ 应该避免,直接用构造函数即可
❌ 领域服务替代实体行为
→ OrderService.cancel(order) 替代 order.cancel()
→ 除非确实跨聚合,否则应该让行为回归实体
❌ 领域事件用命令式命名
→ "ProcessPayment" 是命令,"PaymentProcessed" 才是事件
❌ 仓储返回数据库实体
→ 把 DO 泄漏到领域层
→ 仓储必须返回领域对象
战术设计与分层架构的映射
战术构件 位置 包命名示例
─────────────────────────────────────────────────────────
实体 / 聚合根 domain 层 subscription.Subscription
值对象 domain 层 subscription.SubscriptionId
工厂 domain 层 subscription.SubscriptionFactory
领域服务 domain 层 subscription.SubscriptionDomainService
领域事件 domain 层 subscription.events.SubscriptionCreated
仓储接口 domain 层 subscription.SubscriptionRepository
仓储实现 repository 层 subscription.SubscriptionRepositoryImpl
与其他 DDD 概念的关系
| 概念 | 关系 |
|---|---|
| 战略设计 | 战略先行,战术在限界上下文内展开 |
| 领域模型 | 七大构件是领域模型的砖块 |
| 领域建模 | 建模过程的产物就是战术设计构件 |
| 分层架构 | 战术构件分布到各层中实现 |
| 通用语言 | 战术构件的命名必须用通用语言 |
总结
核心:战术设计在限界上下文内用七大构件把业务建模成代码。
七大构件:实体、值对象、聚合、工厂、仓储、领域服务、领域事件。
精髓:
- 优先用充血模型让业务逻辑回归领域对象
- 聚合要小,通过 ID 引用解耦
- 领域层不依赖技术细节,通过仓储接口倒置依赖
- 用领域事件实现聚合间最终一致性
记住:战术设计不是目的,让业务表达更清晰、让代码更可维护才是。