继承体系重构(上):提取超类、提取子类、折叠继承体系

说实话,继承体系重构这块内容,我当年刚接触时也觉得挺玄乎的。不就是父类子类嘛,有啥好重构的?直到我在一个嵌入式项目里,被一个六层深的继承链坑得死去活来……嗯,从那以后我才真正明白,继承这东西,用好了是利器,用不好就是灾难。

今天咱们聊三个最常用的手法:提取超类提取子类折叠继承体系。这三招你掌握了,大部分继承相关的坏味道都能搞定。

1. 提取超类:把共同点往上提

先说说提取超类。这个场景很常见——你发现两个类长得挺像,字段差不多,方法也差不多。这时候就该考虑,是不是该抽个父类出来?

我个人的习惯是:先别急着动手,把两个类的成员列出来,看看哪些是真正共用的。别一股脑全往上提,有些东西看着像,其实只是巧合。

核心原则:提取超类不是为了少写代码,而是为了表达「这些类属于同一抽象层次」。

举个例子,假设你有两个结构体:

// 原始代码:两个类有重复字段和方法
typedef struct {
    char name[64];
    int id;
    float baseSalary;
    float bonus;
} Manager;

typedef struct {
    char name[64];
    int id;
    float baseSalary;
    float hourlyRate;
    int hoursWorked;
} Engineer;

float calcManagerPay(Manager* m) {
    return m->baseSalary + m->bonus;
}

float calcEngineerPay(Engineer* e) {
    return e->baseSalary + e->hourlyRate * e->hoursWorked;
}

你看,nameidbaseSalary 三个字段是重复的。提取超类后:

// 提取超类 Employee
typedef struct {
    char name[64];
    int id;
    float baseSalary;
} Employee;

typedef struct {
    Employee base;  // 组合方式实现继承
    float bonus;
} Manager;

typedef struct {
    Employee base;
    float hourlyRate;
    int hoursWorked;
} Engineer;

float calcManagerPay(Manager* m) {
    return m->base.baseSalary + m->bonus;
}

float calcEngineerPay(Engineer* e) {
    return e->base.baseSalary + e->hourlyRate * e->hoursWorked;
}
小技巧:在C语言里,我习惯用「组合+类型转换」来模拟继承。把超类作为子类的第一个字段,这样就能安全地把子类指针转成超类指针。我在做网络协议栈时经常用这招。

2. 提取子类:把特殊行为往下放

提取子类正好反过来。有时候一个类承担了太多职责,某些字段和方法只在一部分场景下才用到。这时候就该考虑,是不是该拆出子类?

我曾经在一个订单系统里见过一个Order类,里面既有普通订单的逻辑,又有VIP订单的逻辑,还有企业订单的逻辑……一个类里塞了三个if-else分支,看得我头皮发麻。

为什么会这样?说白了就是当初图省事,觉得「先放一起,以后再说」。结果「以后」来了,代码已经乱成一锅粥。

提取子类的步骤其实很简单:

  1. 找出只在特定场景下才使用的字段和方法
  2. 创建一个新的子类,把这些东西移过去
  3. 在父类中保留公共逻辑,通过虚函数(或函数指针)让子类定制

看个例子:

// 重构前:一个类处理多种订单
typedef struct {
    char orderId[32];
    float amount;
    int type;  // 0=普通, 1=VIP, 2=企业
    float discount;
    float vipExtraDiscount;
    char companyName[128];
    char taxId[32];
} Order;

float calcOrderTotal(Order* o) {
    float total = o->amount;
    if (o->type == 1) {
        total *= (1 - o->discount - o->vipExtraDiscount);
    } else if (o->type == 2) {
        total *= (1 - o->discount);
        // 企业订单还要处理发票信息
    } else {
        total *= (1 - o->discount);
    }
    return total;
}

重构后,提取出VipOrderCorporateOrder子类:

// 重构后:提取子类
typedef struct {
    char orderId[32];
    float amount;
    float discount;
    float (*calcTotal)(void*);
} Order;

