继承体系重构(上):提取超类、提取子类、折叠继承体系
说实话,继承体系重构这块内容,我当年刚接触时也觉得挺玄乎的。不就是父类子类嘛,有啥好重构的?直到我在一个嵌入式项目里,被一个六层深的继承链坑得死去活来……嗯,从那以后我才真正明白,继承这东西,用好了是利器,用不好就是灾难。
今天咱们聊三个最常用的手法:提取超类、提取子类、折叠继承体系。这三招你掌握了,大部分继承相关的坏味道都能搞定。
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;
}
你看,name、id、baseSalary 三个字段是重复的。提取超类后:
// 提取超类 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;
}
2. 提取子类:把特殊行为往下放
提取子类正好反过来。有时候一个类承担了太多职责,某些字段和方法只在一部分场景下才用到。这时候就该考虑,是不是该拆出子类?
我曾经在一个订单系统里见过一个Order类,里面既有普通订单的逻辑,又有VIP订单的逻辑,还有企业订单的逻辑……一个类里塞了三个if-else分支,看得我头皮发麻。
为什么会这样?说白了就是当初图省事,觉得「先放一起,以后再说」。结果「以后」来了,代码已经乱成一锅粥。
提取子类的步骤其实很简单:
- 找出只在特定场景下才使用的字段和方法
- 创建一个新的子类,把这些东西移过去
- 在父类中保留公共逻辑,通过虚函数(或函数指针)让子类定制
看个例子:
// 重构前:一个类处理多种订单
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;
}
重构后,提取出VipOrder和CorporateOrder子类:
// 重构后:提取子类
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个)
- 子类和父类之间的差异越来越模糊
操作步骤也很直接:
- 把子类的字段和方法全部移到父类
- 删除子类定义
- 把所有引用子类的地方改成父类
// 折叠前:子类几乎没做什么
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