大型重构(一):梳理并分解继承体系、将过程化设计转化为对象设计
说实话,我见过太多代码库,一开始只是个小项目,大家图快,用过程化的方式堆功能。结果呢?三年后,一个函数上千行,if-else 嵌套七八层,新来的同事看一眼就想跑。这时候,你就得动大手术了——大型重构。
今天咱们聊两个最核心的“大手术”:梳理并分解继承体系,以及将过程化设计转化为对象设计。这两个活儿,我职业生涯里干过不下十次,每次都是硬仗。
为什么继承体系会变成一团乱麻?
我刚开始做设计时,觉得继承是面向对象最牛的特性。后来发现,滥用继承比不用继承更可怕。你想想看,一个类继承自另一个类,子类又继承自子类,五层下去,谁是谁的爹都搞不清了。
我在项目中遇到过这样一个案例:一个叫 Order 的基类,下面有 SpecialOrder、VIPOrder、InternationalOrder,然后 VIPOrder 又派生出 GoldVIPOrder 和 PlatinumVIPOrder。每个子类都重写了父类的方法,但重写逻辑里又调用了 super 方法。结果呢?改一个父类方法,五个子类全崩。
说白了,继承体系混乱的根源就两个:
- 职责不清:一个类干了太多事,导致子类不得不覆盖大量方法
- 层次过深:超过三层的继承,基本就是灾难
分解继承体系的三个步骤
我个人习惯,拿到一团乱麻的继承体系,先做三件事:
- 找出“上帝类”——那个被最多子类继承的基类,往往就是问题核心
- 识别“变与不变”——哪些行为是稳定的?哪些行为每个子类都不一样?
- 用组合替代继承——把变化的行为提取成接口或策略类
举个例子,咱们看一段典型的“坏味道”代码:
// 过程化风格,一个类处理所有订单类型
public class OrderProcessor {
public double calculateTotal(Order order) {
if (order.getType().equals("NORMAL")) {
return order.getAmount() * 0.9; // 普通订单9折
} else if (order.getType().equals("VIP")) {
return order.getAmount() * 0.8; // VIP订单8折
} else if (order.getType().equals("INTERNATIONAL")) {
double base = order.getAmount() * 0.85;
return base + base * 0.1; // 国际订单加关税
}
// 再加一种类型?继续加 else if
return order.getAmount();
}
}
这段代码,说白了就是披着面向对象外衣的过程化设计。每加一种订单类型,就得改这个函数。我见过最夸张的,这种 if-else 链写了三十多个分支。
重构之后,应该是这样的:
// 策略模式,每个策略独立
public interface DiscountStrategy {
double calculate(double amount);
}
public class NormalDiscount implements DiscountStrategy {
public double calculate(double amount) {
return amount * 0.9;
}
}
public class VIPDiscount implements DiscountStrategy {
public double calculate(double amount) {
return amount * 0.8;
}
}
public class InternationalDiscount implements DiscountStrategy {
public double calculate(double amount) {
double base = amount * 0.85;
return base + base * 0.1;
}
}
// 订单类只依赖策略接口
public class Order {
private DiscountStrategy strategy;
public double getTotal() {
return strategy.calculate(this.amount);
}
}
嗯,这里要注意:不要一上来就全盘重写。我曾经犯过这个错,花了两周重构,结果上线后一堆 bug。正确的做法是:一次只提取一个策略,每提取一个就跑一遍测试。
过程化设计转对象设计:从“怎么做”到“谁来做”
过程化设计的核心思维是“先做什么,再做什么”。对象设计的核心思维是“谁负责做什么”。
我举个例子,你感受一下区别。假设有个报表生成功能:
// 过程化:一个函数搞定所有
public class ReportGenerator {
public void generateReport(String type) {
// 1. 查询数据
List<Data> data = queryData();
// 2. 格式化数据
String formatted = "";
if (type.equals("CSV")) {
formatted = formatAsCSV(data);
} else if (type.equals("PDF")) {
formatted = formatAsPDF(data);
}
// 3. 发送邮件
sendEmail(formatted);
}
}
这个函数干了三件事:查数据、格式化、发邮件。每件事都耦合在一起。如果哪天要加个微信通知,你怎么办?继续往这个函数里塞?
重构后,我们把职责拆开:
// 每个类只做一件事
public class DataFetcher {
public List<Data> fetch() { /* 查询数据 */ }
}
public interface Formatter {
String format(List<Data> data);
}
public class CSVFormatter implements Formatter { /* CSV格式化 */ }
public class PDFFormatter implements Formatter { /* PDF格式化 */ }
public class Notifier {
public void send(String content) { /* 发送通知 */ }
}
// 协调者只负责编排
public class ReportService {
private DataFetcher fetcher;
private Formatter formatter;
private Notifier notifier;
public void generateReport() {
List<Data> data = fetcher.fetch();
String content = formatter.format(data);
notifier.send(content);
}
}
你看,现在每个类都只关心自己的事。加一种通知方式?加一个 Notifier 的子类就行。加一种格式?加一个 Formatter 的实现类。完全不用改现有代码。
避坑指南
我曾经在重构一个支付系统时,把过程化代码全拆成了对象。结果拆得太细,一个支付流程涉及十几个类,调用链特别长,调试起来想死的心都有。
所以,这里给你三个建议:
- 不要过度设计:如果某个逻辑未来两年内都不会变,别急着拆。拆了反而增加复杂度。
- 保持接口稳定:一旦把行为提取成接口,就不要轻易改接口签名。否则所有实现类都得跟着改。
- 先写测试再重构:没有测试覆盖的代码,重构就是裸奔。我吃过这个亏,现在每次重构前,先补测试。
一张图看懂重构思路
下面这张图,是我做这类重构时脑子里始终绷着的一根弦。左边是混乱的继承和过程化代码,右边是清晰的对象设计:
总结一下
大型重构,说白了就是两件事:把混乱的继承体系拆成组合,把过程化的逻辑装进对象里。做的时候记住三点:
- 一次只改一个点,改完就跑测试
- 用策略模式替代 if-else 链
- 组合优于继承,接口优于抽象类
嗯,今天就聊到这儿。下次咱们接着聊大型重构的另一个重头戏——如何安全地拆分一个巨型类。
核心要点:重构不是重写。别想着一步到位,每次改动都要保证系统还能跑。我见过太多重构到一半,项目黄了的案例。稳扎稳打,才是正道。
我的小技巧:每次重构前,先在代码里画一张“依赖图”。把类之间的继承、调用关系画出来。这张图能帮你一眼看出哪些类是“上帝类”,哪些是“孤魂野鬼”。
警告:如果你重构的代码没有单元测试,请先补测试。没有测试的重构,就像在雷区里跳舞。我曾经跳过,结果炸得满身是伤。