世界上没有任何一个项目是不需要迭代的,随着项目的发展壮大,会有越来越多的功能代码会被修改、添加、删除。据统计线上的生产事故90%都有由于变更引起的,因此为保证项目的迭代稳定性,我们需尽可能的遵守开闭原则。那开闭原则到底是什么?开和闭如何矛盾而统一呢?实际开发中该原则是否可执行?又该如何应用呢?
一、开闭原则概念
开闭原则指的是一个软件实体应该通过扩展来实现变化,而不是通过修改已有代码来实现变化,即对扩展开放,对修改关闭。开闭原则主要考虑了项目的未来事件而制定的对现行开发进行约束的原则。这个原则对于实际开发认知观念的改变十分有用,需求并不是实现就行,并不是能跑通即可,必须要能够适应未来的发展变化。
根据原则定义,针对扩展开放,修改关闭是否就意味着我们完全不能够修改代码,只能够添加代码?具体比如在一个类中增加一个方法,难道不是意味着修改了这个类?到底什么样的项目变动可以被定义为扩展,什么样的项目变动可以被定义为修改?
开闭原则的本质就是为了适应未来变化而保证项目的稳定迭代,因此,可以说只要不影响现有功能的改动都是扩展,凡是影响现有功能的改动都是修改。开闭原则要求未来可能发生的改动不会影响到现有代码。回过头看,在类中增加方法,而不是复用原有方法影响原有业务,这就是扩展,是符合开闭原则的。
二、未来变动范围
前面说了,开闭原则主要考虑了项目的未来事件而制定的对现行开发进行约束的原则。为了避免过度设计,我们也需要说清楚未来变化的可能程度。就如用户信息距离,在用户权限管理项目中,我们可能需要细化用户与权限的关系,权限存在很多种,每个用户均可能存在多个权限。如果设计初始化方案如下:
如上图(a)所示,如果后续业务添加权限3,此时需要改动用户1、用户2的权限信息,非常不利于后续维护,甚至会对之前的权限信息产生影响。而后一种方案(b)将各种权限进行分组为角色,用户拥有不同角色,再根据角色关联其拥有的权限。后续添加权限3,只需要增加角色下的权限即可,不会影响用户的其他角色的权限信息。
从上看,针对于用户信息中的权限信息可以考虑通过引入角色来尽可能减少未来变化对已有功能的影响,其实,用户信息中的联系方式、姓名等信息,如果仔细思考也有可能在未来发生改变,如不同种类的联系方式、不同格式的姓名等。所以,我们需要根据自己的具体业务来判断是否需要考虑增加用户某信息后,未来是否可能发生改变。
三、如何做到?
理解了开闭原则概念之后,我们就必须要知道如何做才能尽可能满足开闭原则。这一点非常难,难是因为大部分都没有对此真正的思考过。要说明的是,并不是你使用了哪种设计模式或遵循了各种设计原则,所以你的代码就满足了开闭原则。前面一小节避免开闭原则的过度设计就是要理解当下具体业务。而要满足开闭原则设计,我认为也应当从当下具体业务出发,分解业务需求,根据业务理解把握未来变化的提前设计。下面我们通过具体的案例,来体会下实际开发过程中应当如何满足开闭原则。
假设目前需要开发一个根据系统各种指标监控以及告警规则来向运维同学发出告警的小工具。
3.1 初步方案
初步方案不考虑设计原则,也不考虑代码可扩展性,目的仅是实现功能即可。设计类图及核心相关代码如下所示:
public class Client {
private final Alarm alarm = new Alarm();
private final AlarmRule rule = new AlarmRule();
public Client() {rule.setQpsLimiter(100);}
@Test
public void main() {
double serverQps = 112.5;
alarm.doAlarm(serverQps, rule);
}
}
public class Alarm {
private final TelePhone telePhone = new TelePhone();
public void doAlarm(double qps, AlarmRule rule) {
if(qps > rule.getQpsLimiter()) {
telePhone.call("触发qps阈值告警!!");
}
}
}
目前已经能够实现通过系统qps指标来进行系统监控报警的功能了,但是这样的代码是否满足开闭原则值得我们思考一下。未来如果需要增加系统其他指标以及其他告警规则,这种代码如何修改呢?① 修改alarm的doAram方法,方法入参和内部逻辑均需要修改。并且修改入参这个动作明显会影响到原有功能。② AlarmRule类需要修改,增加其他规则,以及判断规则的方法。很显然仔细分析这种设计代码的方式并不满足开闭原则。
3.2 正确方案
为了满足开闭原则的设计,我们需要分析需求本身。需求的目的是为了开发一个根据系统各种监控指标以及一系列的告警规则来发送告警的工具。首先我们要分析需求本身具备哪些设计对象。
- 监控指标
存在多个,并且后续可能存在其他扩展 - 告警规则
存在多个,不同规则判断逻辑不同 - 发送告警手段
可能存在多个方式去发送告警,如电话、邮件等等。不同方式之间发送的要求不同
然后就是不同设计对象之间的交互问题,如告警规则需依赖监控指标,监控指标、告警手段应都只又Client进行感知即可。设计类图及核心相关代码如下所示:
public class Client {
private final UserInfo userInfo;
private final Notice alarmMethod;
private final List<Rule> ruleList = new ArrayList<>();
public Client() {
userInfo = new UserInfo()
.setName("张三")
.setTelePhone("110")
.setEmailInfo("zs@110.com");
alarmMethod = new TelePhoneNotice();
ruleList.add(new QpsLimiterRule(100));
ruleList.add(new ResponseTimeRule(20, 300));
}
@Test
public void main() {
Monitor monitor = new Monitor()
.setQps(110.2)
.setAverageTime(35)
.setMaxTime(203);
for (Rule rule : ruleList) {
if (rule.checkMonitor(monitor)) {
alarmMethod.sendMsg(userInfo, rule.alarmMsg());
return;
}
}
}
}
相比于之前的设计方案,这种代码组织设计更优。以后即使增加监控指标以及告警规则,在此代码基础上的改动也不会影响到原有规则判断逻辑,只需要在Monitor类中添加监控指标,添加新的规则Rule实现类。每个告警规则都有独自的规则判断逻辑,以及不满足规则时告警文案。
3.3 小结
总结下实现开闭原则的大致步骤:
- 分析需求目标
- 拆分实现需求目标的各个设计元素,设计元素的含义、特点以及后续是否存在改动。
- 分析各设计元素之间的业务依赖关系
- 分析整体设计对代码扩展性、稳定性、可维护性
最后还有一点,避免过度设计,如上例子中,我们并没有对监控指标进行分类拆分,也没有对用户信息进行过度设计,因为这些都不是需求目标(业务)的重点,这种告警工具以后会需要用户权限管理吗?不太可能需求。【但是可能需要用户分组告警,即使如此,这个改动也是在客户端Client中修改,不影响原功能逻辑】