20、大型重构(上):梳理并分解继承体系、将过程化设计转化为对象设计
为什么我们要聊大型重构?
说实话,日常工作中我们做的最多的,是那些小重构——重命名一个变量、提取一个小函数、把魔法数字变成常量。这些动作就像给代码做“微整形”,见效快、风险低。
但有一种重构,你没法一天搞定。它涉及整个模块,甚至跨多个包。我管它叫“大型重构”。
今天要聊的两种大型重构,是我在项目中遇到最多的:
- 继承体系失控了——类层次深得像俄罗斯套娃,改一个基类,下面全崩。
- 过程化代码堆成山——满屏的 if-else、switch,对象只是个数据容器,逻辑全写在外部函数里。
这两种情况,说白了就是代码的“骨架”出了问题。小修小补没用,得动大手术。
梳理并分解继承体系
继承的陷阱
我刚入行那会儿,觉得继承特别“高级”。父类写通用逻辑,子类做扩展,多完美啊。
直到我接手一个项目。那个系统的类继承深度达到了7层。你想改最底层的一个方法,得先搞清楚中间5层有没有覆盖它。改完还要担心顶层会不会出问题。
为什么会这样?因为继承是一种静态的、强耦合的关系。子类依赖父类的实现细节,父类一改,子类就跟着遭殃。
如何分解继承体系?
我总结了一套步骤,你可以参考:
- 识别“上帝基类”——看看哪个类被太多子类继承,而且子类之间差异很大。
- 用“提取接口”或“提取超类”分离职责——把真正通用的部分留下,把“看起来通用但实际不通用”的部分拆出去。
- 用“组合”代替“继承”——这是最核心的一步。把行为委托给独立的策略对象,而不是硬编码在继承链里。
举个例子。假设我们有一个 Animal 基类,下面有 Dog、Bird、Fish。每个子类都重写了 move() 方法。但问题是,有些动物会飞,有些会游,有些会跑——这些行为其实可以独立出来。
// 重构前:继承体系
class Animal {
move() { /* 默认实现 */ }
}
class Dog extends Animal {
move() { /* 跑 */ }
}
class Bird extends Animal {
move() { /* 飞 */ }
}
class Fish extends Animal {
move() { /* 游 */ }
}
// 重构后:组合代替继承
class Animal {
constructor(moveBehavior) {
this.moveBehavior = moveBehavior;
}
move() {
this.moveBehavior.move();
}
}
class MoveBehavior { /* 接口 */ }
class RunBehavior extends MoveBehavior { /* 跑的逻辑 */ }
class FlyBehavior extends MoveBehavior { /* 飞的逻辑 */ }
class SwimBehavior extends MoveBehavior { /* 游的逻辑 */ }
const dog = new Animal(new RunBehavior());
const bird = new Animal(new FlyBehavior());
const fish = new Animal(new SwimBehavior());
你看,这样改完,每个动物的移动行为变成了可插拔的。想给狗加个游泳能力?直接传个 SwimBehavior 进去就行,不用再建一个 SwimmingDog 子类。
将过程化设计转化为对象设计
过程化代码长什么样?
你想想看,有没有见过这样的代码:一个类里全是 getter/setter,没有任何业务逻辑。所有逻辑都写在外部的一个“大管家”类里,里面塞满了 if-else 和 switch。
这就是典型的过程化设计。数据和行为是分离的。数据对象就像个“哑巴”,只会存数据;行为对象像个“管家”,什么活都干。
我记得有一次,我接手一个订单系统。里面有个 Order 类,只有字段和 getter/setter。所有计算逻辑——折扣、税费、运费——全写在一个叫 OrderCalculator 的类里。那个类有3000多行,一个方法里嵌套了5层 if-else。
为什么会这样?说白了,就是写代码的人习惯了“先处理数据,再调用函数”的思维方式。这是C语言时代的遗风。
如何转化为对象设计?
核心原则就一句话:把行为搬回到它所属的数据旁边。
具体怎么做?我一般分三步:
- 识别“数据类”和“行为类”——找到那些只有字段没有方法的类,以及那些只操作外部数据的方法。
- 用“搬移函数”把方法挪到数据类里——如果一个方法主要操作某个类的数据,那就把它搬过去。
- 用“多态”代替条件逻辑——把 if-else 或 switch 里的每个分支,变成一个独立的策略类。
还是用订单的例子。重构前:
// 过程化设计
class Order {
constructor(items, customerType) {
this.items = items;
this.customerType = customerType; // 'normal' 或 'vip'
}
}
class OrderCalculator {
calculateTotal(order) {
let total = 0;
for (const item of order.items) {
total += item.price;
}
if (order.customerType === 'vip') {
total *= 0.9; // 9折
}
return total;
}
}
重构后:
// 对象设计
class Order {
constructor(items, discountStrategy) {
this.items = items;
this.discountStrategy = discountStrategy;
}
calculateTotal() {
let total = 0;
for (const item of this.items) {
total += item.price;
}
return this.discountStrategy.apply(total);
}
}
class DiscountStrategy {
apply(total) { return total; }
}
class VipDiscount extends DiscountStrategy {
apply(total) { return total * 0.9; }
}
class NormalDiscount extends DiscountStrategy {
apply(total) { return total; }
}
// 使用
const order = new Order(items, new VipDiscount());
console.log(order.calculateTotal());
你看,重构后,Order 自己知道怎么算总价。折扣策略被独立出来,可以自由组合。想加个“双倍积分日打8折”?再写个 DoublePointsDiscount 就行,不用改 Order 类。
两种重构的关系
你可能会问:这两种重构,先做哪个?
我个人习惯是:先处理继承体系,再处理过程化设计。因为继承体系的问题往往更“结构性”,不改掉它,后面的对象设计也做不彻底。
但这不是绝对的。如果你发现过程化设计已经严重影响了某个模块的可读性,那就先动它。记住,重构没有标准答案,只有“当前最合适的做法”。
一张图总结
下面这张图,是我自己画的知识结构图。它把今天讲的两条主线串在了一起。
避坑指南
最后,分享几个我踩过的坑:
- 不要试图一次性重构整个继承体系。 我曾经这么干过,结果改到一半,测试全红,回退都花了半天。后来我学乖了,一次只拆一个分支。
- 过程化转对象时,别过度设计。 不是所有 if-else 都要变成多态。如果只有两三个分支,而且未来几乎不会变,那保持原样反而更清晰。
- 一定要有测试保护。 大型重构没有测试,就像走钢丝没有安全网。我建议先补测试,再动手改代码。
super 方法。结果拆完后,子类的行为全变了。从那以后,我每次拆继承链之前,都会先跑一遍所有子类的测试,确保行为没变。
嗯,今天就聊到这里。大型重构是个手艺活,多练几次,你就能找到感觉了。
公众号:蓝海资料掘金营,微信deep3321