52、代码坏味道:过长的消息链 - 实战隐藏委托关系

你有没有见过这样的代码?

String cityName = order.getCustomer().getAddress().getCity().getName();

一行代码,四个点。每次看到这种链式调用,我心里就咯噔一下。这玩意儿,我们叫它「过长的消息链」。说白了,就是一个对象通过一连串的 getter 去拿深层的数据。我早期做项目时,经常写出这种代码,觉得「一行搞定,多简洁」。后来被坑惨了。

什么是消息链?

消息链,就是对象 A 调用对象 B,对象 B 再调用对象 C,一路往下走。代码里全是 a.getB().getC().getD() 这种结构。

为什么会成为坏味道?

  • 耦合太深:调用方知道了整个链条的结构。只要中间任何一个环节变了,调用方就得改。
  • 可读性差:读代码的人得一路追踪下去,才能明白你到底要什么。
  • 违反迪米特法则:一个对象应该只跟它的直接朋友通信,别去跟朋友的朋友的朋友瞎聊。

核心问题:调用方不该知道链条的细节。它只想要结果,不关心你怎么拿到的。

实战案例:一个订单系统

假设我们有一个订单系统。需求是:根据订单,获取客户所在城市的名称。

原始代码长这样:

// 客户端代码
public class OrderService {
    public String getCustomerCityName(Order order) {
        // 过长的消息链
        return order.getCustomer().getAddress().getCity().getName();
    }
}

// 领域模型
public class Order {
    private Customer customer;
    public Customer getCustomer() { return customer; }
}

public class Customer {
    private Address address;
    public Address getAddress() { return address; }
}

public class Address {
    private City city;
    public City getCity() { return city; }
}

public class City {
    private String name;
    public String getName() { return name; }
}

这段代码,我见过无数次。每次重构,我都会问自己一个问题:OrderService 真的需要知道 Customer、Address、City 这些类吗? 它只是想要一个城市名字而已。

重构手法:隐藏委托关系

隐藏委托关系的核心思想很简单:在中间对象上添加方法,把链条「截断」。让每个对象只暴露它直接能提供的服务。

具体怎么做?我们一步步来。

第一步:在 Address 上添加方法

Address 应该直接提供城市名称,而不是让调用方去拿 City 对象再 getName。

public class Address {
    private City city;
    
    // 新增:隐藏委托
    public String getCityName() {
        return city.getName();
    }
    
    // 原有的 getter 可以保留,但客户端不应该直接调用
    public City getCity() { return city; }
}

第二步:在 Customer 上添加方法

Customer 应该直接提供城市名称,而不是让调用方去拿 Address 再 getCityName。

public class Customer {
    private Address address;
    
    // 新增:隐藏委托
    public String getCityName() {
        return address.getCityName();
    }
    
    public Address getAddress() { return address; }
}

第三步:在 Order 上添加方法

Order 应该直接提供城市名称,而不是让调用方去拿 Customer 再 getCityName。

public class Order {
    private Customer customer;
    
    // 新增:隐藏委托
    public String getCustomerCityName() {
        return customer.getCityName();
    }
    
    public Customer getCustomer() { return customer; }
}

第四步:修改客户端代码

现在,客户端代码变得极其简单:

public class OrderService {
    public String getCustomerCityName(Order order) {
        // 一行搞定,不再有链条
        return order.getCustomerCityName();
    }
}

你看,调用方只需要知道 Order 这一个类。它不再关心 Customer、Address、City 的存在。这就是隐藏委托关系的威力。

我的习惯:每次写代码时,如果发现要连续点两次以上,我就会停下来想一想:这个链条能不能砍掉?能不能在源头加个方法?

重构前后的对比

维度 重构前 重构后
耦合度 高:客户端依赖 4 个类 低:客户端只依赖 1 个类
可读性 差:需要理解整个链条 好:意图一目了然
修改影响 大:任何中间类变化都要改客户端 小:变化被封装在中间类内部
测试难度 高:需要 mock 整个链条 低:只需要 mock 直接依赖

什么时候该用?什么时候不该用?

隐藏委托关系不是银弹。我个人经验是:

  • 该用的时候:链条长度超过 2 个点,且链条上的对象都是「数据容器」(没有太多行为)。这时候隐藏委托能大幅降低耦合。
  • 不该用的时候:如果链条上的对象本身有丰富的业务行为,强行隐藏可能会丢失语义。比如 person.getCar().drive(),这里的 drive() 是 Car 的行为,不应该被提升到 Person 上。

我曾经踩过的坑:有一次,我把一个 5 层链条全部隐藏了,结果每个中间类都加了一堆「透传方法」。代码变得臃肿不堪。后来我意识到,有些链条其实是「不合理的职责分配」导致的。这时候应该先考虑「移动方法」或「提取类」,而不是单纯隐藏委托。

知识体系图

下面这张图展示了消息链重构的核心逻辑:

消息链重构:隐藏委托关系 重构前:长消息链 order.getCustomer().getAddress() .getCity().getName() 重构 重构后:隐藏委托 order.getCustomerCityName() (一行搞定,无链条) 核心原则 每个对象只暴露它直接能提供的服务 调用方只跟它的直接朋友通信

避坑指南

最后,分享几个我自己的经验教训:

  • 不要过度隐藏:如果链条只有 2 层,而且不太可能变化,强行隐藏反而增加代码量。我一般以 3 层为界。
  • 注意方法命名:隐藏委托后,方法名要能清晰表达意图。比如 getCustomerCityName() 就比 getCityName() 更明确,因为你知道它是从 Customer 来的。
  • 结合其他重构手法:如果发现某个中间类「无事可做」,全是透传方法,那可能是「中间人」坏味道。这时候应该考虑「移除中间人」。
  • 测试先行:重构前,先写测试。确保重构后行为不变。我吃过亏,改完代码发现某个链条被截断了,但有个地方忘了改,结果线上出了 bug。

嗯,隐藏委托关系,说白了就是「让该知道的人知道,不该知道的人别瞎打听」。代码如此,做人也是这个道理。

公众号:蓝海资料掘金营,微信deep3321