乐趣区

关于android:开闭原则详细介绍

目录介绍

  • 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}
      }

开源我的项目:https://github.com/yangchong2…

开源博客:https://github.com/yangchong2…

退出移动版