• 设计原则之【开闭原则】



    全网最全最细的【设计模式】总目录,收藏起来慢慢啃,看完不懂砍我

    什么是开闭原则

    开闭原则的英文全称是 Open Closed Principle,简写为 OCP。它的英文描述是:software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。我们把它翻译成中文就是:软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。

    可扩展性,就是,添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。是衡量代码质量最重要的标准之一。

    开闭原则理解起来并不难,难的是能够灵活的应用到实际开发工作中。

    简单实例

    以下代码相信也不陌生,这就是典型的使用面向对象语言做着面向过程的事:

    package com.study;
    
    public class Ocp {
    
        public static void main(String[] args) {
            GraphicEditor graphicEditor = new GraphicEditor();
            graphicEditor.drawShape(new Rectangle());
            graphicEditor.drawShape(new Circle());
            graphicEditor.drawShape(new Triangle());
        }
    }
    
    //这是一个用于绘图的类 [使用方]
    class GraphicEditor {
        //接收 Shape 对象,然后根据 type,来绘制不同的图形
        public void drawShape(Shape s) {
            if (s.m_type == 1){
                drawRectangle(s);
            } else if (s.m_type == 2){
                drawCircle(s);
            } else if (s.m_type == 3){
                drawTriangle(s);
            }
        }
    
        //绘制矩形
        public void drawRectangle(Shape r) {
            System.out.println(" 绘制矩形 ");
        }
    
        //绘制圆形
        public void drawCircle(Shape r) {
            System.out.println(" 绘制圆形 ");
        }
    
        //绘制三角形
        public void drawTriangle(Shape r) {
            System.out.println(" 绘制三角形 ");
        }
    }
    
    //Shape 类,基类
    class Shape {
        int m_type;
    }
    
    class Rectangle extends Shape {
        Rectangle() {
            super.m_type = 1;
        }
    }
    
    class Circle extends Shape {
        Circle() {
            super.m_type = 2;
        }
    }
    
    //新增画三角形
    class Triangle extends Shape {
        Triangle() {
            super.m_type = 2;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    优化后:

    package com.study;
    
    public class Ocp {
    
        public static void main(String[] args) {
            GraphicEditor graphicEditor = new GraphicEditor();
            graphicEditor.drawShape(new Rectangle());
            graphicEditor.drawShape(new Circle());
            graphicEditor.drawShape(new Triangle());
        }
    }
    
    //这是一个用于绘图的类 [使用方]
    class GraphicEditor {
        //接收 Shape 对象,然后根据 type,来绘制不同的图形
        public void drawShape(Shape s) {
            s.draw();
        }
    }
    
    //Shape 类,基类
    abstract class Shape {
        abstract void draw();
    }
    
    class Rectangle extends Shape {
        @Override
        void draw() {
            System.out.println(" 绘制矩形 ");
        }
    }
    
    class Circle extends Shape {
        @Override
        void draw() {
            System.out.println(" 绘制圆形 ");
        }
    }
    
    //新增画三角形
    class Triangle extends Shape {
        @Override
        void draw() {
            System.out.println(" 绘制三角形 ");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    实战实例

    现在有一个登录逻辑:

    public class Login {
      private AccountService accountService;
    
      @Autoware
      public Login (AccountService accountService) {
        this.accountService = accountService;
      }
    
      public boolean loginCheck(String password, String name) {
      	// 检查账号密码
        if(!accountService.checkPwd(password, name)){
    		return false;
    	}
    	// 检查登录失败次数
    	if(accountService.getLoginErrorCount() >= 5){
    		return false;
        }
      	return true;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    上述代码相信很多小伙伴都会遇到,看上去似乎并没有什么大问题。

    但是,现在新增一个逻辑:登录时,我要校验验证码是否正确,代码可能需要做以下改动:

    public class Login {
      private AccountService accountService;
    
      @Autoware
      public Login (AccountService accountService) {
        this.accountService = accountService;
      }
    
      // 改动1:方法添加参数
      public boolean loginCheck(String password, String name, String verificationCode) {
      	// 检查账号密码
        if(!accountService.checkPwd(password, name)){
    		return false;
    	}
    	// 检查登录失败次数
    	if(accountService.getLoginErrorCount() >= 5){
    		return false;
        }
        // 改动2:检查验证码
        if(!accountService.getVerificationCode().equals(verificationCode)){
    		return false;
        }
        return true;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    这样的代码修改实际上存在挺多问题的。一方面,我们对接口进行了修改,这就意味着调用这个接口的代码都要做相应的修改。另一方面,修改了 loginCheck() 函数内容,相应的单元测试及其他逻辑都需要修改。

    接下来我们重构一下Login的代码,让其具备扩展性,重构包括两部分:

    • 第一部分是将 loginCheck() 函数的多个入参封装成 LoginCheckInfo类;
    • 第二部分是引入 handler 的概念,将 if 判断逻辑分散在各个 handler 中。
    public class LoginCheckInfo {//省略constructor/getter/setter方法
      private String name;
      private String password;
      private String verificationCode;
    }
    
    public class Login {
      private List<LoginCheckHandler> checkHandlers = new ArrayList<>();
      
      public void addLoginCheckHandler(CheckHandler checkHandler) {
        this.checkHandlers.add(checkHandler);
      }
    
      public void check(LoginCheckInfo loginCheckInfo) {
        for (LoginCheckHandlerhandler : checkHandlers) {
          boolean result = handler.check(loginCheckInfo);
          // 处理逻辑
        }
      }
    }
    
    public abstract class CheckHandler {
      private AccountService accountService;
    
      @Autoware
      public CheckHandler (AccountService accountService) {
        this.accountService = accountService;
      }
      public abstract void check(LoginCheckInfo loginCheckInfo);
    }
    
    public class PwdCheckHandler extends CheckHandler {
      public PwdCheckHandler(AccountService accountService) {
        super(accountService);
      }
    
      @Override
      public boolean check(LoginCheckInfo loginCheckInfo) {
        return accountService.checkPwd(loginCheckInfo.getPassword(), loginCheckInfo.getName());
      }
    }
    
    public class LoginCountCheckHandler extends CheckHandler {
      public LoginCountCheckHandler(AccountService accountService) {
        super(accountService);
      }
    
      @Override
      public boolean check(LoginCheckInfo loginCheckInfo) {
        return accountService.getLoginErrorCount() >= 5;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    要想使用我们重构后的登录逻辑:

    @Bean
    public Login login(){
    	Login login = new Login();
    	login.addLoginCheckHandler(new PwdCheckHandler());
    	login.addLoginCheckHandler(new LoginCountCheckHandler());
    }
    
    // 只需要在service层注入login,然后调用login.check();即可
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    此时,我们如果想要扩展验证码功能:

    public class VerificationCodeCheckHandler extends CheckHandler {
      public VerificationCodeCheckHandler(AccountService accountService) {
        super(accountService);
      }
    
      @Override
      public boolean check(LoginCheckInfo loginCheckInfo) {
        return accountService.getVerificationCode().equals(loginCheckInfo.getVerificationCode());
      }
    }
    
    // 然后在定义Login的bean中将这个处理器添加进去即可。
    login.addLoginCheckHandler(new VerificationCodeCheckHandler());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    重构之后的代码更加灵活和易扩展。如果我们要想添加新的告警逻辑,只需要基于扩展的方式创建新的 handler 类即可,不需要改动原来的 check() 函数的逻辑。而且,我们只需要为新的 handler 类添加单元测试,老的单元测试都不会失败,也不用修改。

    如何理解“对修改关闭”?修改代码就一定违背开闭原则吗

    对修改关闭的前提是:对扩展开放。

    ,添加一个新功能,不可能任何模块、类、方法的代码都不“修改”,这个是做不到的。类需要创建、组装、并且做一些初始化操作,才能构建成可运行的的程序,这部分代码的修改是在所难免的。我们要做的是尽量让修改操作更集中、更少、更上层,尽量让最核心、最复杂的那部分逻辑代码满足开闭原则。

    也就是说,对拓展开放是为了应对变化(需求),对修改关闭是为了保证已有代码的稳定性;最终结果是为了让系统更有弹性!

    添加一个新的功能,如果能够保证老的核心代码不会被修改,那么这就是符合开闭原则的。

    熟练使用各种设计模式、并且应用到实际工作中,是我们开发者一生都要去学习的。

    但是,熟悉了“开闭原则”,这并不意味着你需要随时随地都要考虑扩展。需求永远是在不断变化的,即便我们对业务、对系统有足够的了解,那也不可能识别出所有的扩展点,即便你能识别出所有的扩展点,为这些地方都预留扩展点,这样做的成本也是不可接受的。我们没必要为一些遥远的、不一定发生的需求去提前买单,做过度设计。

    最合理的做法是,对于一些比较确定的、短期内可能就会扩展,或者需求改动对代码结构影响比较大的情况,或者实现成本不高的扩展点,在编写代码的时候之后,我们就可以事先做些扩展性设计。但对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的扩展点,我们可以等到有需求驱动的时候,再通过重构代码的方式来支持扩展的需求。

    所以,具体场景还需要具体分析。

    参考资料

    王争老师的《设计模式之美》
    https://blog.csdn.net/m0_54485604/article/details/113502478

  • 相关阅读:
    Collections unmodifiableCollection,emptyList,singletonList 介绍以及总结
    构建现代应用:Java中的热门架构概览
    Methoxy-PEG-PCL,Methoxy-PEG-Poly(ε-caprolactone)可以作为制备纳米颗粒的重要原料
    endpoint=DefaultEndpoint{ serviceUrl=‘http://127.0.0.1:10086/eureka/
    【论文解读】FFHQ-UV:用于3D面部重建的归一化面部UV纹理数据集
    sql表关联查询,表查询出数据插入到另一张,使用正则查询,查询结果集转换为JSON数据
    【c++提高1】最近共先祖LCA优化求法
    基于协同过滤推荐算法的在线教育平台(Vue+Node.js+SSM)
    纯干货:准备输入文件 | VASP零基础保姆级指南
    ElasticSearch7.3学习(十四)----生产环境实时重建索引
  • 原文地址:https://blog.csdn.net/A_art_xiang/article/details/127610912