9、基本重构手法(下):提炼类、内联类、隐藏委托关系、移除中间人、引入外部函数、引入本地扩展
好,咱们接着聊重构手法。上一章讲了提炼函数、内联函数这些基础操作,这一章我们继续深入,把剩下的六个手法讲透。这些手法说白了,都是在处理「类与类之间的关系」——什么时候该拆,什么时候该合,什么时候该藏起来。
我个人习惯把这一组手法叫做「边界调整器」。它们帮你理清代码的职责边界,让每个类都只做自己该做的事。
提炼类(Extract Class)
一个类承担了太多职责,就该拆了。这跟提炼函数是一个道理——只不过这次拆的是类。
什么时候该拆?
- 类里有两组数据,它们经常一起变化
- 类里有几个方法,只用到部分字段
- 类的名字已经很难概括它做的事了
我在项目中遇到过这样一个场景:一个 Order 类,既管订单信息,又管支付逻辑。后来支付方式从信用卡扩展到支付宝、微信,这个类就越来越臃肿。拆出一个 Payment 类后,两边都清爽了。
重构前:
public class Order {
private String orderId;
private double amount;
private String paymentMethod;
private String cardNumber;
private String cardExpiry;
public void processPayment() {
// 处理信用卡支付
}
}
重构后:
public class Order {
private String orderId;
private double amount;
private Payment payment;
public void processPayment() {
payment.process();
}
}
public class Payment {
private String method;
private String cardNumber;
private String cardExpiry;
public void process() {
// 处理支付
}
}
我的经验:拆类的时候,别急着一步到位。先看看新类能不能独立测试,能不能单独复用。如果拆出来的类只被原类使用,那可能拆早了。
内联类(Inline Class)
有拆就有合。如果一个类已经没什么存在价值了——比如它只被一个类使用,或者它的职责已经被其他类吸收了——那就把它合并回去。
说白了,内联类就是提炼类的逆操作。我见过不少代码,拆类拆得太细,结果类之间全是 getter/setter 链条,反而增加了复杂度。
注意:内联类不是偷懒。如果一个类确实有独立存在的理由——比如它有独立的生命周期、独立的业务规则——那就别合。我曾经为了「代码整洁」把一个 Address 类内联回 Customer,结果后来地址格式变了,改得我头皮发麻。
隐藏委托关系(Hide Delegate)
这个手法很有意思。它解决的是「消息链」问题——A 调用 B,B 调用 C,C 调用 D。一旦中间某个环节变了,整条链都得改。
核心思想:让客户端只跟直接对象打交道,不要知道内部细节。
举个例子:
// 客户端代码
Person person = new Person();
Department dept = person.getDepartment();
Manager mgr = dept.getManager();
String name = mgr.getName();
这段代码里,客户端知道了 Person 有 Department,Department 有 Manager。如果哪天 Department 改了,所有客户端都得改。
隐藏委托关系后:
// Person 类增加一个方法
public String getManagerName() {
return department.getManager().getName();
}
// 客户端代码
Person person = new Person();
String name = person.getManagerName();
嗯,这里要注意:隐藏委托关系不是让你把所有方法都加一层包装。只隐藏那些「客户端不该知道」的细节。如果客户端确实需要操作 Department 对象,那就别藏。
移除中间人(Remove Middle Man)
这个手法跟隐藏委托关系是对应的。如果你隐藏得太狠,每个类都变成了「传话筒」——只转发调用,不做任何实际工作——那就该移除中间人了。
我见过一个项目,一个 Service 类里有 30 多个方法,每个方法都是直接调用 Repository 的同名方法。这就是典型的中间人。说白了,这个 Service 层就是一层皮,没有任何业务逻辑。
判断标准:如果一个类的大部分方法都是「转发调用」,而且转发时没有增加任何逻辑,那就该考虑移除它了。
引入外部函数(Introduce Foreign Method)
有时候你用的第三方类或者系统类,缺少一个你需要的功能。你又不能修改它的源码。怎么办?
引入外部函数就是解决方案——在你自己的代码里写一个函数,参数是那个类的实例,然后实现你需要的功能。
// 第三方类,不能修改
public class DateUtil {
// 只有基本日期操作
}
// 你的外部函数
public static boolean isWeekend(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
return dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY;
}
这个手法看起来简单,但有个坑:外部函数多了,会散落在各个地方。我建议把它们集中放在一个工具类里,或者用扩展方法(C#)或扩展函数(Kotlin)来组织。
引入本地扩展(Introduce Local Extension)
引入外部函数只能解决单个方法的问题。如果你需要给第三方类添加一组相关功能,那就该用引入本地扩展了。
本地扩展有两种实现方式:
- 子类化(Subclassing):继承第三方类,添加新方法
- 包装类(Wrapper):创建一个新类,内部持有第三方类的实例
我个人更推荐包装类。为什么呢?因为第三方类可能被标记为 final,或者它的构造函数很复杂。包装类更灵活,也不容易受第三方类内部变化的影响。
避坑指南:我曾经用子类化给一个第三方 HTTP 客户端添加了重试功能。结果第三方库升级后,内部实现变了,我的子类直接编译不过。后来改成包装类,问题就解决了。
知识体系总览
这六个手法其实可以分成三组:
这三组手法,分别对应了三种常见的代码坏味道:
- 类太大或太小 → 提炼类 / 内联类
- 过度封装或过度暴露 → 隐藏委托关系 / 移除中间人
- 第三方类功能不足 → 引入外部函数 / 引入本地扩展
你想想看,这些手法其实都是在回答同一个问题:「这个类的边界在哪里?」
没有标准答案。每个项目、每个阶段,边界都可能不同。但有了这些手法,你就能随时调整边界,而不必担心代码崩塌。
总结一下:
| 手法 | 解决的问题 | 我的建议 |
|---|---|---|
| 提炼类 | 类职责过重 | 先拆出独立可测试的部分 |
| 内联类 | 类职责过轻 | 确认没有独立生命周期再合 |
| 隐藏委托关系 | 客户端知道太多内部细节 | 只隐藏不该知道的 |
| 移除中间人 | 类变成传话筒 | 转发无逻辑时果断移除 |
| 引入外部函数 | 第三方类缺单个方法 | 集中管理,别散落 |
| 引入本地扩展 | 第三方类缺一组功能 | 优先用包装类 |
好了,这一章就到这里。这些手法看起来简单,但真正用好需要大量实践。下次你重构代码时,不妨想想:这个类的边界,是不是该调一调了?