20、大型重构(上):梳理并分解继承体系、将过程化设计转化为对象设计

继承体系,怎么就“烂”了?

说实话,我见过太多被继承“玩坏”的项目。

一开始,设计师画了个漂亮的类图。动物→哺乳动物→狗,三层继承,清晰明了。但半年后呢?需求一多,有人往“狗”里塞了个“会飞的狗”,有人给“哺乳动物”加了“下蛋”方法。最后这个继承树,长得跟榕树的气根似的,谁都不敢动。

为什么会这样?

因为继承本身是一种强耦合。子类依赖父类的实现细节。一旦父类变了,所有子类都得跟着改。我有个项目,一个基类改了构造函数的参数,结果下面二十多个子类全部报错。那感觉,就像多米诺骨牌倒了一片。

所以,大型重构的第一步,就是梳理继承体系。说白了,就是看看这棵树哪些枝干该砍,哪些该嫁接。

识别“坏”继承的几种味道

我个人习惯,先闻味道。代码不会骗人,有问题的继承体系,通常有这些特征:

  • 子类只用到了父类的一部分方法——那说明继承关系可能不对,应该用组合。
  • 父类有大量受保护字段——子类直接操作父类数据,耦合太深。
  • 继承层次过深——超过三层,我就要警惕了。五层以上,基本是灾难。
  • 子类重写了父类的大部分方法——那你还继承它干嘛?

核心原则:继承应该表达“is-a”关系,而不是“has-a”或“reuse-code”。如果只是为了复用代码而继承,趁早改成组合。

分解继承体系:从“树”到“林”

我常用的手法,叫“把一棵大树,拆成一片小树林”。

具体怎么做?看个例子。

假设我们有个Employee类,下面有EngineerManagerSalesman。每个子类都有calculatePay()方法,但实现完全不同。同时,它们又共享一些公共字段,比如nameid

这种结构,其实可以拆成两层:

  • 一层是“员工基本信息”,用组合或简单继承。
  • 另一层是“薪酬策略”,用策略模式。

代码重构后,大概长这样:

// 不再用继承,改用组合
public class Employee {
    private String name;
    private String id;
    private PayStrategy payStrategy; // 策略对象

    public double calculatePay() {
        return payStrategy.calculate();
    }
}

// 策略接口
public interface PayStrategy {
    double calculate();
}

// 具体策略
public class EngineerPay implements PayStrategy {
    public double calculate() { /* 工程师算法 */ }
}

public class ManagerPay implements PayStrategy {
    public double calculate() { /* 经理算法 */ }
}

你看,继承体系被分解了。每个策略类只关心自己的计算逻辑,互不干扰。想加新的薪酬类型?加个策略类就行,不用动Employee

我的经验:分解继承体系时,先找出“变化点”和“不变点”。把不变的部分留在基类,把变化的部分抽出去。我曾经把一个7层的继承树,拆成了3个独立的策略族,代码量少了40%,bug率直线下降。

过程化设计:一段“不堪回首”的往事

我记得刚入行那会儿,接手过一个老系统。一个函数两千行,全是if-else。数据结构和行为完全分离。你想知道一个订单的状态?得去查三个不同的数组。你想修改一个逻辑?得先搞清楚这堆全局变量谁在写谁在读。

这就是典型的过程化设计。说白了,就是C语言的写法,硬塞进了Java里。

过程化设计的特征很明显:

  • 数据是数据,方法是方法,各玩各的。
  • 大量使用switch或if-else来判断类型。
  • 全局变量满天飞。
  • 一个函数做太多事,职责不单一。

这种代码,改一个地方,往往牵一发动全身。我那时候最怕的就是“加一个新功能”,因为不知道会炸到哪里。

将过程化设计转化为对象设计

怎么改?核心就一句话:把数据和操作数据的行为,封装到一起。

来看个具体例子。假设我们有个过程化的订单处理:

// 过程化风格
public class OrderProcessor {
    public double calculateTotal(String orderType, double amount) {
        if (orderType.equals("NORMAL")) {
            return amount;
        } else if (orderType.equals("DISCOUNT")) {
            return amount * 0.9;
        } else if (orderType.equals("VIP")) {
            return amount * 0.8;
        }
        return amount;
    }
}

