重构与可扩展性:重构代码以支持未来功能扩展
说实话,我见过太多项目死在「扩展性」这三个字上。
刚上线时跑得飞快,需求一来就改不动了。加个新功能,要动七八个文件。改一个接口,测试全挂。你想想看,这种代码谁敢动?
我个人习惯是:写代码时就要想好,三个月后别人怎么加功能。这不是过度设计,这是职业素养。
什么是可扩展性?
可扩展性说白了就是:不改或少改已有代码,就能加新功能。
我在项目中遇到过一种典型场景:一个订单处理系统,最开始只支持微信支付。后来要加支付宝,结果发现支付逻辑散落在十几个方法里。改一个地方,另外三个地方跟着崩。这就是典型的「不可扩展」。
可扩展性的核心原则:
- 对扩展开放,对修改关闭(开闭原则)
- 依赖抽象,不依赖具体实现
- 单一职责:每个模块只做一件事
重构前的代码:硬编码的噩梦
先看一段典型的「不可扩展」代码。嗯,我敢打赌你见过类似的:
// 重构前:硬编码支付逻辑
public class OrderService {
public void processPayment(String type) {
if ("wechat".equals(type)) {
// 微信支付逻辑
System.out.println("调用微信支付API");
System.out.println("记录微信支付日志");
System.out.println("发送微信支付通知");
} else if ("alipay".equals(type)) {
// 支付宝支付逻辑
System.out.println("调用支付宝API");
System.out.println("记录支付宝日志");
System.out.println("发送支付宝通知");
}
// 再加一种支付方式?继续加 else if
}
}
这段代码有什么问题?
- 加新支付方式要改
processPayment方法 - 每种支付方式的逻辑混在一起,没法单独测试
- 日志、通知等公共逻辑被重复写
我曾经在一个电商项目里看到类似的代码,当时要加「银联支付」。改完上线后,微信支付突然报错——因为一个全局变量被不小心覆盖了。嗯,这就是硬编码的代价。
重构后的代码:策略模式 + 工厂模式
怎么改?我建议用策略模式把每种支付方式封装成独立的类。再加一个简单工厂来创建它们。
// 1. 定义支付策略接口
public interface PaymentStrategy {
void pay(Order order);
}
// 2. 微信支付实现
public class WechatPay implements PaymentStrategy {
@Override
public void pay(Order order) {
System.out.println("调用微信支付API");
System.out.println("记录微信支付日志");
System.out.println("发送微信支付通知");
}
}
// 3. 支付宝支付实现
public class AlipayPay implements PaymentStrategy {
@Override
public void pay(Order order) {
System.out.println("调用支付宝API");
System.out.println("记录支付宝日志");
System.out.println("发送支付宝通知");
}
}
// 4. 工厂类:根据类型创建策略
public class PaymentFactory {
public static PaymentStrategy getStrategy(String type) {
switch (type) {
case "wechat": return new WechatPay();
case "alipay": return new AlipayPay();
default: throw new IllegalArgumentException("未知支付类型");
}
}
}
// 5. 使用:OrderService 不再关心具体实现
public class OrderService {
public void processPayment(String type, Order order) {
PaymentStrategy strategy = PaymentFactory.getStrategy(type);
strategy.pay(order);
}
}
现在加新支付方式,只需要:
- 新建一个类实现
PaymentStrategy接口 - 在
PaymentFactory里加一个 case
OrderService 一行代码都不用改。这就是可扩展性。
我的经验:工厂里的 switch 其实也可以用反射或注册表来消除。但小项目没必要过度设计。先让代码「能扩展」,再考虑「优雅地扩展」。
可扩展性的知识体系
下面这张图是我自己总结的「可扩展性重构路线图」。你想想看,从硬编码到可扩展,其实就三步:
更复杂的场景:插件化架构
如果功能扩展点很多(比如一个报表系统要支持几十种图表类型),策略模式就不够用了。我建议用插件化架构。
核心思路:
- 定义一个插件接口
- 每个插件独立打包(jar/dll)
- 主程序通过配置文件或扫描目录加载插件
// 插件接口
public interface ReportPlugin {
String getType();
void generate(Data data);
}
// 主程序:动态加载插件
public class ReportEngine {
private Map<String, ReportPlugin> plugins = new HashMap<>();
public void loadPlugins(String pluginDir) {
// 扫描目录,反射加载所有实现 ReportPlugin 的类
// 注册到 plugins 中
}
public void generateReport(String type, Data data) {
ReportPlugin plugin = plugins.get(type);
if (plugin != null) {
plugin.generate(data);
}
}
}
这样做的好处是:加新报表类型,只需要丢一个 jar 包进去。主程序不用改,甚至不用重启(配合热加载)。
避坑指南:我曾经在一个项目中过度使用插件化,每个小功能都做成插件。结果插件之间互相依赖,加载顺序混乱,调试起来痛不欲生。记住:插件化的粒度要适中。一个插件应该是一个完整的功能模块,不是一个方法。
可扩展性的代价
说实话,可扩展性不是免费的。它需要:
| 代价 | 说明 |
|---|---|
| 代码量增加 | 接口、工厂、策略类,比直接写 if-else 多不少代码 |
| 理解成本 | 新人看代码要理解设计模式,上手慢一些 |
| 调试难度 | 调用链变长,出问题时追踪路径更复杂 |
所以我的建议是:只在变化点做抽象。如果一个功能未来三个月内没有扩展的可能,就别用策略模式。YAGNI(You Ain't Gonna Need It)原则在这里同样适用。
总结:可扩展性重构的 checklist
最后,我整理了一个 checklist,每次重构前可以对照一下:
- 识别变化点:哪些功能未来可能增加变体?
- 提取接口:把变化的行为抽象成接口
- 封装实现:每个变体一个类,独立测试
- 工厂/DI:用工厂或依赖注入管理对象创建
- 验证:加一个新功能,看需要改几处代码
如果加一个新功能只需要新建一个类,然后在配置里加一行——恭喜你,重构成功了。
一个小技巧:重构时先写测试。把现有行为用测试固定下来,再动手改代码。这样改完后跑一遍测试,就知道有没有破坏已有功能。我每次重构都这么干,从来没翻过车。
可扩展性不是一蹴而就的。它是在一次次重构中慢慢养成的习惯。嗯,从今天开始,写代码时多问自己一句:「三个月后,别人怎么在这里加功能?」
公众号:蓝海资料掘金营,微信deep3321