Tell-Don't-Ask + Law of Demeter = 行为下沉与封装
一、业务问题
两个反模式让 DDD 变味:
反模式 1:贫血模型
python
class Order:
status = "NEW"
# 只有 getter/setter
def confirm_order(order):
if order.status == "NEW":
order.status = "CONFIRMED"反模式 2:链式访问
python
city = order.customer.address.city # 跨多层访问Vernon 在 Ch 5 和 Ch 10 给出两个原则:
- Tell-Don't-Ask(不要问,告诉)
- Law of Demeter(迪米特法则)
二、控制论视角
TDA + LoD = 控制律封装(两个不同维度的封装)。
TDA(Tell-Don't-Ask):
- 行为下沉到对象
- 控制论对位 = Ch 4 §4.6 反馈(控制器封装)
- 控制器不外泄内部状态,外部只给目标
LoD(Law of Demeter):
- 不跨级访问
- 控制论对位 = Ch 6 §6.3 解耦(跨对象不深入)
- 控制器只和自己直接相关的对象通信Vernon 的两个原则恰好对应控制论的两条原理:
- TDA = 反馈封装(Ch 4)
- LoD = 解耦(Ch 6)
钱学森 Ch 4 §4.6:
"没有反馈,就没有自动控制"——TDA 让对象的"反馈"封装在内部。
钱学森 Ch 6 §6.3:
"只影响和它们相应的受控量"——LoD 让控制器只访问直接朋友,不跨级。
三、Tell-Don't-Ask 详解
3.1 核心思想
原则:
- 对象暴露行为,不暴露数据
- 不问对象"你有什么"
- 告诉对象"做什么"
反模式(Ask):
if order.getItems().size() > 0 and order.getStatus() == "NEW":
order.setStatus("CONFIRMED")
正模式(Tell):
order.confirm() # 内部判断 + 修改3.2 为什么有效
Ask 模式的 3 个问题:
1. 业务规则散落在多个地方
2. 修改时容易遗漏规则
3. 实体的行为 = 0(贫血)
Tell 模式的优势:
1. 业务规则集中在对象内部
2. 修改时对象自己处理
3. 实体有丰富行为(充血)3.3 实现模式
python
# 基础:行为下沉
class Order:
def confirm(self):
# 业务规则在内部
if not self._is_confirmable():
raise BusinessRuleViolation("Cannot confirm order")
self.status = "CONFIRMED"
self.add_event(OrderConfirmed(self.id))
def _is_confirmable(self):
return self.status == "NEW" and len(self.items) > 0四、Law of Demeter 详解
4.1 核心思想
原则:只和直接朋友说话(Only talk to your immediate friends)
允许的调用:
- 自己的方法
- 自己的字段
- 自己创建的对象
- 自己的参数
禁止的调用:
- 跨级访问(链式)
- 全局变量
- "朋友的朋友"4.2 反例与正例
python
# ❌ 反例:链式访问
city = order.customer.address.city
country = order.customer.address.country.name
phone = order.customer.profile.phone.number
# ✓ 正例:通过聚合根
city = order.getCustomerCity()
country = order.getCustomerCountry()
phone = order.getCustomerPhone()4.3 何时违反 LoD
反模式:链式访问
- 暴露内部结构
- 耦合泄露
- 修改内部结构影响外部
反例的"诱惑":
- 简单直接
- 不写额外方法
- "反正内部就是这个结构"
但代价:
- 修改 Order 的 Customer 时,所有链式访问都要改
- 业务逻辑散落
- 测试困难五、两个原则的协同
TDA(行为下沉):
- 业务规则在对象内
- 外部只发命令
LoD(封装):
- 外部不访问内部结构
- 通过聚合根访问
两个原则 = 充血模型 + 封装完整正例:
python
class Order:
def __init__(self, customer_id):
self._customer_id = customer_id
self._items = []
self._status = "NEW"
def add_item(self, product_id, quantity, price):
# 业务规则:已支付不能加
if self._status == "PAID":
raise BusinessRuleViolation("Cannot add items to paid order")
self._items.append(OrderItem(product_id, quantity, price))
def confirm(self):
# 业务规则:空订单不能确认
if len(self._items) == 0:
raise BusinessRuleViolation("Cannot confirm empty order")
# 业务规则:非 NEW 状态不能确认
if self._status != "NEW":
raise BusinessRuleViolation("Only NEW orders can be confirmed")
self._status = "CONFIRMED"
self.add_event(OrderConfirmed(self.id))
def get_total_amount(self):
# 聚合内计算
return sum(item.subtotal() for item in self._items)
# TDA:暴露业务方法
# LoD:不暴露内部 items 的细节六、工程实践
实战 1:识别贫血实体
症状检查:
□ 实体只有 getter/setter?
□ 业务逻辑在 Service 层?
□ 单元测试只测 getter/setter?
□ 行为方法 0 个?
修复:
1. 把行为从 Service 移到实体
2. 实体 = 行为 + 状态
3. Service 只做跨实体编排实战 2:避免链式访问
检查:
- grep "\.get.*\.\(get\|do\)" → 发现链式
- 重构:每个跨级访问 = 聚合根的一个方法
反例 → 正例:
# 反
if order.customer.profile.subscription.is_premium:
...
# 正
if order.is_premium_customer():
...实战 3:行为下沉的判断
问 3 个问题:
1. 这个操作只涉及一个实体?
→ 实体方法
2. 这个操作涉及多个实体?
→ 领域服务
3. 这个操作跨 BC?
→ 应用服务 + ACL七、性能指标
| 指标 | 衡量 |
|---|---|
| 贫血实体率 | 多少实体只有 getter/setter(应该 0) |
| 行为下沉率 | 多少行为在实体上 vs Service |
| 链式访问 | 多少代码有链式访问(应该 0) |
| 聚合根封装 | 多少聚合有清晰的外部接口 |
八、边界判断
何时用 TDA:
- 任何业务规则
- 任何状态变化
何时不用 TDA(用 Service):
- 涉及多个实体
- 跨 BC 协调
- 基础设施相关
何时用 LoD:
- 任何聚合内的对象
- 任何"朋友"访问
何时可以违反 LoD:
- 同模块内的紧耦合类
- 性能关键路径(用内联)
- DTO 内部(不是领域对象)
九、与其他 L 层方法论的关系
| 方法论 | 关系 |
|---|---|
| entity-vo | TDA 让实体有行为(区别于值对象) |
| aggregate | TDA 应用到聚合根 |
| aggregate-rules-of-thumb | TDA + LoD 是聚合 4 规则的子集 |
| domain-vs-application-service | TDA 决定什么在实体/服务 |
十、钱学森原话
Ch 6 §6.3(解耦): "只影响和它们相应的受控量"——控制律不应跨级泄露。
Ch 6 §6.7(耦合取舍): "受控对象中的相互关联并不总是有害的……有些耦合作用是有益的。"
十一、总结
TDA + LoD = 充血模型 + 封装:
| 原则 | 核心 |
|---|---|
| TDA | 行为下沉到对象,外部只发命令 |
| LoD | 外部只访问聚合根,不跨级 |
核心原则:
- 行为在对象内——不要问对象的状态再决策
- 访问通过聚合根——不要链式访问
- 封装是义务——保护内部结构
一句话总结:Tell-Don't-Ask 让对象有行为,Law of Demeter 让封装完整。