天天看点

设计原则之【开闭原则】

文章目录

  • ​​什么是开闭原则​​
  • ​​简单实例​​
  • ​​实战实例​​
  • ​​如何理解“对修改关闭”?修改代码就一定违背开闭原则吗​​
  • ​​参考资料​​

什么是开闭原则

开闭原则的英文全称是 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;
    }
}      

优化后:

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(" 绘制三角形 ");
    }
}      

实战实例

现在有一个登录逻辑:

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

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

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

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

这样的代码修改实际上存在挺多问题的。一方面,我们对接口进行了修改,这就意味着调用这个接口的代码都要做相应的修改。另一方面,修改了 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;
  }
}      

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

@Bean
public Login login(){
  Login login = new Login();
  login.addLoginCheckHandler(new PwdCheckHandler());
  login.addLoginCheckHandler(new LoginCountCheckHandler());
}

// 只需要在service层注入login,然后调用login.check();即可      

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

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());      

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

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

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

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

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

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

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

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

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