本文主要是介绍面向对象设计之开闭原则,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
设计模式专栏: http://t.csdnimg.cn/4Mt4u
目录
1.引言
2.如何理解“对扩展开放、对修改关闭”
3.修改代码就意味着违反开闭原则吗
4.如何做到“对扩展开放、对修改关闭”
5.如何在项目中灵活应用开闭原则
6.总结
1.引言
开闭原则(Open Closed Principle,OCP),又称为“对扩展开发、对修改关闭”原则。开闭原则既是 SOLID 原则中最难理解、最难掌握的,又是最有用的。之所以说开闭原则难理解,是因为“怎样的代码改动才被定义为'扩展’?怎样的代码改动才被定义为'修改’?怎么才算满足或违反'开闭原则’?修改代码就一定意味着违反'开闭原则’吗?”等问题都比较难理解。之所以说开闭原则难掌握,是因为“如何做到'对扩展开发、对修改关闭’?如何在项目中灵活应用'开闭原则’,避免在追求高扩展性的同时影响代码的可读性?”等问题都比较难掌握。
之所以说开闭原则最有用,是因为扩展性是代码质量的重要衡量标准。在22种经典设计模式中,大部分设计模式都是为了解决代码的扩展性问题而产生的,它们主要遵守的设计原则就是开闭原则。
2.如何理解“对扩展开放、对修改关闭”
开闭原则的英文描述是:software enities(modules,classes,fiuncions,etc.)should be open for extension but closed for modification。对应的中文为:软件实体(模块、类和方法等)应该“对扩展开放、对修改关闭”,详细表述为: 添加一个新功能时应该是在已有代码基础上扩展代码(新类和方法等),而非修改已有代码(修改模块、类和方法等)。
为了让读者更好地理解开闭原则,我们举例说明。
下面是一段AR(应用程序编程接口)监控告警的代码。其中,AlertRule 类存储告警规则:Notification类负责告警通知,支持电子邮件、短信和微信等多种通知渠道;NotificationEmergencyLevel类表示告警通知的紧急程度,包括SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)和 TRIVIAL(无关紧要),不同的紧急程度对应不同的通知渠道。
public class Alert{private AlertRule rule;private Notifcation notification;public Alert(AlertRule rule, Notification notification){this.rule = rule;this.notifcation = notifcation;}public void check(String api,long requestCount, long errorCount, long duration){long tps=requestCount/duration;if (tps > rule.getMatchedRule(api).getMaxTps()) { notification.notify(NotificationEmergencyLevel.URGENCY,"...");}if(errorCount>rule.getMatchedRule(api).getMaxErrorCount()){notification.notify(NotifcationEmergencyLevel.SEVERE,"...");}}
}
上面这段代码的业务逻辑主要集中在check()函数中。当接口的TPS(Transactions PerSecond,每秒事务数)超过预先设置的最大值时,或者当接口请求出错数大于最大允许值时就会触发告警,通知接口的相关负责人或团队。
如果我们需要添加更多的告警规则:“当每秒接口超时请求个数超过预先设置的最大值时也要触发告警并发送通知”,那么如何改动代码呢?代码的主要改动有两处:第一处是修改check()函数的入参,添加一个新的统计数据 timeoutCount,表示超时接口请求数; 第二处是有check()函数中添加新的告警逻辑。具体的代码改动如下所示。
public class Alert{//...省略AlertRule/Notifcation属性和构造函数...//改动一:添加参数timeoutCountpublic void check(String api, long regueatcount, long erorcount, long timeoutCount, long duration){long tps=reqestCount /duration;if (tps > rule.getMatchedRule(api) .getMaxTps()){notification.notify(NotificationEmergencyLevel.URGENCY, "...");}if(errorCount> rule.getMatchedRule(api).getMaxErrorCount())){notification.notify(NotificationEmergencyLevel.SEVERE, "...");}//改动二:添加接口超时处理逻辑long timeoutTps=timeoutCount /duration;if(timeoutTps > rule.getMatchedRule(api).getMaxTimeoutTps()) {notification.notify(NotificationEmergencyLevel.URGENCY, "...");}}
}
上述代码的改动带来下列两方面的问题。一方面,对接口进行了修改,调用这个接口的代码就要做相应的修改。另一方面,修改了check()函数,相应的单元测试需要修改。
上述代码改动是基于“修改”方式增加新的告警。如果我们遵守开闭原则,也就是“对扩展开放、对修改关闭”,那么如何通过“扩展”方式增加新的告警呢?
我们先重构添加新的告警之前的 Alert类的代码,让它的扩展性更好。重构的内容主要包含两部分:第一部分是将check()函数的多个入参封装成ApiStatInfo类;第二部分是引入handler(告警处理器),将if判断逻辑分散到各个handler 中。具体的代码实现如下。
public class Alert{private list<AlertHandler> alertHandlers = new ArrayList<>();public void addAlertHandler(AlertHandler alertHandler) { this.alertHandlers.add(alertHandler);}public void check(ApiStatInfo apistatInfo){for(AlertHandler handler:alertHandlers){handler.check(apistatInfo);}}public class ApistatInfo{//省略constructor、getter和setter方法private string api;private long requestCount;private long errorCount;private long duration;}public abstract class AlertHandler(protected AlertRule rule;protected Notification notification;public AlertHandler(AlertRule rule, Notifcation notification){this.rule rule;this.notification=notifcation;}public abstract void check(ApiStatInfo apistatInfo) ;}public class TpsAlertHandler extends AlertHandler {public TpsAlertHandler(AlertRule rule, Notification notification){super(rule, notifcation);}}@Overridepublic void check(ApiStatInfo apistatInfo){long tps = apiStatInfo.getReguestcount () / apiStatInfo.getpuration();if (tps > rule.getMatchedRule(apiStatInfo.getApi()).getMaxTps()){ notification.notify(NotifcationEmergencyLevel.URGENCY, "...");}}}public class ErrorAlertHandler extends AlertHandler{public ErrorAlertHandler (AlertRule rule, Notifcation notification){super(rule, notifcation);}@Overridepublic void check (ApiStatInfo apistatInfo) {if (apistatInfo.getErrorCount() > rule.getMatchedRule(apistatInfo.getApi()).getMaxErrorCount()){notifcation.notify(NotifcationEmergencyLevel.SEVERE,"...");}}
}
接下来,我们看一下重构之后的Alent类的具体使用方式,如下列代码所示。其中ApplicationContext是一个单例类,负责Alert类的创建、组装(alertRule和notification 的依赖注入)和初始化(添加handler)。
public class ApplicationContext{private AlertRule alertRule;private Notifcation notification;private Alert alert;public void initializeBeans(){alertRule=new AlertRule(/*.省略参数,*/);//省略一些初始化代码notifcation=new Notification(/*.省略参数.*/);//省略一些初始化代码alert=new Alert();alert.addAlertHandler(new TpsAlertHandler(alertRule,notifcation));alert.addAlertHandler(new ErrorAlertHandler(alertRule, notifcatíon));}public Alert getAlert(){ return alert; )//“饿汉式”单例private static final Applicationcontext instance = new ApplicationContext();private ApplicationContext(){initializeBeans();}public static ApplicationContext getInstance(){return instance;}
}public class Demo{public static void main(string[] args){ApiStatInfo apistatInfo = new ApiStatInfo();//...省略设置apistatInfo数据值的代码Applicationcontext.getinstance().getAlert().check(apistatInfo)}
}
对于重构之后的代码,如果添加新的告警:“如果每秒接口超时请求个数超过最大值,就告警”,那么如何改动代码呢?主要的改动有下面4处。
改动一: 在 ApiStatInfo 类中添加新属性 timeoutCount。
改动二:添加新的 TimeoutAlertHander 类。
改动三:在ApplicationContecxt 类的 initializeBeans()方法中,向 alert对象中注册
Timeout-AlertHandler。
改动四:使用 Alert 类时,需要给check()函数的入参 apiStatInfo对象设置timeoutCount属性值。
改动之后的代码如下所示:
public class Alert{//代码未改动}public class ApistatInfo{//省略constructorgetter和setter方法private String api;private long requegtCount;private long errorCount;private long duratlon; private 1ong timeoutCount;//改动一:添加新属性timeoutcount
}public abstract class AlertHandler{//代码未改动
public class TpsAlertHandler extends AlertHandler{ //代码未改动}
public class ErrorAlertHandler extends AlertHandler { //代码未改动}
//改动二:添加新的TimeoutAlertHander类
public class TimeoutAlertHandler extends AlertHandler {//省略代码public class ApplicationContext{private AlertRule alertRule;private Notification notifcation;private Alert alert;public void initinlizeBeanst{alertRule = new AlertRule(/*.省路参数。*/);//省略一些初始化代码 notification = new Notification(/*.省略参数.*/);//省路一些初始化代码alert = now Alert();alert.addAlertHandler(new psAlertHandler(alertRule, notification ));alert.addAlertHandler(new ErrorAlertHandler(alertRule, notification));//改动三:向alert对象中注册TimeoutAlertHandleralert.addAlertHandler(new imeoutAlertHandler(alertRule, notification));}//...省略其他未改动代码
}public class Demo{public static void main(Stringl] args){ApiStatInfo apiStatInfo = new ApistatInfo();apistatInfo.setTimeoutCount(289);//改动四:设置timeoutCount值//...省路apistatInfo的set字段代码ApplicationContext.getInstance().getAlert().check(apiStatInfo);
}
重构之后的代码更加灵活,更容易扩展。如果想要添加新的告警,那么只需要基于扩展的方式创建新的handler类,不需要改动check()函数。不仅如此,我们只需要为新的handler头添加新的单元测试,旧的单元测试都不会失败,也不用修改。
3.修改代码就意味着违反开闭原则吗
读者可能对上面重构之后的代码产生疑间:在添加新的告警时,尽管改动二(添加新的TimeoutAlertHander类)是基于扩展而非修改的方式完成的,但改动一、改动三和改动四是基于修改而非扩展的方式完成的,改动一、改动三和改动四不违反开闭原则吗?
我们先分析一下改动一: 在 ApiStatInfo类中添加新属性 timeoutCount。
在改动一中,我们不仅在ApiStatInfo的类中添加了新的属性,还添加了对应的getter和setter方法。那么,上述问题就转化为:在类中添加新的属性和方法属于“修改”还是“扩展”?
我们回忆一下开闭原则的定义: 软件实体(模块、类和方法等)应该“对扩展开放、对修改关闭”。从定义中可以看出,开闭原则作用的对象可以是不同粒度的代码,如模块、类和方法(及其属性)。对于同一代码改动,在粗代码粒度下,可以被认定为“修改”,在细代码粒度下,可以被认定为“扩展”。例如,“改动一”中添加属性和方法相当于修改类,在类这个层面,这个代码改动可以被认定为“修改”;但这个代码改动并没有修改已有的属性和方法,在方法(及其属性)这一层面,它又可以被认定为“扩展”。
实际上,我们没有必要纠结某个代码改动是“修改”还是“扩展”,更没有必要纠结它是否违反“开闭原则”。回到开闭原则的设计初衷:只要代码改动没有破坏原有代码的正常运行和原有的单元测试,我们就可以认为这是一个合格的代码改动。
我们再来分析一下改动三和改动四:在ApplicationContext类的initializeBeans()方法中向alert 对象中注册 TimeoutAlertHandler;使用Alert类时,给check()函数的入参 apiStatInfo对象设置 timeoutCount 属性值。
这两处改动是在方法内部进行的,无论从哪个层面(模块、类、方法)来看,都不能算是“扩展”,而是“修改”。不过,有些修改是在所难免的,是可以接受的。
在重构之后的 Alent 类代码中,核心逻辑集中在Alent类及其各个handler类中。当添加的告警时,Alen类完全不需要修改,而只需要扩展(新增)一个handler类。如果把 Alent类及其各个handler 类看作一个“模块”,那么,从模块这个层面来说,向模块添加新功能时只需要扩展,不需要修改,完全满足开闭原则。
我们也要认识到,添加一个新功能时,不可能做到任何模块、类和方法的代码都不“修改”。类需要创建、组装,并且会进行一些初始化操作,这样才能构建可运行的程序,这部分代码的修改在所难免。我们努力的方向是尽量让修改操作集中在上层代码中,尽量让核心、复杂、通用、底层的那部分代码满足开闭原则。
4.如何做到“对扩展开放、对修改关闭”
在上面的 Alert类的例子中,我们通过引入一组 handler 类的方式满足了开闭原则。如果读者没有太多复杂代码的设计和开发经验,就可能有这样的疑问:这样的代码设计思路我怎么想不到呢?你是怎么想到的呢?
实际上,之所以作者能够想到,依靠的是扎实的理论知识和丰富的实战经验,这需要读者慢慢学习和积累。对于如何做到“对扩展开放、对修改关闭”,作者有一些指导思想和具体方法分享给读者。
实际上,开闭原则涉及的就是代码的扩展性问题,该原则是判断一段代码是否易扩展的“金标准”。如果某段代码在应对未来需求变化时,能够到“对扩展开放、对修改关闭”,就说明这段代码的扩展性很好。
为了写出扩展性好的代码,我们需要具备扩展意识、抽象意识和封装意识。这些意识可能比任何开发技巧都重要。
在编写代码时,我们需要多花点时间思考: 对于当前这段代码,未来可能有哪些需求变更。如何设计代码结构,事先预留了扩展点,在未来进行需求变更时,不需要改动代码的整体结构,新的代码能够灵活地插入到扩展点上,完成需求变更,从而实现代码的最小化改动。
我们还要善于识别代码中的可变部分和不可变部分。我们将可变部分封装,达到隔离变化的效果,并提供抽象化的不可变接口给上层系统使用。当具体的实现发生变化时,只需要基于相同的抽象接口扩展一个新的实现,替换旧的实现,上层系统的代码几乎不需要修改。
为了实现开闭原则,除在写代设时,我们需要时间具备扩展意识、抽象意识、封装意识以外,我们还有一些具体的方法可以使用。
代码的扩展性是评判代码质量的重要标准。实际上,本文涉及的大部分知识点都是围绕如何提高高代码的扩展性来展开讲解的,本文提到的大部分设计原则和设计模式都是以提高代码的扩展性为最终目的。22种经典设计模式中的大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则而设计的。
有众多的设计原则和设计模式中,常用来提高代码扩展性的方法包括多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(如策略模式、模板方法模式和职责链模式等)设计模式这一部分的内容较多,后面也会详细讲解。本节通过一个简单例子来介绍如何利用多态、依赖注入、基于接口而非实现编程实现开闭原则。
例如,我们希望实现通过Kafka发送异步消息。对于这样一个功能的开发,我们抽象定义一组与具体消息队列(Kafka)无关的异步消息发送接口。所有上层系统都依赖这组抽象的接口编程,并且通过依赖注入的方式来调用。当需要替换消息队列或消息格式时,如将Kafka替换成RocketMQ或将消息的格式从JSON替换为XML,因为代码设计满足开闭原则,所以替换起来非常轻松。具体的代码实现如下所示。
//这一部分代码体现了抽象意识
public interface MessageQueue(...)
public class KafkaMessageQueue implements MessageQueue {...)
public class RocketMQMessageQueue implements MessageQueue (...)public interface MessageFromatter{...}
public class JsonMessagerromatter implements MessageFromatter {..)
public class ProtoBufMessageFromatter implements MessageFromatter { ... }public class Demo(private MessageQueue msgQueue;//基于接口而非实现编程public Demo(MessageQueue msgQueue){ //依赖注入this.msqQueue = msgQueue;}public void send (Notification notification, Messageformatter msgforatter) {...}
}
5.如何在项目中灵活应用开闭原则
上文提到,写出支持开闭原则(扩展性好)的代码的关键是预留扩展点。如何才能识别出有可能的扩展点呢?
如果我们开发的是业务系统,如金融系统、电商系统和物流系统等,要想识别出尽可能多的扩展点,就要对业务有足够的了解。只有这样,才能预见未来可能要支持的业务需求。如要我们开发的是与业务无关的、通用的、偏底层的功能模块,如框架、组件和类库,如果想设出尽可能多的扩展点,就需要了解它们会被如何使用和使用者未来会有哪些功能需求等。
但是,即使我们对业务和系统有足够的了解,也不可能识别出所有的扩展点。即便我们的够识别出所有的扩展点,但为了预留所有扩展点而付出的开发成本往往是不可接受的。因此我们没必要为一些未来不一定需要实现的需求提前“买单”,也就是说,不要进行过度设计。
推荐的做法是,对于一些短期内可能进行的扩展,需求改动对代码结构影响比较大的扩展,或者实现成本不高的扩展,在编写代码时,我们可以事先进行可扩展性设计;但对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的扩展,我们可以等到有需求驱动时,再通过重构的方式来满足扩展的需求。
除此之外,我们还要认识到,开闭原则并不是“免费”的。代码的扩展性往往与代码的可读性冲突。例如上文提供的 Alert类的例子,为了更好地支持扩展性,我们对代码进行了重构,重构之后的代码比原始代码复杂很多,理解难度也增加不少。因此,在平时的开发中,我们需要权衡代码的扩展性和可读性。在一些场景下,代码的扩展性更重要,我们就适当地“牺牲”一些代码的可读性;在一些场景下,代码的可读性更重要,我们就适当地“牺牲”一些代码的扩展性。
在上文提到的 Alert类的例子中,如果告警规则不是很多,也不复杂,那么check()函数中的if分支就不会有很多,对应的代码逻辑不会太复杂,代码行数也不会太多,因此,使用最初的代码实现即可。相反,如果告警规则多且复杂,那么check()函数中的if分支就会有很多对应的代码逻辑就会变复杂,代码行数也会增加,check()函数的可维护性和扩展性就会变差此时,重构代码就变得合理了。
6.总结
开闭原则是面向对象设计中最基础的设计原则之一。其核心思想是:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。这意味着,当应用的需求改变时,在不修改软件实体的源代码或二进制代码的前提下,可以拓展模块的功能,使其满足需求。换句话说,强调的是用抽象构建框架,用实现扩展细节,从而提高软件系统的可复用性及可维护性。
开闭原则并不是要求所有代码都不能修改,而是要求将变化的部分尽可能地封装和抽象出来。通过遵循这一原则,开发者可以构建出更加稳定、灵活且易于维护的软件系统。
这篇关于面向对象设计之开闭原则的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!