Skip to content

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-voTDA 让实体有行为(区别于值对象)
aggregateTDA 应用到聚合根
aggregate-rules-of-thumbTDA + LoD 是聚合 4 规则的子集
domain-vs-application-serviceTDA 决定什么在实体/服务

十、钱学森原话 ​

Ch 6 §6.3(解耦): "只影响和它们相应的受控量"——控制律不应跨级泄露。

Ch 6 §6.7(耦合取舍): "受控对象中的相互关联并不总是有害的……有些耦合作用是有益的。"

十一、总结 ​

TDA + LoD = 充血模型 + 封装:

原则核心
TDA行为下沉到对象,外部只发命令
LoD外部只访问聚合根,不跨级

核心原则:

  • 行为在对象内——不要问对象的状态再决策
  • 访问通过聚合根——不要链式访问
  • 封装是义务——保护内部结构

一句话总结:Tell-Don't-Ask 让对象有行为,Law of Demeter 让封装完整。

从名家方法论与工程化思路中蒸馏出自己的工程体系。