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();

这段代码里,客户端知道了 PersonDepartmentDepartmentManager。如果哪天 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 客户端添加了重试功能。结果第三方库升级后,内部实现变了,我的子类直接编译不过。后来改成包装类,问题就解决了。

知识体系总览

这六个手法其实可以分成三组:

基本重构手法(下)知识体系 类的拆分与合并 提炼类(Extract Class) 内联类(Inline Class) 职责过重 → 拆;职责过轻 → 合 委托关系的调整 隐藏委托关系(Hide Delegate) 移除中间人(Remove Middle Man) 隐藏细节 vs 消除冗余 扩展第三方类 引入外部函数(Introduce Foreign Method) 引入本地扩展(Introduce Local Extension) 单个方法 vs 一组功能

这三组手法,分别对应了三种常见的代码坏味道:

  • 类太大或太小 → 提炼类 / 内联类
  • 过度封装或过度暴露 → 隐藏委托关系 / 移除中间人
  • 第三方类功能不足 → 引入外部函数 / 引入本地扩展

你想想看,这些手法其实都是在回答同一个问题:「这个类的边界在哪里?」

没有标准答案。每个项目、每个阶段,边界都可能不同。但有了这些手法,你就能随时调整边界,而不必担心代码崩塌。

总结一下:

手法 解决的问题 我的建议
提炼类 类职责过重 先拆出独立可测试的部分
内联类 类职责过轻 确认没有独立生命周期再合
隐藏委托关系 客户端知道太多内部细节 只隐藏不该知道的
移除中间人 类变成传话筒 转发无逻辑时果断移除
引入外部函数 第三方类缺单个方法 集中管理,别散落
引入本地扩展 第三方类缺一组功能 优先用包装类

好了,这一章就到这里。这些手法看起来简单,但真正用好需要大量实践。下次你重构代码时,不妨想想:这个类的边界,是不是该调一调了?

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