首页 / 资讯中心 / 文章详情

【设计模式之美】SOLID 原则之二:开闭原则方法论、开闭原则如何取舍

【设计模式之美】SOLID 原则之二:开闭原则方法论、开闭原则如何取舍 ★ FEATURED ARTICLE
文章目录一. 如何理解“对扩展开放、修改关闭”二. 修改代码就意味着违背开闭原则吗三. 如何做到“对扩展开放、修改关闭”四. 如何在项目中灵活应用开闭原则一. 如何理解“对扩展开放、修改关闭”具体的说添加一个新的功能应该是在已有代码基础上扩展代码新增模块、类、方法等而非修改已有代码修改模块、类、方法等。举例说明//业务逻辑主要集中在 check() 函数中。当接口的 TPS 超过某个预先设置的最大值时以及当接口请求出错数大于某个最大允许值时就会触发告警通知接口的相关负责人或者团队。publicclassAlert{privateAlertRulerule;privateNotificationnotification;publicAlert(AlertRulerule,Notificationnotification){this.rulerule;this.notificationnotification;}publicvoidcheck(Stringapi,longrequestCount,longerrorCount,longdurationOfSeconds){longtpsrequestCount/durationOfSeconds;if(tpsrule.getMatchedRule(api).getMaxTps()){notification.notify(NotificationEmergencyLevel.URGENCY,...);}if(errorCountrule.getMatchedRule(api).getMaxErrorCount()){notification.notify(NotificationEmergencyLevel.SEVERE,...);}}}现在如果我们需要添加一个功能当每秒钟接口超时请求个数超过某个预先设置的最大阈值时我们也要触发告警发送通知。通过修改已有代码来实现//- 第一处是修改 check() 函数的入参添加一个新的统计数据 timeoutCount表示超时接口请求数//- 第二处是在 check() 函数中添加新的告警逻辑。public class Alert{...// 改动一添加参数timeoutCountpublicvoidcheck(String api,longrequestCount,longerrorCount,longtimeoutCount,longdurationOfSeconds){longtpsrequestCount/durationOfSeconds;if(tpsrule.getMatchedRule(api).getMaxTps()){notification.notify(NotificationEmergencyLevel.URGENCY,...);}if(errorCountrule.getMatchedRule(api).getMaxErrorCount()){notification.notify(NotificationEmergencyLevel.SEVERE,...);}// 改动二添加接口超时处理逻辑longtimeoutTpstimeoutCount/durationOfSeconds;if(timeoutTpsrule.getMatchedRule(api).getMaxTimeoutTps()){notification.notify(NotificationEmergencyLevel.URGENCY,...);}}}这样的代码修改实际上存在挺多问题的。一方面我们对接口进行了修改这就意味着调用这个接口的代码都要做相应的修改。另一方面修改了 check() 函数相应的单元测试都需要修改关于单元测试的内容我们在重构那部分会详细介绍。通过“扩展”的方式来实现同样的功能先重构一下之前的 Alert 代码让它的扩展性更好一些。重构一将 check() 的入参封装到 ApiStatInfo 类重构二引入 handler 的概念将 if 判断逻辑分散在各个 handler 中。public class Alert{private ListAlertHandleralertHandlersnew ArrayList();publicvoidaddAlertHandler(AlertHandler alertHandler){this.alertHandlers.add(alertHandler);}//封装入参publicvoidcheck(ApiStatInfo apiStatInfo){for(AlertHandler handler:alertHandlers){handler.check(apiStatInfo);}}}public class ApiStatInfo{//省略constructor/getter/setter方法private String api;privatelongrequestCount;privatelongerrorCount;privatelongdurationOfSeconds;}//2. 添加handler抽象类定义公共属性和方法public abstract class AlertHandler{protected AlertRule rule;protected Notification notification;publicAlertHandler(AlertRule rule,Notification notification){this.rulerule;this.notificationnotification;}public abstractvoidcheck(ApiStatInfo apiStatInfo);}public class TpsAlertHandler extends AlertHandler{publicTpsAlertHandler(AlertRule rule,Notification notification){super(rule,notification);}Override publicvoidcheck(ApiStatInfo apiStatInfo){longtpsapiStatInfo.getRequestCount()/apiStatInfo.getDurationOfSeconds();if(tpsrule.getMatchedRule(apiStatInfo.getApi()).getMaxTps()){notification.notify(NotificationEmergencyLevel.URGENCY,...);}}}public class ErrorAlertHandler extends AlertHandler{publicErrorAlertHandler(AlertRule rule,Notification notification){super(rule,notification);}Override publicvoidcheck(ApiStatInfo apiStatInfo){if(apiStatInfo.getErrorCount()rule.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()){notification.notify(NotificationEmergencyLevel.SEVERE,...);}}}在重构之后的代码中实现新的需求有如下改动在 ApiStatInfo 类中添加新的属性 timeoutCount。添加新的 TimeoutAlertHander 类。在 ApplicationContext 类的 initializeBeans() 方法中往 alert 对象中注册新的 timeoutAlertHandler。在使用 Alert 类的时候需要给 check() 函数的入参 apiStatInfo 对象设置 timeoutCount 的值。publicclassAlert{// 代码未改动... }publicclassApiStatInfo{//省略constructor/getter/setter方法privateStringapi;privatelongrequestCount;privatelongerrorCount;privatelongdurationOfSeconds;privatelongtimeoutCount;// 改动一添加新字段}publicabstractclassAlertHandler{//代码未改动... }publicclassTpsAlertHandlerextendsAlertHandler{//代码未改动...}publicclassErrorAlertHandlerextendsAlertHandler{//代码未改动...}// 改动二添加新的handlerpublicclassTimeoutAlertHandlerextendsAlertHandler{//省略代码...}publicclassApplicationContext{privateAlertRulealertRule;privateNotificationnotification;privateAlertalert;publicvoidinitializeBeans(){alertRulenewAlertRule(/*.省略参数.*/);//省略一些初始化代码notificationnewNotification(/*.省略参数.*/);//省略一些初始化代码alertnewAlert();alert.addAlertHandler(newTpsAlertHandler(alertRule,notification));alert.addAlertHandler(newErrorAlertHandler(alertRule,notification));// 改动三注册handleralert.addAlertHandler(newTimeoutAlertHandler(alertRule,notification));}//...省略其他未改动代码...}publicclassDemo{publicstaticvoidmain(String[]args){ApiStatInfoapiStatInfonewApiStatInfo();// ...省略apiStatInfo的set字段代码apiStatInfo.setTimeoutCount(289);// 改动四设置tiemoutCount值ApplicationContext.getInstance().getAlert().check(apiStatInfo);}重构之后的代码更加灵活和易扩展。如果我们要想添加新的告警逻辑只需要基于扩展的方式创建新的 handler 类即可不需要改动原来的 check() 函数的逻辑。而且我们只需要为新的 handler 类添加单元测试老的单元测试都不会失败也不用修改。二. 修改代码就意味着违背开闭原则吗看了上面重构之后的代码你可能还会有疑问在添加新的告警逻辑的时候尽管改动二添加新的 handler 类是基于扩展而非修改的方式来完成的但改动一、三、四貌似不是基于扩展而是基于修改的方式来完成的那改动一、三、四不就违背了开闭原则吗我们回到这条原则的设计初衷只要它没有破坏原有的代码的正常运行没有破坏原有的单元测试我们就可以说这是一个合格的代码改动。分析一下改动三和改动四这两处改动都是在方法内部进行的不管从哪个层面模块、类、方法来讲都不能算是“扩展”而是地地道道的“修改”。不过有些修改是在所难免的是可以被接受的。如果我们把 Alert 类及各个 handler 类合起来看作一个“模块”那模块本身在添加新的功能的时候完全满足开闭原则。要认识到添加一个新功能不可能任何模块、类、方法的代码都不“修改”这个是做不到的。类需要创建、组装、并且做一些初始化操作才能构建成可运行的的程序这部分代码的修改是在所难免的。我们要做的是尽量让修改操作更集中、更少、更上层尽量让最核心、最复杂的那部分逻辑代码满足开闭原则。三. 如何做到“对扩展开放、修改关闭”实际上开闭原则讲的就是代码的扩展性问题是判断一段代码是否易扩展的“金标准”。如果某段代码在应对未来需求变化的时候能够做到“对扩展开放、对修改关闭”那就说明这段代码的扩展性比较好。所以问如何才能做到“对扩展开放、对修改关闭”也就粗略地等同于在问如何才能写出扩展性好的代码。在讲具体的方法论之前我们先来看一些更加偏向顶层的指导思想。为了尽量写出扩展性好的代码我们要具备扩展意识、抽象意识、封装意识。支持开闭原则的一些更加具体的方法论。在众多的设计原则、思想、模式中最常用来提高代码扩展性的方法有多态、依赖注入、基于接口而非实现编程以及大部分的设计模式比如装饰、策略、模板、职责链、状态等。如何利用多态、依赖注入、基于接口而非实现编程来实现“对扩展开放、对修改关闭”。比如我们代码中通过 Kafka 来发送异步消息。对于这样一个功能的开发我们要学会将其抽象成一组跟具体消息队列Kafka无关的异步消息接口。所有上层系统都依赖这组抽象的接口编程并且通过依赖注入的方式来调用。当我们要替换新的消息队列的时候比如将 Kafka 替换成 RocketMQ可以很方便地拔掉老的消息队列实现插入新的消息队列实现。具体代码如下所示// 这一部分体现了抽象意识publicinterfaceMessageQueue{//... }publicclassKafkaMessageQueueimplementsMessageQueue{//... }publicclassRocketMQMessageQueueimplementsMessageQueue{//...}publicinterfaceMessageFromatter{//... }publicclassJsonMessageFromatterimplementsMessageFromatter{//...}publicclassProtoBufMessageFromatterimplementsMessageFromatter{//...}publicclassDemo{privateMessageQueuemsgQueue;// 基于接口而非实现编程publicDemo(MessageQueuemsgQueue){// 依赖注入this.msgQueuemsgQueue;}// msgFormatter多态、依赖注入publicvoidsendNotification(Notificationnotification,MessageFormattermsgFormatter){//...}}四. 如何在项目中灵活应用开闭原则前面我们提到写出支持“对扩展开放、对修改关闭”的代码的关键是预留扩展点。那问题是如何才能识别出所有可能的扩展点呢最合理的做法对于一些比较确定的、短期内可能就会扩展或者需求改动对代码结构影响比较大的情况或者实现成本不高的扩展点在编写代码的时候之后我们就可以事先做些扩展性设计。对于一些不确定未来是否要支持的需求或者实现起来比较复杂的扩展点我们可以等到有需求驱动的时候再通过重构代码的方式来支持扩展的需求。开闭原则增加了理解难度我们之前举的 Alert 告警的例子。为了更好地支持扩展性我们对代码进行了重构重构之后的代码要比之前的代码复杂很多理解起来也更加有难度。需要在扩展性和可读性之间做权衡在某些场景下代码的扩展性很重要我们就可以适当地牺牲一些代码的可读性在另一些场景下代码的可读性更加重要那我们就适当地牺牲一些代码的可扩展性。在 Alert 告警的例子中如果告警规则并不是很多、也不复杂那 check() 函数中的 if 语句就不会很多代码逻辑也不复杂代码行数也不多那最初的第一种代码实现思路简单易读就是比较合理的选择。相反如果告警规则很多、很复杂check() 函数的 if 语句、代码逻辑就会很多、很复杂相应的代码行数也会很多可读性、可维护性就会变差那重构之后的第二种代码实现思路就是更加合理的选择了。参考《设计模式之美》–王争
阅读完成 · 觉得有帮助?
咨询建站