typedef struct {
    Order base;
    float vipExtraDiscount;
} VipOrder;

typedef struct {
    Order base;
    char companyName[128];
    char taxId[32];
} CorporateOrder;

float calcVipOrderTotal(void* self) {
    VipOrder* o = (VipOrder*)self;
    return o->base.amount * (1 - o->base.discount - o->vipExtraDiscount);
}

float calcCorporateOrderTotal(void* self) {
    CorporateOrder* o = (CorporateOrder*)self;
    return o->base.amount * (1 - o->base.discount);
}
注意:提取子类不是越多越好。我见过有人把只有两行差异的逻辑也拆成子类,结果类数量翻了三倍,维护成本反而更高。我的经验是:子类至少要有两个以上的专属字段或方法,才值得拆。

3. 折叠继承体系:该合并时就合并

折叠继承体系,说白了就是「子类没啥用,合并回父类吧」。这种情况在代码演进过程中很常见——当初设计时觉得以后会有很多变体,结果几年过去了,子类就一个,而且几乎没加什么新东西。

我记得有个项目,一个BaseDevice类下面只有一个SerialDevice子类。子类里就多了个baudRate字段和一个setBaudRate方法。你说这有必要拆吗?完全没必要。直接合并回父类,少一层间接,代码反而更清晰。

折叠的时机其实很好判断:

  • 子类只有一个,且短期内看不到增加第二个的可能
  • 子类新增的字段/方法很少(比如不超过2个)
  • 子类和父类之间的差异越来越模糊

操作步骤也很直接:

  1. 把子类的字段和方法全部移到父类
  2. 删除子类定义
  3. 把所有引用子类的地方改成父类
// 折叠前:子类几乎没做什么
typedef struct {
    char name[32];
    int id;
} BaseDevice;

typedef struct {
    BaseDevice base;
    int baudRate;
} SerialDevice;

void setBaudRate(SerialDevice* d, int rate) {
    d->baudRate = rate;
}

// 折叠后:合并成一个类
typedef struct {
    char name[32];
    int id;
    int baudRate;  // 从子类移过来
} Device;

void setBaudRate(Device* d, int rate) {
    d->baudRate = rate;
}
我的建议:折叠继承体系时,别只盯着代码看。去翻翻版本历史,看看这个子类是不是曾经有过兄弟类,只是后来被删了。如果是,那可能不是折叠的问题,而是架构设计本身需要重新审视。

4. 三种手法的选择策略

说了这么多,你可能会问:什么时候该提取超类?什么时候该提取子类?什么时候该折叠?

我整理了一个简单的判断表,供你参考:

场景 推荐手法 典型信号
多个类有重复字段/方法 提取超类 复制粘贴的代码、相似的成员列表
一个类承担了多种职责 提取子类 大量if-else、字段只在部分场景使用
子类几乎没有独立价值 折叠继承体系 子类只有一个、子类新增内容极少

你想想看,这三种手法其实都是在回答同一个问题:「这个继承关系,到底有没有必要存在?」

提取超类是在说「有必要,但公共部分应该共享」。提取子类是在说「有必要,但特殊部分应该分离」。折叠继承体系是在说「没必要,合并了吧」。

5. 知识体系总览

下面这张图,我把今天讲的三种手法和它们之间的关系画了出来。你可以对照着回顾一下:

继承体系重构三种手法 继承体系重构 提取超类 提取子类 折叠继承体系 适用场景 多个类有重复字段/方法 公共逻辑需要共享 适用场景 一个类承担多种职责 特殊行为需要分离 适用场景 子类只有一个 子类新增内容极少 核心思想:继承关系不是越多越好,也不是越少越好 关键是「这个继承关系,到底有没有必要存在?」

嗯,今天的内容就到这儿。这三种手法看着简单,但真正用好需要大量实践。我建议你下次重构时,先拿一个小模块练手,别一上来就动核心架构。等手感找到了,再逐步扩大范围。


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