开闭原则|设计原则


开闭原则|设计原则

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第4天,点击查看活动详情

设计原则系列文章

单一职责原则|设计原则 - 掘金 (juejin.cn)

开闭原则|设计原则 - 掘金 (juejin.cn)

里氏替换原则|设计原则 - 掘金 (juejin.cn)

接口隔离原则|设计原则 - 掘金 (juejin.cn)

依赖反转原则|设计原则 - 掘金 (juejin.cn)

迪米特原则|设计原则 - 掘金 (juejin.cn)

六大原则之外的设计原则|设计原则 - 掘金 (juejin.cn)

前置知识

  • 有项目编写经历
  • 做过代码的功能拓展
  • 听说过设计模式

前言

本文带大家继续学习 开闭原则

由于在软件体系中,唯一不变的,就是软件一直在变。这就意味着我们的软件、系统,需要把可拓展性做好,才不会让后期的功能拓展变得困难。事实上,大多数的设计模式都是在提高代码的可拓展性。而 开闭原则 就是提高代码可拓展性的核心原则。

开闭原则 定义

面向对象编程领域中,开闭原则规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”,这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。该特性在产品化的环境中是特别有价值的,在这种环境中,改变源代码需要代码审查单元测试以及诸如此类的用以确保产品使用质量的过程。遵循这种原则的代码在扩展时并不发生改变,因此无需上述的过程。

上述的定义来自 百度百科 。其精髓部分是,对拓展开放,对修改关闭。意思是说,我们不能去修改原有功能的代码,但是可以利用面向对象特性,在不修改原有代码的基础上进行功能的拓展。

什么样的代码不符合开闭原则

开闭原则的定义很清晰,但是想理解和应用起来却感觉很抽象。下面我们结合一段代码来看看,什么样的代码是不符合开闭原则的

public class Work{
    private A a;
    private B b;
    
    public Work(A a , B b){
        this.a = a;
        this.b = b;
    }
    
    public void process(String processA,String processB){
        ...
        actionA(a,processA);
        actionB(b,processB);
    }
    
}

上述的代码中,在 process() 部分,实现了 actionAactionB 两个功能。

而当我们想要拓展一个功能 actionB 的时候,会做如下修改

public class Work{
    private A a;
    private B b;
    private C c;//修改
    
    public Work(A a , B b , C c){
        this.a = a;
        this.b = b;
        this.c = c;//修改
    }
    
    public void process(String processA,String processB,String processC){//修改
        ...
        a.actionA(processA);
        b.actionB(processB);
        c.actionC(processC);//修改
    }
    
}

可见,上述对功能的拓展中,其修改了核心函数的参数,所以这类修改不符合开闭原则。

因为直接修改了核心代码,会导致原有对该函数的调用会出错,需要修改入参的值。少量代码需要修改还好说,但是当代码里逐渐变大,每次添加新功能,都会需要修改大量的代码。而且,原有的单例测试代码也将不能正确运行。

如何让代码符合开闭原则

需要将代码修改得符合开闭原则,就需要把开闭原则的对 修改关闭 落实。

如下方的代码,我们需要将代码中会变化的参数 提取出来成为一个单独的类 ,这样子便于后面对参数的添加和删除。

public class WorkBean{
    private A a;
    private B b;
    
    public void setA(A a){
        this.a = a;
    }
    
    public A getA(){
        return a;
    }
    
    ... ...
} 

public class Processbean{
    private String processA;
    private String processB;
    ...
}

然后第二部分,我们需要 将 Work 类抽象出一个父类来,然后每个功能都是继承该父类来实现。

public abstract class BaseWork{
    protected WorkBean workBean;
    
    public Work(WorkBean workBean){
        this.workBean = workBean;
    }
    
    public abstract void process(Processbean processbean);
    
}
//对应的子类继承父类
puvlic class AWork extends BaseWork{
    
    public AWork(WorkBean workBean){
        super(work);
    }
    
    @Override
    public process(Processbean processbean){
        workBean.getA().actionA(processbean.getProcessA);
    }
}

puvlic class BWork extends BaseWork{
    
    public BWork(WorkBean workBean){
        super(work);
    }
    
    @Override
    public process(Processbean processbean){
        workBean.getB().actionB(processbean.getProcessB);
    }
}

