8. 封装(二):以查询取代临时变量、提炼类、将类内联化、隐藏委托关系
这一章我们继续聊封装。上一讲我们处理了数据和行为之间的边界,这一讲我们深入到更细的粒度——临时变量、类的边界、以及对象之间的委托关系。
说实话,封装这件事,做得好了,代码像搭积木一样清晰;做得不好,就像一团乱麻,改一个地方牵出一堆问题。我见过太多项目,就是因为封装不到位,最后重构成本高得吓人。
好,我们一个一个来看。
8.1 以查询取代临时变量
临时变量是代码里最常见的“坏味道”之一。你想想看,一个方法里写了七八个临时变量,每个变量只为了计算下一步的输入,读起来就像在解一道数学题。
核心思想:把临时变量的计算逻辑提炼成一个独立的查询方法。
举个例子:
// 重构前
public double getPrice() {
double basePrice = quantity * itemPrice;
double discount = Math.max(0, basePrice - 100) * 0.05;
double shippingCost = Math.min(basePrice * 0.1, 5);
return basePrice - discount + shippingCost;
}
这段代码里,basePrice 被用了三次。每次读这段代码,你都得在心里重新算一遍 basePrice 是什么。更麻烦的是,如果以后价格计算规则变了,你得在这个方法里找来找去。
重构后:
// 重构后
public double getPrice() {
return getBasePrice() - getDiscount() + getShippingCost();
}
private double getBasePrice() {
return quantity * itemPrice;
}
private double getDiscount() {
return Math.max(0, getBasePrice() - 100) * 0.05;
}
private double getShippingCost() {
return Math.min(getBasePrice() * 0.1, 5);
}
关键点:每个查询方法只做一件事,并且方法名就是它的文档。你不需要看实现,就知道它返回什么。
我在项目中遇到过一种情况:一个方法里有十几个临时变量,层层依赖。我当时花了整整一个下午,才把它们全部拆成查询方法。拆完之后,那个类从 300 行变成了 500 行,但每个方法都短小精悍,后来维护的人都说好。
什么时候该做?
- 临时变量被多次引用
- 临时变量的计算逻辑可能变化
- 你想让方法更短、更聚焦
8.2 提炼类
一个类承担了太多职责,就会变得臃肿。我见过一个类,里面既有订单逻辑,又有支付逻辑,还有物流逻辑——说白了,它什么都管,但什么都管不好。
核心思想:把一组紧密相关的字段和方法,从原类中抽出来,形成一个新的类。
举个实际例子:
// 重构前:Person 类太臃肿
public class Person {
private String name;
private String officeAreaCode;
private String officeNumber;
public String getTelephoneNumber() {
return "(" + officeAreaCode + ") " + officeNumber;
}
// 还有一堆其他方法...
}
这里,电话号码相关的字段和方法其实可以独立出来:
// 重构后
public class TelephoneNumber {
private String areaCode;
private String number;
public String getTelephoneNumber() {
return "(" + areaCode + ") " + number;
}
}
public class Person {
private String name;
private TelephoneNumber telephoneNumber;
public String getTelephoneNumber() {
return telephoneNumber.getTelephoneNumber();
}
}
你看,提炼之后,TelephoneNumber 可以独立测试、独立复用。如果以后电话号码格式变了,你只需要改一个地方。
注意:提炼类不是越多越好。我见过有人把两个字段就抽成一个类,结果类爆炸了。提炼的边界应该是“一组逻辑上紧密相关的数据和行为”。
我曾经在一个电商项目中,把订单类里的地址信息提炼成 Address 类。一开始觉得多此一举,后来发现地址校验、格式化、打印标签这些功能都可以复用,省了不少事。
8.3 将类内联化
有提炼就有内联。有时候,一个类太“瘦”了,反而增加了复杂度。
核心思想:如果一个类做的事情太少,不值得独立存在,就把它的字段和方法合并到使用它的类中。
举个例子:
// 重构前:ShippingRate 类太单薄
public class ShippingRate {
private double rate;
public double getRate() { return rate; }
}
public class Order {
private ShippingRate shippingRate;
public double getShippingCost() {
return shippingRate.getRate() * weight;
}
}
如果 ShippingRate 只有这一个字段,而且只在 Order 中使用,那不如直接内联:
// 重构后
public class Order {
private double shippingRate;
public double getShippingCost() {
return shippingRate * weight;
}
}
判断标准:如果一个类没有自己的生命周期,没有独立的行为,只是作为数据的“搬运工”,那就该内联掉。
嗯,这里要注意:内联和提炼是相反的操作。你不需要纠结哪个更好,关键是看当前类的职责是否清晰。我个人的习惯是:先提炼,等代码稳定了,再回头看哪些类可以内联。
8.4 隐藏委托关系
这是封装中最容易被忽视的一点。你想想看,客户端代码要调用一个功能,结果要经过好几层对象才能拿到数据——这就是“委托链”。
核心思想:在服务对象上直接提供客户端需要的方法,隐藏内部的委托关系。
看代码:
// 重构前:客户端需要知道委托关系
public class Person {
private Department department;
public Department getDepartment() { return department; }
}
public class Department {
private Person manager;
public Person getManager() { return manager; }
}
// 客户端调用
Person person = ...;
Person manager = person.getDepartment().getManager();
客户端代码必须知道 Person 有 Department,Department 有 Manager。如果以后部门结构变了,所有客户端都得改。
重构后:
// 重构后:隐藏委托关系
public class Person {
private Department department;
public Person getManager() {
return department.getManager();
}
}
// 客户端调用
Person person = ...;
Person manager = person.getManager();
客户端不再需要知道 Department 的存在。这就是“最少知识原则”的体现。
避坑指南:我曾经在一个项目中,把所有的委托关系都隐藏了,结果服务类变得很“胖”,每个方法都是转发。后来我意识到,隐藏委托关系不是无脑转发,而是只隐藏那些“客户端不需要知道”的中间层。
知识体系总览
下面这张图总结了本章四个重构手法的关系:
总结
这四个手法,说白了都是在回答一个问题:代码的边界在哪里?
- 以查询取代临时变量:把计算逻辑封装成方法,让方法名代替注释。
- 提炼类:把一组紧密相关的数据和行为,从大类中分离出来。
- 将类内联化:如果一个类太“瘦”,就把它合并回去。
- 隐藏委托关系:让客户端只依赖直接的对象,不关心内部结构。
我个人觉得,封装这件事,没有绝对的对错。关键是看你的代码在哪个阶段。项目初期,可以稍微松散一点;到了维护期,封装得越干净,改起来越轻松。
好,这一讲就到这里。记住一句话:好的封装,让调用者不需要知道实现细节。
公众号:蓝海资料掘金营,微信deep3321