这种代码,每加一种订单类型,就得改calculateTotal方法。而且orderType是字符串,拼写错了就出bug。

改成对象设计后:

// 面向对象风格
public interface OrderType {
    double calculate(double amount);
}

public class NormalOrder implements OrderType {
    public double calculate(double amount) {
        return amount;
    }
}

public class DiscountOrder implements OrderType {
    public double calculate(double amount) {
        return amount * 0.9;
    }
}

public class VipOrder implements OrderType {
    public double calculate(double amount) {
        return amount * 0.8;
    }
}

// 使用
OrderType type = new VipOrder();
double total = type.calculate(100.0);

你看,每个订单类型自己管自己的计算逻辑。想加新类型?加个类就行,不用改现有代码。这就是开闭原则——对扩展开放,对修改关闭。

注意:不要为了“面向对象”而面向对象。如果只有两三种类型,而且几乎不变,那用过程化也没什么。我见过有人把简单的if-else硬拆成十几个类,反而增加了复杂度。重构的度,要把握好。

重构的节奏:小步快跑

大型重构最忌讳的就是“一步到位”。我刚开始做重构时,总想一次性把整个继承体系拆完。结果呢?改到一半,测试全挂,回退都来不及。

后来我学乖了。每次只改一小块,改完就跑测试。哪怕只是把一个方法从子类提到父类,或者把一个字段从父类下放到子类,都值得单独提交一次。

具体节奏:

  1. 先画出现状图——把当前的继承体系画出来,标注哪些类有问题。
  2. 确定目标结构——想清楚最终要拆成什么样。
  3. 从叶子节点开始拆——先动那些影响最小的子类。
  4. 每步都跑测试——确保没有破坏现有功能。
  5. 逐步替换调用方——不要一次性改所有引用。

我曾经重构过一个支付模块的继承体系,前后花了三周。每天只改一两个类,但每天都能保证代码可运行。最后一天,我把旧的基类删掉,整个系统跑得稳稳的。那种感觉,比写新代码还爽。

一张图看懂本章核心

下面这张图,总结了继承体系分解和过程化转对象的核心逻辑:

大型重构核心逻辑 继承体系分解 基类 (Employee) 子类A 子类B 子类C 分解 组合 + 策略 Employee (组合) └─ PayStrategy (接口) ├─ EngineerPay └─ ManagerPay 过程化 → 对象化 过程化: if (type == "A") { ... } else if (type == "B") { ... } else if (type == "C") { ... } 封装 多态 + 策略 interface Type { calculate(); } class TypeA implements Type { ... } class TypeB implements Type { ... } class TypeC implements Type { ... } 核心:识别变化点 → 封装变化 → 组合优先于继承

避坑指南

最后,分享几个我踩过的坑:

  • 不要一次性拆太多。我曾经试图在一个迭代里拆掉整个继承体系,结果代码冲突不断,测试覆盖不全。后来改成每次只拆一个分支,稳多了。
  • 警惕“过度设计”。不是所有过程化代码都需要转成对象。如果那个逻辑几乎不变,或者只有两三个分支,保持原样反而更好。重构的目的是让代码更容易改,不是让代码更“高级”。
  • 先写测试,再动手。没有测试的重构,就像走钢丝不带安全绳。我吃过这个亏,现在每重构一个类,先把它的行为用测试固定住。
  • 注意性能影响。策略模式会引入额外的对象和间接调用。如果是在热点路径上,要考虑性能损耗。不过大多数业务系统,这点损耗可以忽略。

一个小技巧:重构继承体系时,可以先用“提取接口”或“提取超类”作为过渡。不要一步到位拆成组合,先让代码结构清晰起来,再慢慢调整。我习惯用“三步法”:先提取接口,再替换调用方,最后删除旧继承关系。

嗯,大型重构的上半部分就到这里。记住,重构不是炫技,是为了让代码更好维护。下一节,我们会继续聊大型重构的其他手法,包括如何分离职责、如何处理跨横切关注点。但今天的内容,已经足够你开始动手了。


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