DDD 基础:从战略设计到战术设计的完整体系
DDD 基础:从战略设计到战术设计的完整体系
整合 9-22 战略设计 + 9-24 战术设计(Day 1+Day 2)。
关键字:战略 5 件事 / 子域 3 类 / 通用语言 / 限界上下文 / 3 个空间 / 实体 / 值对象 / 聚合 / 聚合根。
这是 DDD 3 天计划的"基础篇"(Day 1 + Day 2),Day 3 是实战篇。
📌 一句话核心
"DDD 不是设计模式,不是架构,是一种服务业务的设计思想。"
战略设计 = 在业务视角下划清边界 + 统一语言;
战术设计 = 在技术视角下实现业务规则。
两者形成完整的'从业务到代码'的桥梁。"
📌 为什么写这篇
我花 3 天复习 DDD。Day 1 战略设计讲"业务怎么切",Day 2 战术设计讲"代码怎么写"。
但单独看都不够完整:
- 战略设计:知道"要做什么",但不会"怎么写代码"
- 战术设计:知道"怎么写",但不知道"为什么这么分"
今天把两天内容整合成一篇——从"业务边界"到"代码实体"的完整链路。
💎 第 1 部分:DDD 一句话定义
"DDD(领域驱动设计)= 围绕业务概念构建领域模型,控制业务复杂性的设计思想。"
1.1 核心三要素
| 要素 | 含义 |
|---|---|
| 领域(Domain) | 业务要解决的问题空间(如"无线测试") |
| 模型(Model) | 对领域的抽象表达(用业务语言) |
| 驱动(Driven) | 模型驱动代码,不是反过来 |
1.2 DDD 解决什么问题
| 问题 | 没有 DDD | 有 DDD |
|---|---|---|
| 业务理解 | 代码跟业务对不上 | 模型跟代码同构 |
| 团队沟通 | 各说各话 | 通用语言 |
| 服务划分 | 按技术分层 | 按业务能力 |
| 演进成本 | 改业务要改代码 | 模型稳,代码变 |
💎 第 2 部分:DDD 的两大设计层次
DDD
├─ 战略设计(Strategic Design) — 从业务视角出发
│
└─ 战术设计(Tactical Design) — 从技术视角出发| 层次 | 视角 | 关键产物 | 何时用 |
|---|---|---|---|
| 战略设计 | 业务 | 限界上下文、通用语言、子域 | 一开始(业务优先) |
| 战术设计 | 技术 | 聚合根、实体、值对象、领域服务 | 战略定下来后 |
2.1 战略 vs 战术 = 望远镜 vs 显微镜
| 维度 | 战略设计 | 战术设计 |
|---|---|---|
| 视角 | 业务 | 技术 |
| 粒度 | 粗(几个上下文) | 细(几十上百个类) |
| 抽象度 | 高(业务术语) | 低(代码术语) |
| 稳定性 | 稳定(几年不变) | 易变(每次重构) |
| 参与者 | 业务方主导 | 工程师主导 |
| 产出 | 图 + 文档 | 代码 + 测试 |
关键洞察:战略设计想清楚"做什么",战术设计想清楚"怎么做"。
💎 第 3 部分:战略设计的 5 件事
战略设计要做 5 件事,这是 DDD 战略设计的核心:
[业务视角]
↓
① 建立业务领域模型
② 划分领域边界
③ 建立通用语言
④ 划分限界上下文
⑤ 上下文之间怎么协作
↓
[产出]
├─ 领域模型图
├─ 通用语言词典
└─ 限界上下文图3.1 战略设计回答的 5 个核心问题
| # | 问题 | 对应概念 | 产出 |
|---|---|---|---|
| 1 | 业务有哪些子域? | 子域划分 | 子域地图 |
| 2 | 核心域是什么? | 识别核心竞争力 | 核心域清单 |
| 3 | 边界在哪? | 限界上下文 | 上下文图 |
| 4 | 语言怎么统一? | 通用语言 | 语言词典 |
| 5 | 上下文之间怎么协作? | 上下文映射 | 协作模式表 |
3.2 子域的 3 类划分
领域不断划分 → 子域 → 3 类(根据重要性和功能属性)。
| 子域类型 | 定义 | 关键属性 |
|---|---|---|
| 核心域(Core Domain) | 决定产品成功和核心竞争力 | 差异化 + 核心竞争力 |
| 通用域(Generic Domain) | 个性化需求少、被其他子域通用 | 低差异化 + 高复用 |
| 支撑域(Supporting Domain) | 业务必需但不是核心 | 中差异化 + 中复用 |
3.4 资源投入原则(80/15/5)
| 子域 | 资源投入 | 关注度 | 建设策略 |
|---|---|---|---|
| 核心域 | 80% 团队主力 | 最高(每天) | 自研 + 重兵投入 + 持续迭代 |
| 支撑域 | 15% 内部团队 | 中(周) | 部分自研 + 部分采购 |
| 通用域 | 5% 最小 | 低(季度) | 优先外包 / 套件 / 商业方案 |
核心原则:资源 = 子域重要性(不是代码量,不是工作量)。
3.5 子域识别 2 问
- 这是我们产品差异化的来源吗?
- 这是用户买我们不买别人的原因吗?
两个都不是 → 不是核心域。
3.6 子域拆分的 5 大终止条件
子域可以无限递归,但终止条件不是"子问题解决",而是 5 问:
| # | 检查项 | 是 → 停止 |
|---|---|---|
| 1 | 通用语言是否一致? | ✅ 停止 |
| 2 | 模型边界是否清晰? | ✅ 停止 |
| 3 | 数据所有权是否明确? | ✅ 停止 |
| 4 | 是否可以独立建模/交付/演进? | ✅ 停止 |
| 5 | 是否还有业务价值继续拆? | ❌ 停止(无价值) |
关键洞察:拆得好不好,看拆完能不能独立演进。
💎 第 4 部分:通用语言 + 限界上下文
4.1 通用语言精讲
通用语言 = 团队的统一语言,贯穿 DDD 设计的整个过程,让领域专家和开发无障碍沟通。
通用语言的 3 要素
| 要素 | 含义 | 例子 |
|---|---|---|
| 术语(Terminology) | 业务概念的名称 | "用例"、"判据"、"工单"、"环境" |
| 用例场景 | 业务发生的具体过程 | "用例已生成 → 评审 → 通过" |
| 统一性 | 同一概念只用一个词 | "WO-001"(不是"WO001" / "W001") |
通用语言的 2 大要求
- 限界上下文内术语统一,无二义性
- 核心领域概念要在代码中有对应表达(代码即文档)
通用语言词典模板
bounded_context: test-design
terms:
- name: 用例
english: TestCase
definition: 在某个测试场景下,验证某项功能的最小单元
code_class: src/domain/test_design/test_case.py::TestCase
aliases: [] # 禁用别名
deprecated_terms: ["测试用例", "Case"]"DDD 失败的常见原因 = 通用语言没在代码里表达。"
4.2 限界上下文精讲
限界上下文 = 用来确定语义所在的边界,用来定义通用语言所在边界。
限界上下文的一句话定义
"限界上下文 = 语义边界 + 通用语言边界 + 模型自治边界的三位一体。"
核心洞察:必须翻译
┌─────────────────────┐ ┌─────────────────────┐
│ 测试设计上下文 │ │ 测试分析上下文 │
│ │ │ │
│ "用例" = TestCase │ ←→ │ "用例" = TestSample│
│ (待测试的最小单元) │ 翻译 │ (历史执行的样本) │
│ │ │ │
└─────────────────────┘ └─────────────────────┘关键洞察:同一个词在不同上下文里有不同含义,必须翻译——这是 DDD 的核心动作。
限界上下文 vs 子域
| 维度 | 子域 | 限界上下文 |
|---|---|---|
| 概念 | 业务概念 | 解决方案边界 |
| 数量关系 | 不机械 1:1 | N:N 关系 |
| 1 个子域可拆多个上下文 | 1 个上下文可包含多个子域 |
"一个限界上下文可能包含多个子域,也可能只包含 1 个。"
💎 第 5 部分:3 个空间的清晰区分
子域是问题空间的业务分解,限界上下文是解决方案空间的模型和语言边界,微服务是物理部署边界。三者相关,但不能混为一谈。
5.1 3 个空间总览
[问题空间] [解决方案空间] [物理部署空间]
"业务有哪些问题" → "我们用什么模型解决" → "程序怎么部署运行"
领域 / 子域 限界上下文 / 通用语言 微服务 / 进程 / 容器
核心域 / 支撑域 上下文映射 网络 / API
通用域5.2 3 个空间对比表
| 空间 | 关注点 | 核心概念 |
|---|---|---|
| 问题空间 | 业务本身有哪些问题 | 领域、子域、核心域、支撑域、通用域 |
| 解决方案空间 | 我们用什么模型解决 | 限界上下文、通用语言、上下文映射 |
| 物理部署空间 | 程序怎么部署运行 | 微服务、进程、容器、网络 |
5.3 三者关系
子域 → 限界上下文:业务驱动模型(1:N 或 N:N)
限界上下文 → 微服务:模型驱动部署(1:1 为主)关键洞察:好的 DDD 实践者,3 个空间都看得见;差的,只盯一个空间。
5.4 上下文协作的 7 模式
| 模式 | 含义 |
|---|---|
| 共享内核 | 两个上下文共享一部分代码/模型 |
| 客户-供应商 | 上游提供数据,下游消费 |
| 防腐层 | 翻译外部模型,保护核心 |
| 开放主机服务 | 提供标准 API 给外部 |
| 发布语言 | 用标准格式交换数据(JSON/Protobuf) |
| 分离方式 | 两个上下文完全独立 |
| 大泥球 | 没有清晰边界(反模式) |
关键洞察:大多数上下文协作 = 防腐层 + 开放主机服务 + 发布语言。
💎 第 6 部分:实体 + 值对象(战术设计基础)
用户原话:"实体和值对象是从业务模型向系统落地过程中的设计的一个过程。"
6.1 实体(Entity)一句话定义
"实体 = 拥有唯一标识符的领域重要对象,包含属性、操作或行为的载体。"
6.2 值对象(Value Object)一句话定义
"值对象 = 描述领域的特定方面,没有标识符,通过属性识别,不可变。"
6.3 实体 vs 值对象 对比总览
| 维度 | 实体 | 值对象 |
|---|---|---|
| 标识符 | 有唯一 ID | 无 ID |
| 生命周期 | 跨越多个状态 | 跟宿主(实体)同生死 |
| 可变性 | 可变(状态变化) | 不可变(只能整体替换) |
| 比较方式 | 通过 ID 比较 | 通过值比较 |
| 业务逻辑 | 多(充血模型) | 几乎没有 |
| 例子 | 用户、订单、商品 | 地址、金额、颜色 |
| 数据建模 | 独立表 | 嵌入实体表 |
6.4 充血模型 vs 贫血模型
| 模型 | 实体长什么样 | 业务逻辑在哪 |
|---|---|---|
| 贫血模型 | 只有 getter/setter | Service 层(贫血) |
| 充血模型 | 实体有完整业务逻辑 | 实体类(DDD 推荐) |
| 胀血模型 | 实体啥都干 | 实体类(过载) |
"DDD = 充血模型;贫血模型是 DDD 的失败。"
6.5 实体的实战代码示例
class WorkOrder:
"""工单实体 — 充血模型"""
def __init__(self, work_order_id: str, title: str, owner: str):
self.work_order_id = work_order_id # 唯一标识符
self.title = title
self.owner = owner
self.state = "S0" # 初始状态
self.created_at = datetime.now()
# 业务行为 1:开始
def start(self):
if self.state != "S0":
raise InvalidStateError(f"当前状态 {self.state} 不能 start")
self.state = "S1"
# 业务行为 2:完成
def complete(self):
if self.state not in ["S3", "S4"]:
raise InvalidStateError(f"当前状态 {self.state} 不能 complete")
self.state = "S4"
# 业务规则:校验金额
def validate_amount(self, amount: float):
if amount <= 0:
raise ValueError("金额必须大于 0")6.6 值对象的实战代码示例
from dataclasses import dataclass
@dataclass(frozen=True) # 不可变
class Address:
"""地址值对象"""
province: str
city: str
street: str
zip_code: str
def __post_init__(self):
if not self.province or not self.city:
raise ValueError("省和市不能为空")
@dataclass(frozen=True)
class Money:
amount: float
currency: str # "CNY" / "USD"
def __add__(self, other: "Money") -> "Money":
if self.currency != other.currency:
raise ValueError("货币不同不能相加")
return Money(self.amount + other.amount, self.currency)6.7 DDD vs 数据建模思维
| 思维 | 出发点 | 顺序 |
|---|---|---|
| 传统数据建模 | 数据库表 | 表 → 类 → 业务 |
| DDD 领域建模 | 领域模型 | 业务 → 类 → 表 |
关键洞察:DDD 是"业务驱动",数据建模是"表驱动"。
6.8 实体和值对象的划分原则
"实体和值对象的划分,要根据实际场景区分。"
这个对象有没有唯一标识符?
├─ 是 → 实体
└─ 否 → 值对象重要提醒:同一概念在不同上下文里可能是不同的类型:
| 概念 | 在"用户管理"上下文 | 在"订单管理"上下文 |
|---|---|---|
| 地址 | 值对象(描述用户) | 实体(可改) |
| 金额 | 值对象(用户余额) | 实体(交易历史) |
💎 第 7 部分:聚合 + 聚合根(战术设计核心)
"聚合是一组相关领域对象的集合,是业务不变量的一致性边界、事务边界和生命周期边界。"
7.1 聚合的 3 大边界
| 边界类型 | 含义 | 例子 |
|---|---|---|
| 业务不变量一致性边界 | 业务规则不能被破坏 | "订单总金额 = 所有订单项之和" |
| 事务边界 | 一起成功或一起失败 | 创建订单 + 订单项 = 1 个事务 |
| 生命周期边界 | 聚合内的对象同生死 | 订单删除 → 所有订单项删除 |
关键洞察:3 个边界 = 1 个聚合(同时满足)。
7.2 聚合的 2 大设计原则
- 高内聚:聚合内的对象必须强相关
- 低耦合:聚合之间尽量弱依赖
7.3 一个微服务可以包含多个聚合
[微服务 A]
├─ [聚合 1:订单聚合] (Order + OrderItem + Address)
└─ [聚合 2:用户聚合] (User + Profile + Address)关键洞察:微服务 ≠ 聚合(微服务更粗)。
7.4 聚合根(Aggregate Root)的 4 大属性
"聚合根 = 聚合内具有全局唯一标识 + 独立生命周期的实体,负责协调聚合内的所有对象。"
| 属性 | 含义 |
|---|---|
| 全局唯一标识 | 跨聚合也能识别 |
| 独立生命周期 | 自己的 CRUD |
| 聚合内唯一 | 1 个聚合只有 1 个聚合根 |
| 业务规则执行者 | 跨子对象的业务规则 |
7.5 关键区分:聚合内 vs 聚合间
用户原话:"聚合根对聚合内的实体和值对象采用对象引用的方式进行组织协调。聚合根和聚合根之间通过 id 关联的方式进行协调,不是对象引用。"
[聚合内]
聚合根 → 实体(对象引用)
聚合根 → 值对象(对象引用)
↓
[聚合间]
聚合根 A 持有 聚合根 B 的 ID(不直接引用)关键洞察:聚合内 = 对象引用(紧密耦合);聚合间 = ID 关联(松散耦合)。
7.6 为什么聚合根之间用 ID 关联?
用户原话:"聚合根的主要模板是为了避免由于复杂数据模型缺失统一业务规则控制而导致的不一致性问题。"
| 用对象引用 | 用 ID 关联 |
|---|---|
| ❌ 强耦合(跨聚合的修改) | ✅ 弱耦合 |
| ❌ 跨聚合事务(难) | ✅ 单聚合事务 |
| ❌ 删除一个聚合会带飞另一个 | ✅ 各自独立 |
| ❌ 难以独立部署 | ✅ 各自独立演进 |
7.7 聚合的完整实战代码
class Order:
"""订单聚合根"""
def __init__(self, order_id: str, customer_id: str):
self.order_id = order_id
# 聚合根之间:通过 ID 关联,不是对象引用
self.customer_id = customer_id
# 聚合内的对象(对象引用)
self.items: list[OrderItem] = []
self.shipping_address: Address | None = None
self.status = "Pending"
def add_item(self, product_id: str, quantity: int, price: Money):
if self.status != "Pending":
raise InvalidStateError("只有 Pending 状态可加订单项")
item = OrderItem(product_id=product_id, quantity=quantity, unit_price=price)
self.items.append(item)
def total_amount(self) -> Money:
total = Money(0, "CNY")
for item in self.items:
total = total + item.subtotal()
return total
def validate(self):
# 业务规则:订单总金额必须 > 0(在聚合根里)
if self.total_amount().amount <= 0:
raise BusinessRuleError("订单总金额必须大于 0")
@dataclass
class OrderItem:
product_id: str # 跨聚合用 ID
quantity: int
unit_price: Money
def subtotal(self) -> Money:
return Money(self.quantity * self.unit_price.amount, self.unit_price.currency)关键:
Order.customer_id是 ID(不是Customer对象)OrderItem.product_id是 ID(不是Product对象)Address/Money是 值对象(不可变)- 业务规则(总金额 > 0)在
Order.validate()里(聚合根)
7.8 聚合的 4 大反模式
| 反模式 | 表现 | 后果 |
|---|---|---|
| 跨聚合对象引用 | Order.customer 直接是 Customer 对象 | 强耦合,数据不一致 |
| 聚合过大 | 1 个聚合包含 20+ 实体 | 改不动,事务过大 |
| 没有聚合边界 | 所有实体直接放一起 | 业务规则失控 |
| 聚合根被绕过 | 直接改聚合内非聚合根对象 | 业务规则失效 |
💎 第 8 部分:Day 1 + Day 2 完整总结
8.1 战略 vs 战术 完整对比
| 维度 | 战略设计 | 战术设计 |
|---|---|---|
| 视角 | 业务 | 技术 |
| 回答 | 业务怎么切 | 代码怎么写 |
| 关键产物 | 子域、限界上下文、通用语言 | 聚合根、实体、值对象 |
| 粒度 | 粗(几个上下文) | 细(几十上百个类) |
| 稳定性 | 几年不变 | 每次重构 |
| 参与者 | 业务方 + 架构师 | 工程师 |
| 核心问 | 5 问(子域/核心域/边界/语言/协作) | 4 概念(实体/值对象/聚合/聚合根) |
8.2 DDD 完整输出清单
8.3 DDD 跟之前项目的关联
- 8 周深读 AgentScope 2.0 — 代码里大量 DDD 影子(
agent/= Agent 上下文,pipeline/= 业务流上下文) - 指挥中心 — 多模块拆分 = 微服务架构
- 工单状态机 S0-S6 — 业务流 = 状态机 = 限界上下文的具象化
关键洞察:之前项目不自觉地用到了 DDD 思想——只是没显式说出来。
8.4 给工程师的行动建议
| 时间 | 行动 |
|---|---|
| 本周 | 找一个项目,画一张"领域地图" |
| 本月 | 写一份"通用语言词典"(YAML) |
| 本季度 | 识别核心域,80% 资源集中 |
| 半年内 | 把通用语言落到代码里(类名 / 字段名) |
| 持续 | 实体充血(不写贫血)+ 聚合根协调(对象引用 + ID 关联) |
💬 最后一句话
"DDD 不是设计模式,不是架构,是一种服务业务的设计思想。"
"战略设计想清楚'做什么',战术设计想清楚'怎么做'。"
"业务优先,技术其次。代码跟业务同构,才是 DDD 的灵魂。"
"3 个空间(问题/解决方案/物理部署),3 种关注,3 种粒度。"
"AI 让写代码变快,但没让'业务怎么切'变容易——DDD 是 AI 时代最稀缺的视角。"
DDD 基础篇(Day 1 + Day 2)整合的核心目标:把"领域怎么设计"这件事讲清楚——业务优先、技术其次。