在程序设计领域, SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)是由罗伯特·C·马丁在21世纪早期 引入的记忆术首字母缩略字,指代了面向对象编程和面向对象设计的五个基本原则。
“SOLID”原则中的 O 指代了开闭原则,英文是 The Open/Closed Principle
,缩写为 OCP
。
开闭原则,指的是对于扩展是开放的,但是对于修改是封闭的,这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。
在 23 种经典设计模式中,大部分设计模式都是为了解决代码的扩展性问题而存在的,主要遵从的设计原则就是开闭原则。
如何理解对扩展开放,对修改封闭?
开闭原则,对于扩展是开放的,但是对于修改是封闭的。详细点描述,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
大伙可能觉得还是比较模糊,我们看下这个例子。
public class ChatService {
public static boolean filtMessage(String msg, String userId, int level) {
// 等级小于10,过滤聊天信息
if(level < 10) {
return true;
}
return false;
}
}
上面的示例代码实现了聊天数据的简单过滤功能。此时,我们有新的需求,需要根据用户的渠道有不同的等级过滤聊天信息,如A渠道用户等级限制10,B渠道用户等级限制15,我们应该怎么修改呢?
public class ChatService {
// 修改1:新增channel渠道参数,以及调用此方法的所有地方
public static boolean filtMessage(String msg, String userId, int level, int channel) {
// 修改2
// A渠道用户等级限制10, 等级小于10,过滤聊天信息
if (channel == 1) {
if(level < 10) {
return true;
}
}
// B渠道用户等级限制15, 等级小于15,过滤聊天信息
if (channel == 2) {
if(level < 15) {
return true;
}
}
return false;
}
}
上面的代码修改,存在几个问题:
- filtMessage 方法参数修改,调用此方法的所有代码处都要一同修改;
- filtMessage 方法逻辑修改,而且参数也发生修改,对应的单元测试也要一起修改;
上面的方式,是基于修改已有代码的方式来实现需求,明显违背了开闭原则。我们再来看看下面的方式:
public class Message {
private String msg;
private String userId;
private int level;
privater int channel;
// getter & setter
}
public interface Filter {
boolean check(Message msg);
}
// A渠道用户等级限制10, 等级小于10,过滤聊天信息
public class ChannelAFilter implements Filter {
public boolean check(Message msg) {
if (msg.getChannel() == 1) {
if(msg.getLevel() < 10) {
return true;
}
}
return false;
}
}
// B渠道用户等级限制15, 等级小于15,过滤聊天信息
public class ChannelBFilter implements Filter {
public boolean check(Message msg) {
if (msg.getChannel() == 2) {
if(msg.getLevel() < 15) {
return true;
}
}
return false;
}
}
public class ChatService {
private List<Filter> filters = new ArrayList<>();
public static boolean filtMessage(Message msg) {
for (Filter filter : filters) {
if(filter.check(msg)) {
return true;
}
}
return false;
}
}
基于上面的例子,做了俩个地方的调整:
- 将 filtMessage 方法的参数封装到 Message,随着版本的功能迭代,新的限制条件可直接在 Message 新增字段即可,而不用直接在 filtMessage 方法新增参数;
- 将 filtMessage 方法的检测逻辑拆分到不同的 Filter 中。如果有新的限制规则,我们直接新增 Filter 即可满足需求。
重构之后的代码更加灵活和易扩展。我们只需要为新的 Filter 类添加单元测试,老的单元测试都不会失败,也不用修改。
如何做到对扩展开放,对修改封闭?
为了代码的扩展性,扩展、抽象、封装这些潜意识,我们要时刻保持,就好像打LOL时,要有gank意识,不能做万年野。
同时也要思考,要预留应对未来的需求变更的扩展点,以便在不改变代码整体结构的前提下,以较小的改动代价来完成需求,新的代码能灵活扩展,做到对扩展开放、对修改关闭
。
分辨代码的可变与不变,将可变的封装,隔离,抽象成不可变的接口,给到底层系统使用。
当有新的变化来临时,我们基于接口进行扩展,替换老实现或者新增实现,底层系统几乎不用修改(除非当初的设计已经无法满足,要进行重构)。
在众多的设计原则、思想、模式中,最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态等)。
最合理的做法是,对于一些比较确定的、短期内可能就会扩展,或者需求改动对代码结构影响比较大的情况,或者实现成本不高的扩展点,在编写代码的时候之后,我们就可以事先做些扩展性设计。但对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的扩展点,我们可以等到有需求驱动的时候,再通过重构代码的方式来支持扩展的需求。
而且,开闭原则也并不是完美的。有些情况下,代码的扩展性会跟可读性相冲突。为了更好地支持扩展性,我们对代码进行了重构,重构之后的代码要比之前的代码复杂很多,理解起来也更加有难度。很多时候,我们都需要在扩展性和可读性之间做权衡。在某些场景下,代码的扩展性很重要,我们就可以适当地牺牲一些代码的可读性;在另一些场景下,代码的可读性更加重要,那我们就适当地牺牲一些代码的可扩展性。