开闭原则:对扩展开放、对修改关闭
开闭原则,说白了就是一句话:软件实体(类、模块、函数)应该对扩展开放,对修改关闭。
我第一次听到这个定义时,觉得有点绕。后来在项目中摔过几次跟头,才真正理解它的分量。你想想看,一个系统上线后,需求变更是家常便饭。如果每次加功能都要改老代码,那改着改着就改崩了。
开闭原则的核心思想是:用扩展代替修改。老代码不动,新功能通过新增代码来实现。这样既保证了稳定性,又保证了灵活性。
为什么需要开闭原则?
我在一个电商项目中遇到过这样的场景:
- 初期只支持支付宝支付
- 后来要接入微信支付
- 再后来要支持银联、花呗、白条...
如果每次加一种支付方式,都要去改支付处理的核心类,那这个类会越来越臃肿,越来越脆弱。改一次,测一次,上线提心吊胆一次。
这就是典型的违反开闭原则的后果。
我曾经...在一个老项目中接手过一个订单处理模块。里面if-else嵌套了七八层,每加一种订单类型就要加一个else if。我改了一次,改出了三个bug。从那以后,我发誓绝不让自己的代码变成那样。
策略模式:开闭原则的最佳实践
策略模式,就是用来解决这个问题的。它把每个业务逻辑封装成独立的策略类,客户端通过上下文来调用。加新策略时,只需要新增一个类,不用改老代码。
说白了,策略模式就是「把变化的部分抽出来,变成可替换的策略」。
策略模式的结构
| 角色 | 职责 |
|---|---|
| 策略接口 | 定义所有策略的公共方法 |
| 具体策略 | 实现具体的业务逻辑 |
| 上下文 | 持有策略引用,调用策略方法 |
实战:支付策略的热插拔
我们来看一个支付系统的例子。假设现在有支付宝和微信两种支付方式。
第一步:定义策略接口
public interface PaymentStrategy {
void pay(BigDecimal amount);
}
第二步:实现具体策略
public class AlipayStrategy implements PaymentStrategy {
@Override
public void pay(BigDecimal amount) {
System.out.println("使用支付宝支付:" + amount + "元");
// 调用支付宝SDK
}
}
public class WechatPayStrategy implements PaymentStrategy {
@Override
public void pay(BigDecimal amount) {
System.out.println("使用微信支付:" + amount + "元");
// 调用微信SDK
}
}
第三步:创建上下文
public class PaymentContext {
private PaymentStrategy strategy;
public PaymentContext(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void executePayment(BigDecimal amount) {
strategy.pay(amount);
}
}
第四步:客户端使用
public class Client {
public static void main(String[] args) {
PaymentContext context = new PaymentContext(new AlipayStrategy());
context.executePayment(new BigDecimal("100.00"));
// 切换策略
context = new PaymentContext(new WechatPayStrategy());
context.executePayment(new BigDecimal("200.00"));
}
}
关键点:当需要新增银联支付时,只需要新增一个 UnionPayStrategy 类,实现 PaymentStrategy 接口。PaymentContext 和已有的策略类完全不用动。这就是「对扩展开放,对修改关闭」。
策略模式 + 工厂模式:更优雅的写法
在实际项目中,我习惯把策略模式和工厂模式结合起来。客户端不用自己创建策略对象,而是通过工厂来获取。
public class PaymentStrategyFactory {
private static Map<String, PaymentStrategy> strategyMap = new HashMap<>();
static {
strategyMap.put("alipay", new AlipayStrategy());
strategyMap.put("wechat", new WechatPayStrategy());
}
public static PaymentStrategy getStrategy(String type) {
PaymentStrategy strategy = strategyMap.get(type);
if (strategy == null) {
throw new IllegalArgumentException("不支持的支付类型:" + type);
}
return strategy;
}
}
客户端调用就变成了:
PaymentStrategy strategy = PaymentStrategyFactory.getStrategy("alipay");
PaymentContext context = new PaymentContext(strategy);
context.executePayment(new BigDecimal("100.00"));
这样,客户端连策略类的具体实现都不需要知道了。加新策略时,只需要在工厂的Map里注册一下就行。
我个人习惯把策略的注册做成可配置的,比如通过配置文件或数据库来管理。这样连代码都不用改,改个配置就能热插拔。当然,这要看项目的复杂度。
避坑指南
策略模式虽好,但也不是万能的。我总结几个常见的坑:
- 策略类过多:如果策略类太多,管理起来会麻烦。可以考虑用组合模式或模板方法模式来优化。
- 策略之间共享状态:如果多个策略需要共享一些数据,要注意线程安全问题。我建议把共享数据放在上下文里,而不是策略类里。
- 策略选择逻辑复杂:如果选择策略的条件很复杂,可以考虑用策略选择器(Selector)来封装选择逻辑。
我曾经在一个项目中,把策略类写成了单例。结果多个线程同时调用时,出现了数据错乱。后来发现是策略类里有个成员变量没处理好。教训是:策略类最好是无状态的,或者用ThreadLocal来隔离。
总结
开闭原则是面向对象设计的基石之一。策略模式是实现开闭原则的经典手段。它让我们的代码更灵活、更稳定、更容易扩展。
你想想看,如果每个新需求都要改老代码,那系统迟早会变成一团乱麻。而策略模式就像给系统装上了「热插拔」的能力——插上新的策略,拔掉旧的策略,核心代码纹丝不动。
嗯,这就是开闭原则的魅力所在。