目录介绍
- 00. 问题思考剖析
- 01. 前沿简略介绍
- 02. 如何了解开闭准则
- 03. 举一个原始的例子
- 04. 批改后的代码
- 05. 批改代码违反准则么
- 06. 如何做到开闭准则
- 07. 如何使用开闭准则
- 08. 总结一下内容
00. 问题思考剖析
- 01. 什么叫作开闭准则,他的主要用途是什么?
- 02. 如何做到拓展凋谢,批改关闭这一准则,联合案例说一下如何实现?
01. 前沿简略介绍
- 学习 SOLID 中的第二个准则:开闭准则。集体感觉,开闭准则是 SOLID 中最难了解、最难把握,同时也是最有用的一条准则。
- 之所以说这条准则难了解,那是因为,“怎么的代码改变才被定义为‘扩大’?怎么的代码改变才被定义为‘批改’?怎么才算满足或违反‘开闭准则’?批改代码就肯定意味着违反‘开闭准则’吗?”等等这些问题,都比拟难了解。
- 之所以说这条准则难把握,那是因为,“如何做到‘对扩大凋谢、批改敞开’?如何在我的项目中灵便地利用‘开闭准则’,以防止在谋求扩展性的同时影响到代码的可读性?”等等这些问题,都比拟难把握。
- 之所以说这条准则最有用,那是因为,扩展性是代码品质最重要的衡量标准之一。在 23 种经典设计模式中,大部分设计模式都是为了解决代码的扩展性问题而存在的,次要听从的设计准则就是开闭准则。
02. 如何了解开闭准则
-
开闭准则的英文全称是 Open Closed Principle,简写为 OCP。它的英文形容是:software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。咱们把它翻译成中文就是:软件实体(模块、类、办法等)应该“对扩大凋谢、对批改敞开”。
- 这个形容比拟简略,如果咱们具体表述一下,那就是,增加一个新的性能应该是,在已有代码根底上扩大代码(新增模块、类、办法等),而非批改已有代码(批改模块、类、办法等)。
03. 举一个原始的例子
-
为了让你更好地了解这个准则,我举一个例子来进一步解释一下。这是一段 API 接口监控告警的代码。
-
其中,AlertRule 存储告警规定,能够自在设置。Notification 是告警告诉类,反对邮件、短信、微信、手机等多种告诉渠道。NotificationEmergencyLevel 示意告诉的紧急水平,包含 SEVERE(重大)、URGENCY(紧急)、NORMAL(一般)、TRIVIAL(无关紧要),不同的紧急水平对应不同的发送渠道。对于 API 接口监控告警这部分,更加具体的业务需要剖析和设计,咱们会在前面的设计模式模块再拿进去进一步解说,这里你只有简略晓得这些,就够咱们明天用了。
public class Alert { private AlertRule rule; private Notification notification; public Alert(AlertRule rule, Notification notification) { this.rule = rule; this.notification = notification; } public void check(String api, long requestCount, long errorCount, long durationOfSeconds) { long tps = requestCount / durationOfSeconds; if (tps > rule.getMatchedRule(api).getMaxTps()) {notification.notify(NotificationEmergencyLevel.URGENCY, "..."); } if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {notification.notify(NotificationEmergencyLevel.SEVERE, "..."); } } }
-
- 下面这段代码非常简单,业务逻辑次要集中在 check() 函数中。当接口的 TPS 超过某个事后设置的最大值时,以及当接口申请出错数大于某个最大允许值时,就会触发告警,告诉接口的相干负责人或者团队。
-
当初,如果咱们须要增加一个性能,当每秒钟接口超时申请个数,超过某个事后设置的最大阈值时,咱们也要触发告警发送告诉。这个时候,咱们该如何改变代码呢?次要的改变有两处:第一处是批改 check() 函数的入参,增加一个新的统计数据 timeoutCount,示意超时接口申请数;第二处是在 check() 函数中增加新的告警逻辑。具体的代码改变如下所示:
public class Alert { // ... 省略 AlertRule/Notification 属性和构造函数... // 改变一:增加参数 timeoutCount public void check(String api, long requestCount, long errorCount, long timeoutCount, long durationOfSeconds) { long tps = requestCount / durationOfSeconds; if (tps > rule.getMatchedRule(api).getMaxTps()) {notification.notify(NotificationEmergencyLevel.URGENCY, "..."); } if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {notification.notify(NotificationEmergencyLevel.SEVERE, "..."); } // 改变二:增加接口超时解决逻辑 long timeoutTps = timeoutCount / durationOfSeconds; if (timeoutTps > rule.getMatchedRule(api).getMaxTimeoutTps()) {notification.notify(NotificationEmergencyLevel.URGENCY, "..."); } } }
- 这样的代码批改实际上存在挺多问题的。一方面,咱们对接口进行了批改,这就意味着调用这个接口的代码都要做相应的批改。另一方面,批改了 check() 函数,相应的单元测试都须要批改(对于单元测试的内容咱们在重构那局部会具体介绍)。
04. 批改后的代码
-
下面的代码改变是基于“批改”的形式来实现新性能的。如果咱们遵循开闭准则,也就是“对扩大凋谢、对批改敞开”。那如何通过“扩大”的形式,来实现同样的性能呢?咱们先重构一下之前的 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 durationOfSeconds; } public abstract class AlertHandler { protected AlertRule rule; protected Notification notification; public AlertHandler(AlertRule rule, Notification notification) { this.rule = rule; this.notification = notification; } public abstract void check(ApiStatInfo apiStatInfo); } public class TpsAlertHandler extends AlertHandler {public TpsAlertHandler(AlertRule rule, Notification notification) {super(rule, notification); } @Override public void check(ApiStatInfo apiStatInfo) {long tps = apiStatInfo.getRequestCount()/ apiStatInfo.getDurationOfSeconds(); if (tps > rule.getMatchedRule(apiStatInfo.getApi()).getMaxTps()) {notification.notify(NotificationEmergencyLevel.URGENCY, "..."); } } } public class ErrorAlertHandler extends AlertHandler {public ErrorAlertHandler(AlertRule rule, Notification notification){super(rule, notification); } @Override public void check(ApiStatInfo apiStatInfo) {if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()) {notification.notify(NotificationEmergencyLevel.SEVERE, "..."); } } }
-
下面的代码是对 Alert 的重构,咱们再来看下,重构之后的 Alert 该如何应用呢?具体的应用代码我也写在这里了。其中,ApplicationContext 是一个单例类,负责 Alert 的创立、组装(alertRule 和 notification 的依赖注入)、初始化(增加 handlers)工作。
public class ApplicationContext { private AlertRule alertRule; private Notification notification; private Alert alert; public void initializeBeans() {alertRule = new AlertRule(/*. 省略参数.*/); // 省略一些初始化代码 notification = new Notification(/*. 省略参数.*/); // 省略一些初始化代码 alert = new Alert(); alert.addAlertHandler(new TpsAlertHandler(alertRule, notification)); alert.addAlertHandler(new ErrorAlertHandler(alertRule, notification)); } public Alert getAlert() { return alert;} // 饿汉式单例 private static final ApplicationContext instance = new ApplicationContext(); private ApplicationContext() {instance.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); } }
-
当初,咱们再来看下,基于重构之后的代码,如果再增加下面讲到的那个新性能,每秒钟接口超时申请个数超过某个最大阈值就告警,咱们又该如何改变代码呢?次要的改变有上面到处。
- 第一处改变是:在 ApiStatInfo 类中增加新的属性 timeoutCount。
- 第二处改变是:增加新的 TimeoutAlertHander 类。
- 第三处改变是:在 ApplicationContext 类的 initializeBeans() 办法中,往 alert 对象中注册新的 timeoutAlertHandler。
- 第四处改变是:在应用 Alert 类的时候,须要给 check() 函数的入参 apiStatInfo 对象设置 timeoutCount 的值。
-
改变之后的代码如下所示:
public class Alert {// 代码未改变...} public class ApiStatInfo {// 省略 constructor/getter/setter 办法 private String api; private long requestCount; private long errorCount; private long durationOfSeconds; private long timeoutCount; // 改变一:增加新字段 } public abstract class AlertHandler {// 代码未改变...} public class TpsAlertHandler extends AlertHandler {// 代码未改变...} public class ErrorAlertHandler extends AlertHandler {// 代码未改变...} // 改变二:增加新的 handler public class TimeoutAlertHandler extends AlertHandler {// 省略代码...} public class ApplicationContext { private AlertRule alertRule; private Notification notification; private Alert alert; public void initializeBeans() {alertRule = new AlertRule(/*. 省略参数.*/); // 省略一些初始化代码 notification = new Notification(/*. 省略参数.*/); // 省略一些初始化代码 alert = new Alert(); alert.addAlertHandler(new TpsAlertHandler(alertRule, notification)); alert.addAlertHandler(new ErrorAlertHandler(alertRule, notification)); // 改变三:注册 handler alert.addAlertHandler(new TimeoutAlertHandler(alertRule, notification)); } //... 省略其余未改变代码... } public class Demo {public static void main(String[] args) {ApiStatInfo apiStatInfo = new ApiStatInfo(); // ... 省略 apiStatInfo 的 set 字段代码 apiStatInfo.setTimeoutCount(289); // 改变四:设置 tiemoutCount 值 ApplicationContext.getInstance().getAlert().check(apiStatInfo); }
- 重构之后的代码更加灵便和易扩大。如果咱们要想增加新的告警逻辑,只须要基于扩大的形式创立新的 handler 类即可,不须要改变原来的 check() 函数的逻辑。而且,咱们只须要为新的 handler 类增加单元测试,老的单元测试都不会失败,也不必批改。
05. 批改代码违反准则么
- 看了下面重构之后的代码,你可能还会有疑难:在增加新的告警逻辑的时候,只管改变二(增加新的 handler 类)是基于扩大而非批改的形式来实现的,但改变一、三、四貌似不是基于扩大而是基于批改的形式来实现的,那改变一、三、四不就违反了开闭准则吗?
-
咱们先来剖析一下改变一:往 ApiStatInfo 类中增加新的属性 timeoutCount。
- 实际上,咱们不仅往 ApiStatInfo 类中增加了属性,还增加了对应的 getter/setter 办法。那这个问题就转化为:给类中增加新的属性和办法,算作“批改”还是“扩大”?
- 咱们再一块回顾一下开闭准则的定义:软件实体(模块、类、办法等)应该“对扩大凋谢、对批改敞开”。从定义中,咱们能够看出,开闭准则能够利用在不同粒度的代码中,能够是模块,也能够类,还能够是办法(及其属性)。同样一个代码改变,在粗代码粒度下,被认定为“批改”,在细代码粒度下,又能够被认定为“扩大”。比方,改变一,增加属性和办法相当于批改类,在类这个层面,这个代码改变能够被认定为“批改”;但这个代码改变并没有批改已有的属性和办法,在办法(及其属性)这一层面,它又能够被认定为“扩大”。
- 实际上,咱们也没必要纠结某个代码改变是“批改”还是“扩大”,更没必要太纠结它是否违反“开闭准则”。咱们回到这条准则的设计初衷:只有它没有毁坏原有的代码的失常运行,没有毁坏原有的单元测试,咱们就能够说,这是一个合格的代码改变。
-
咱们再来剖析一下改变三和改变四:在 ApplicationContext 类的 initializeBeans() 办法中,往 alert 对象中注册新的 timeoutAlertHandler;在应用 Alert 类的时候,须要给 check() 函数的入参 apiStatInfo 对象设置 timeoutCount 的值。
- 这两处改变都是在办法外部进行的,不论从哪个层面(模块、类、办法)来讲,都不能算是“扩大”,而是地地道道的“批改”。不过,有些批改是在劫难逃的,是能够被承受的。为什么这么说呢?
- 在重构之后的 Alert 代码中,咱们的外围逻辑集中在 Alert 类及其各个 handler 中,当咱们在增加新的告警逻辑的时候,Alert 类齐全不须要批改,而只须要扩大一个新 handler 类。如果咱们把 Alert 类及各个 handler 类合起来看作一个“模块”,那模块自身在增加新的性能的时候,齐全满足开闭准则。
- 而且,咱们要意识到,增加一个新性能,不可能任何模块、类、办法的代码都不“批改”,这个是做不到的。类须要创立、组装、并且做一些初始化操作,能力构建成可运行的的程序,这部分代码的批改是在劫难逃的。咱们要做的是尽量让批改操作更集中、更少、更下层,尽量让最外围、最简单的那局部逻辑代码满足开闭准则。
06. 如何做到开闭准则
- 在刚刚的例子中,咱们通过引入一组 handler 的形式来实现反对开闭准则。如果你没有太多简单代码的设计和开发教训,你可能会有这样的疑难:这样的代码设计思路怎么想不到呢?你是怎么想到的呢?
- 先给你个论断,之所以我能想到,靠的就是理论知识和实战经验,这些须要你缓缓学习和积攒。对于如何做到“对扩大凋谢、批改敞开”,咱们也有一些指导思想和具体的方法论,咱们一块来看一下。实际上,开闭准则讲的就是代码的扩展性问题,是判断一段代码是否易扩大的“金规范”。如果某段代码在应答将来需要变动的时候,可能做到“对扩大凋谢、对批改敞开”,那就阐明这段代码的扩展性比拟好。
-
在讲具体的方法论之前,咱们先来看一些更加偏差顶层的指导思想。为了尽量写出扩展性好的代码,咱们要时刻具备扩大意识、形象意识、封装意识。这些“潜意识”可能比任何开发技巧都重要。
- 在写代码的时候后,咱们要多花点工夫往前多思考一下,这段代码将来可能有哪些需要变更、如何设计代码构造,当时留好扩大点,以便在将来需要变更的时候,不须要改变代码整体构造、做到最小代码改变的状况下,新的代码可能很灵便地插入到扩大点上,做到“对扩大凋谢、对批改敞开”。
- 在辨认出代码可变局部和不可变局部之后,咱们要将可变局部封装起来,隔离变动,提供抽象化的不可变接口,给下层零碎应用。当具体的实现发生变化的时候,咱们只须要基于雷同的形象接口,扩大一个新的实现,替换掉老的实现即可,上游零碎的代码简直不须要批改。
-
讲了实现开闭准则的一些偏差顶层的指导思想,当初咱们再来看下,反对开闭准则的一些更加具体的方法论。
- 代码的扩展性是代码品质评判的最重要的规范之一。实际上,咱们整个专栏的大部分知识点都是围绕扩展性问题来解说的。专栏中讲到的很多设计准则、设计思维、设计模式,都是以进步代码的扩展性为最终目标的。特地是 23 种经典设计模式,大部分都是为了解决代码的扩展性问题而总结进去的,都是以开闭准则为领导准则的。
- 明天我重点讲一下,如何利用多态、依赖注入、基于接口而非实现编程,来实现“对扩大凋谢、对批改敞开”。实际上,多态、依赖注入、基于接口而非实现编程,以及后面提到的形象意识,说的都是同一种设计思路,只是从不同的角度、不同的层面来论述而已。这也体现了“很多设计准则、思维、模式都是相通的”这一思维。
-
接下来,我就通过一个例子来解释一下,如何利用这几个设计思维或准则来实现“对扩大凋谢、对批改敞开”。
- 留神,依赖注入前面会讲到,如果你对这块不理解,能够临时先疏忽这个概念,只关注多态、基于接口而非实现编程以及形象意识。
-
比方,咱们代码中通过 Kafka 来发送异步音讯。对于这样一个性能的开发,咱们要学会将其形象成一组跟具体音讯队列(Kafka)无关的异步音讯接口。所有下层零碎都依赖这组形象的接口编程,并且通过依赖注入的形式来调用。当咱们要替换新的音讯队列的时候,比方将 Kafka 替换成 RocketMQ,能够很不便地拔掉老的音讯队列实现,插入新的音讯队列实现。具体代码如下所示:
// 这一部分体现了形象意识 public interface MessageQueue {//...} public class KafkaMessageQueue implements MessageQueue {//...} public class RocketMQMessageQueue implements MessageQueue {//...} public interface MessageFormatter {//...} public class JsonMessageFormatter implements MessageFormatter {//...} public class MessageFormatter implements MessageFormatter {//...} public class Demo { private MessageQueue msgQueue; // 基于接口而非实现编程 public Demo(MessageQueue msgQueue) { // 依赖注入 this.msgQueue = msgQueue; } // msgFormatter:多态、依赖注入 public void sendNotification(Notification notification, MessageFormatter msgFormatter) {//...} }
07. 如何使用开闭准则
- 如果你开发的是一个业务导向的零碎,比方金融零碎、电商零碎、物流零碎等,要想辨认出尽可能多的扩大点,就要对业务有足够的理解,可能晓得当下以及将来可能要反对的业务需要。如果你开发的是跟业务无关的、通用的、偏底层的零碎,比方,框架、组件、类库,你须要理解“它们会被如何应用?今后你打算增加哪些性能?使用者将来会有哪些更多的性能需要?”等问题。
- 不过,有一句话说得好,“惟一不变的只有变动自身”。即使咱们对业务、对系统有足够的理解,那也不可能辨认出所有的扩大点,即使你能辨认出所有的扩大点,为这些中央都预留扩大点,这样做的老本也是不可承受的。咱们没必要为一些边远的、不肯定产生的需要去提前买单,做适度设计。
- 最正当的做法是,对于一些比拟确定的、短期内可能就会扩大,或者需要改变对代码构造影响比拟大的状况,或者实现老本不高的扩大点,在编写代码的时候之后,咱们就能够当时做些扩展性设计。但对于一些不确定将来是否要反对的需要,或者实现起来比较复杂的扩大点,咱们能够等到有需要驱动的时候,再通过重构代码的形式来反对扩大的需要。
- 而且,开闭准则也并不是收费的。有些状况下,代码的扩展性会跟可读性相冲突 。比方,咱们之前举的 Alert 告警的例子。为了更好地反对扩展性,咱们对代码进行了重构,重构之后的代码要比之前的代码简单很多,了解起来也更加有难度。 很多时候,咱们都须要在扩展性和可读性之间做衡量。在某些场景下,代码的扩展性很重要,咱们就能够适当地就义一些代码的可读性;在另一些场景下,代码的可读性更加重要,那咱们就适当地就义一些代码的可扩展性。
- 之前举的 Alert 告警的例子中,如果告警规定并不是很多、也不简单,那 check() 函数中的 if 语句就不会很多,代码逻辑也不简单,代码行数也不多,那最后的第一种代码实现思路简略易读,就是比拟正当的抉择。相同,如果告警规定很多、很简单,check() 函数的 if 语句、代码逻辑就会很多、很简单,相应的代码行数也会很多,可读性、可维护性就会变差,那重构之后的第二种代码实现思路就是更加正当的抉择了。总之,这里没有一个放之四海而皆准的参考规范,全凭理论的利用场景来决定。
08. 总结一下内容
-
1. 如何了解“对扩大凋谢、对批改敞开”?
- 增加一个新的性能,应该是通过在已有代码根底上扩大代码(新增模块、类、办法、属性等),而非批改已有代码(批改模块、类、办法、属性等)的形式来实现。对于定义,咱们有两点要留神。第一点是,开闭准则并不是说齐全杜绝批改,而是以最小的批改代码的代价来实现新性能的开发。第二点是,同样的代码改变,在粗代码粒度下,可能被认定为“批改”;在细代码粒度下,可能又被认定为“扩大”。
-
2. 如何做到“对扩大凋谢、批改敞开”?
- 咱们要时刻具备扩大意识、形象意识、封装意识。在写代码的时候,咱们要多花点工夫思考一下,这段代码将来可能有哪些需要变更,如何设计代码构造,当时留好扩大点,以便在将来需要变更的时候,在不改变代码整体构造、做到最小代码改变的状况下,将新的代码灵便地插入到扩大点上。
-
3. 学习设计准则,要多问个为什么。
- 不能把设计准则当真谛,而是要了解设计准则背地的思维。搞清楚这个,比单纯了解准则讲的是啥,更能让你灵便利用准则。
09. 实际案例剖析
- 将不同对象分类的服务办法进行形象,把业务逻辑的紧耦合关系拆开,实现代码的隔离保障了不便的扩大?
-
看看上面这段代码,改编某平凡公司产品代码,你感觉能够利用面向对象设计准则如何改良?
public class VIPCenter {void serviceVIP(T extend User user>) {if (user instanceof SlumDogVIP) { // 穷 X VIP,流动抢的那种 // do somthing } else if(user instanceof RealVIP) {// do somthing} // ... }
- 这段代码的一个问题是,业务逻辑集中在一起,当呈现新的用户类型时,比方,大数据发现了咱们是肥羊,须要去播种一下,这就须要间接去批改服务办法代码实现,这可能会意外影响不相干的某个用户类型逻辑。
-
利用开关准则,能够尝试革新为上面的代码。将不同对象分类的服务办法进行形象,把业务逻辑的紧耦合关系拆开,实现代码的隔离保障了不便的扩大。技术博客大总结
public class VIPCenter { private Map<User.TYPE, ServiceProvider> providers; void serviceVIP(T extend User user){providers.get(user.getType()).service(user); } } interface ServiceProvider{void service(T extend User user) ; } class SlumDogVIPServiceProvider implements ServiceProvider{void service(T extend User user){// do somthing} } class RealVIPServiceProvider implements ServiceProvider{void service(T extend User user) {// do something} }