前言软件设计原则的关键原则之一是开闭原则,即所谓对扩展开放,对修改封闭。我个人认为这个原则非常重要。它直接关系到你的设计是否具有良好的可扩展性,但相对难以理解和掌握。什么样的代码更改被定义为“扩展”?什么样的代码更改被定义为“修改”?如何满足或违反“开闭原则”?别担心,这篇文章会详细解释。举个例子方便理解。为了更好的说明,我们直接举个例子。这是监控告警类。Alert是监控告警类。AlertRule存储告警规则信息。Notification是告警通知类。publicclassAlert{//存储警报规则privateAlertRule规则;//告警通知类,支持邮件、短信、微信、手机等多种通知渠道。私人通知通知;publicAlert(AlertRulerule,Notificationnotification){this.rule=rule;this.notification=通知;}//检查是否发送告警publicvoidcheck(Stringapi,longrequestCount,longerrorCount,longdurationOfSeconds){//计算请求的tpslongtps=requestCount/durationOfSeconds;//如果tps大于阈值,则发送告警}//如果错误数量大于规则阈值则发出警报if(errorCount>rule.getMatchedRule(api).getMaxErrorCount()){notification.notify(NotificationEmergencyLevel.SEVERE,"...");}}}复制代码这个alert的核心业务逻辑主要集中在check()函数中。当接口的TPS超过预设的最大值时,触发告警并发送通知。当接口请求错误次数超过某个最大允许值时,会触发告警,通知接口相关负责人或团队。现在有一个新的要求。当每秒接口超时请求数超过预设的最大阈值时,我们还需要触发告警发送通知。这个时候,我们应该怎么改代码呢?第一种方法简单,开始工作后直接写下面的代码即可。publicclassAlert{//...省略AlertRule/Notification属性和构造函数...//更改1:添加参数timeoutCountpublicvoidcheck(Stringapi,longrequestCount,longerrorCount,longtimeoutCount){longtps=requestCount/持续时间;如果(tps>rule.getMatchedRule(api).getMaxTps()){notification.notify(NotificationEmergencyLevel.URGENCY,"...");}if(errorCount>rule.getMatchedRule(api).getMaxErrorCount()){notification.notify(NotificationEmergencyLevel.SEVERE,"...");}//变化2:增加接口超时处理逻辑longtimeoutTps=timeoutCount/durationOfSeconds;if(timeoutTps>rule.getMatchedRule(api).getMaxTimeoutTps()){notification.notify(NotificationEmergencyLevel.URGENCY,"...");}}复制代码,修改如下:check()方法增加一个timeoutCount参数。在check()方法的逻辑中增加接口超时处理逻辑。这种方法有什么问题?你居然调整了check()方法的参数,原来调用的地方都得修改。如果有很多,我讨厌你。修改了check()函数,需要修改相应的单元测试。这种情况下,我们完全是在修改原来的代码,不符合开闭原则。方法二这时候你动脑筋大刀阔斧地重构。引入ApiStatInfo类来封装check的入参信息。publicclassApiStatInfo{//省略constructor/getter/setter方法privateStringapi;私人长请求计数;私人长错误计数;私人长durationOfSeconds;}复制代码引入handler的概念,将if判断逻辑分散在各个handler中publicabstractclassAlertHandler{protectedAlertRulerule;受保护的通知通知;publicAlertHandler(AlertRulerule,Notificationnotification){this.rule=rule;this.notification=通知;}publicabstractvoidcheck(ApiStatInfoapiStatInfo);}//TPS警报处理程序publicclassTpsAlertHandlerextendsAlertHandler{publicTpsAlertHandler(AlertRulerule,Notificationnotification){super(rule,notification);}@Overridepublicvoidcheck(ApiStatInfoapiStatInfo){longtps=apiStatInfo。.getMatchedRule(apiStatInfo.getApi()).getMaxTps()){notification.notify(NotificationEmergencyLevel.URGENCY,"...");}}}//错误计数报警处理器publicclassErrorAlertHandlerextendsAlertHandler{publicErrorAlertHandler(AlertRulerule,Notificationnotification){super(rule,notification);}@Overridepublicvoidcheck(ApiStatInfoapiStatInfo){if(apiStatInfo.getErrorCount()>rule.getApiStatched()notification.notify(NotificationEmergencyLevel.SEVERE,"...");}}复制代码修改Alert类和添加各种警报处理程序公共类警报{privateListalertHandlers=newArrayList<>();publicvoidaddAlertHandler(AlertHandleralertHandler){this.alertHandlers.add(alertHandler);}publicvoidcheck(ApiStatInfoapiStatInfo){//遍历各种告警Processorfor(AlertHandlerhandler:alertHandlers){handler.check(apiStatInfo);}}}复制代码UppersingletonclassApplicationContext创建、组装、使用Alert类publicclassApplicationContext{privateAlertRulealertRule;私人通知通知;私人警报警报;publicvoidinitializeBeans(){alertRule=newAlertRule(/*。省略参数。*/);//省略一些初始化代码notification=newNotification(/*.省略参数.*/);//省略一些初始化代码alert=newAlert();//添加警报处理程序alert.addAlertHandler(newTpsAlertHandler(alertRule,notification));alert.addAlertHandler(newErrorAlertHandler(alertRule,notification));}//返回警报器AlertpublicAlertgetAlert(){returnalert;}//饿了么中国风单例privatestaticfinalApplicationContextinstance=n新的应用程序上下文();privateApplicationContext(){instance.initializeBeans();}publicstaticApplicationContextgetInstance(){返回实例;}}publicclassDemo{publicstaticvoidmain(String[]args){ApiStatInfoapiStatInfo=newApiStatInfo();//...省略设置apiStatInfo数据值的代码//执行警报操作ApplicationContext.getInstance().getAlert().check(apiStatInfo);在此基础上,我们应该如何修改代码来响应每秒接口超时请求数超过某个最大阈值并报警的要求呢?ApiStatInfo类添加新字段publicclassApiStatInfo{//省略构造函数/getter/setter方法privateStringapi;私人长请求计数;私人长错误计数;私人长durationOfSeconds;NewhandlerclassTimeoutAlertHandlerpublicclassTimeoutAlertHandlerextendsAlertHandler{//省略代码...}复制代码修改ApplicationContext类添加注册TimeoutAlertHandlerpublicclassApplicationContext{....publicvoidinitializeBeans(){alertRule=newAlertRule(/*.省略参数.*/);//省略一些初始化代码notificationation=newNotification(/*。省略参数。*/);//省略一些初始化代码alert=newAlert();alert.addAlertHandler(新TpsAlertHandler(alertRule,通知));alert.addAlertHandler(newErrorAlertHandler(alertRule,notification));//更改3:注册处理程序alert.addAlertHandler(newTimeoutAlertHandler(alertRule,notification));}//...省略其他不变的代码}复制代码调用处理告警的地方并设置参数publicclassDemo{publicstaticvoidmain(String[]args){//...省略apiStatInfo的设置字段代码apiStatInfo.setTimeoutCount(289);//更改4:设置tieoutCount值ApplicationContext.getInstance()。getAlert().check(apiStatInfo);}}复制代码。有没有发现重构完成后,代码的可扩展性特别好?如果有新的告警处理,我只需要添加一个新的handler类并注册即可,而不需要修改原来的校验逻辑,只需要为新添加的类编写单元测试即可。这种情况符合开闭原则。可能你会纠结我明明修改了代码,为什么修改关闭了?第一个修改是向ApiStatInfo类添加一个新属性timeoutCount。事实上,开闭原则可以应用于不同粒度的代码,可以是模块、类,也可以是方法(及其属性)。同样的代码改动,在粗代码粒度下可以识别为“修改”,在细代码粒度下可以识别为“扩展”。比如这里添加属性和方法等同于修改类。在类的层面上,这种代码变化可以被识别为“修改”;但此代码更改不会修改现有的属性和方法。在方法(及其属性)的一个层面上,可以将其标识为“扩展”。另一个修改是在ApplicationContext类的initializeBeans()方法中的alert对象中注册一个新的timeoutAlertHandler;使用Alert类时,需要为check()函数的apiStatInfo对象设置timeoutCount的值。首先,不“修改”任何模块、类或方法的代码就不可能添加新功能。主要看修改的内容。这里修改的是上层的代码,不是核心下层的代码,所以可以接受。如何理解开闭原理?上面通过一个例子详细说明了开闭原则的核心思想,对修改封闭,对扩展开放。这里再次总结一下,让大家进一步了解开闭的原理。添加新功能应该在已有代码的基础上通过扩展代码(添加模块、类、方法、属性等)来完成,而不是修改已有代码(修改模块、类、方法、属性等)完成.关于定义,我们有两点需要注意。第一点,开闭原则不是说完全杜绝修改,而是要以修改代码的最小成本完成新功能的开发,尽量修改上层代码,而不是修改底层或核心逻辑代码。第二点,同样的代码改动在粗代码粒度上可能被识别为“修改”;在细的代码粒度上,它可能被识别为“扩展”,比如给一个类增加一个字段或方法,在某些情况下我们也可以认为它是一个扩展。开闭原则就一定好吗?开闭原则不是没有条件的。在某些情况下,代码的可伸缩性与可读性相冲突。比如我们之前给出的Alert报警的例子。为了更好地支持可扩展性,我们对代码进行了重构。重构后的代码比之前的代码复杂了很多,也更难理解。很多时候,我们需要在可扩展性和可读性之间做出权衡。在某些场景下,代码的可扩展性非常重要,我们可以适当牺牲一些代码的可读性;在其他场景下,代码的可读性更重要,那么我们可以适当牺牲一些代码可扩展性的可读性。在我们之前给出的Alert告警的例子中,如果告警规则不是很多很复杂,那么check()函数中的if语句就不会很多,代码逻辑并不复杂,代码行数不多,那么原先的代码实现思路简单易读,是比较合理的选择。反之,如果告警规则多而复杂,check()函数的if语句和代码逻辑就会多而复杂,相应的代码行也会多,可读性和可维护性就会变差。结构之后的第二种代码实现思路是比较合理的选择。总之,这里没有放之四海而皆准的参考标准,全看实际应用场景。如何实现“对扩展开放,对修改关闭”?开闭原则本质上是为了让你写的程序具有可扩展性。这就需要你平时慢慢积累、慢慢学习,需要时刻有扩展、抽象、封装的意识。这些“潜意识”可能比任何开发技能都重要。通常,你需要考虑很多,未来这段代码可能会有什么需求变化,代码结构如何设计,提前预留扩展点,这样以后需求变化的时候,你不需要去改变整体结构的代码并实现最小的代码更改。新代码可以灵活地插入到扩展点中,做到“对扩展开放,对修改关闭”。但切记不要过度设计,否则维护起来会很困难,而且后果不堪设想。至于具体的方法论层面,我强烈建议大家应该是面向接口编程。你怎么理解的?比如有一个业务需求,向Kafka发送消息。可以在业务代码中直接调用Kafka的API发送消息。这就是面向实现的编程。?这个时候我们是不是应该定义一个发送消息的接口,让上层直接调用这个接口。总结本文阐述了软件设计中最重要的设计原则之一,开闭原则,即对扩展开放,对修改封闭,它将指导我们编写具有良好扩展性的代码,设计出更具扩展性的架构。