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 层链条全部隐藏了,结果每个中间类都加了一堆「透传方法」。代码变得臃肿不堪。后来我意识到,有些链条其实是「不合理的职责分配」导致的。这时候应该先考虑「移动方法」或「提取类」,而不是单纯隐藏委托。
知识体系图
下面这张图展示了消息链重构的核心逻辑:
避坑指南
最后,分享几个我自己的经验教训:
- 不要过度隐藏:如果链条只有 2 层,而且不太可能变化,强行隐藏反而增加代码量。我一般以 3 层为界。
- 注意方法命名:隐藏委托后,方法名要能清晰表达意图。比如
getCustomerCityName()就比getCityName()更明确,因为你知道它是从 Customer 来的。 - 结合其他重构手法:如果发现某个中间类「无事可做」,全是透传方法,那可能是「中间人」坏味道。这时候应该考虑「移除中间人」。
- 测试先行:重构前,先写测试。确保重构后行为不变。我吃过亏,改完代码发现某个链条被截断了,但有个地方忘了改,结果线上出了 bug。
嗯,隐藏委托关系,说白了就是「让该知道的人知道,不该知道的人别瞎打听」。代码如此,做人也是这个道理。