最后再使用一个 ActionList 类来执行上述的子类,利用这个类可以很方便的新增和执行对应的 Work

public class ActionList{
    private List<BaseWork> list = new ArrayList<>();
    
    public void add(BaseWork work){
        list.add(work);
    }
    
    puclic void actionWork((Processbean processbean){
        for(BaseWork work : list){
            work.process(processbean);
        }
    }
}

我们最后将代码进行整合应用,贪简单这样子写

public class HappyNationalDay{
    public static void main(String[] args) {
        ActionList list = new ActionList();
        WorkBean bean = new WorkBean();
        ...//bean的设置
        list.add(new AWork(bean));
        list.add(new BWork(bean));
        Processbean processbean = new Processbean();
        ...//processbean的设置
        list.actionWork(processbean);
    }
}

如上,一个遵从开闭原则的代码就写好了

当我们要添加新功能时候,我们只需更改以下几点

  1. 添加 bean 类 属性
public class WorkBean{
    private A a;
    private B b;
    private C c;//修改
    
    ... ...
} 

public class Processbean{
    private String processA;
    private String processB;
    private String processC;//修改
    ...
}
  1. 继承新的 work 子类
puvlic class CWork extends BaseWork{
    
    public CWork(WorkBean workBean){
        super(work);
    }
    
    @Override
    public process(Processbean processbean){
        workBean.getC().actionC(processbean.getProcessC);
    }
}
  1. 代码执行处直接添加
public class HappyNationalDay{
    public static void main(String[] args) {
        ActionList list = new ActionList();
        WorkBean bean = new WorkBean();
        ...
        list.add(new AWork(bean));
        list.add(new BWork(bean));
        list.add(new CWork(bean));//修改
        Processbean processbean = new Processbean();
        ...
        list.actionWork(processbean);
    }
}

辩证认清开闭原则

如上文,我们学习了如何写出具有开闭原则的代码。事实上,我们利用接口,多态等特性,写出 对拓展开发,对修改关闭 的代码都是具有开闭原则的代码。

但是我们从上文的例子也可以看出,我们所说的 对修改关闭 指的并非不允许修改,而是不修改核心代码,修改不会造成其他位置出现问题。这些代码就是优质的 可拓展 的代码。

并且,我们可以看到,为了写出这类易于拓展,符合开闭原则的代码,我们是降低了其可读性,且花费了些思考和时间成本才写出来的。所以,如果确定代码无需拓展,便不应该使用开闭原则,免得画蛇添足。

实事求是,适合需求的代码,就是优质好代码。

参考

开闭原则_百度百科 (baidu.com)

16 | 理论二:如何做到“对扩展开放、修改关闭”?扩展和修改各指什么? (geekbang.org)

附代码(不发布)

开闭原则:基于拓展打开,基于修改关闭

//原代码
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;
      //功能1
    if (tps > rule.getMatchedRule(api).getMaxTps()) {
      notification.notify(NotificationEmergencyLevel.URGENCY, "...");
    }
      //功能2
    if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {
      notification.notify(NotificationEmergencyLevel.SEVERE, "...");
    }
  }
}
//做出拓展
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, "...");
    }
  }
}

上述不符合开闭原则,因为直接修改了核心代码,会导致原有的调用函数出错,需要修改。单例测试代码也不能正确运行

如何写出符合 开闭原则 的代码?


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

//第一部分是将 check() 函数的多个入参封装成 ApiStatInfo 类
public class ApiStatInfo {//省略constructor/getter/setter方法
  private String api;
  private long requestCount;
  private long errorCount;
  private long durationOfSeconds;
}

//第二部分是引入 handler 的概念,将 if 判断逻辑分散在各个 handler 中
//提取抽象父类
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);
}

//子类单一职责的负责对于的判断逻辑 1
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, "...");
    }
  }
}

//子类单一职责的负责对于的判断逻辑 2
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, "...");
    }
  }
}
//调用代码
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() {
    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);
  }
}

那么我们修改时候就很方便了


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

文章作者: DYJ
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 DYJ !
评论
  目录