设计模式之美——单一职责原则和开闭原则

news2024/11/20 0:26:44

“看懂”和“会用”是两回事,而“用好”更是难上加难。

SOLID 原则:
SRP单一职责原则(the single responsibility principle )
OCP开闭原则(the open closed principle)
LSP里氏替换原则(the liskov substitution principle)
ISP接口独立原则(the interface segregation principle)
DIP依赖倒置原则(the dependency inversion principle)

单一职责:

A class or module should have a single responsibility.
一个类或者模块只负责完成一个职责(或者功能)。

这个原则描述的对象包含两个,一个是类(class),一个是模块(module)。关于这两个概念,有两种理解方式。一种理解是:把模块看作比类更加抽象的概念,类也可以看作模块。另一种理解是:把模块看作比类更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块。

不管哪种理解方式,单一职责原则在应用到这两个描述对象的时候,道理都是相通的。下面以类为例:

不要设计大而全的类,要设计粒度小、功能单一的类。换个角度来讲就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。

不同的应用场景、不同阶段的需求背景下,对同一个类的职责是否单一的判定,可能都是不一样的。在某种应用场景或者当下的需求背景下,一个类的设计可能已经满足单一职责原则了,但如果换个应用场景或着在未来的某个需求背景下,可能就不满足了,需要继续拆分成粒度更细的类。

评价一个类的职责是否足够单一,我们并没有一个非常明确的、可以量化的标准,可以说,这是件非常主观、仁者见仁智者见智的事情。实际上,在真正的软件开发中,我们也没必要过于未雨绸缪,过度设计。所以,我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构
在这里插入图片描述

为了满足单一职责原则,是不是把类拆得越细就越好呢?答案是否定的。

不管是应用设计原则还是设计模式,最终的目的还是提高代码的可读性、可扩展性、复用性、可维护性等。我们在考虑应用某一个设计原则是否合理的时候,也可以以此作为最终的考量标准。
在这里插入图片描述


1,内聚和耦合其实是对一个意思(即合在一块)从相反方向的两种阐述。

2,内聚是从功能相关来谈,主张高内聚。把功能高度相关的内容不必要地分离开,就降低了内聚性,成了低内聚。

3,耦合是从功能无关来谈,主张低耦合。把功能明显无关的内容随意地结合起来,就增加了耦合性,成了高耦合。

开闭原则

开闭原则是 SOLID 中最难理解、最难掌握,同时也是最有用的一条原则。
在这里插入图片描述

software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。
软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。

添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。

举个例子,这是一段 API 接口监控告警的代码。

其中,AlertRule 存储告警规则,可以自由设置。Notification 是告警通知类,支持邮件、短信、微信、手机等多种通知渠道。NotificationEmergencyLevel 表示通知的紧急程度,包括 SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要),不同的紧急程度对应不同的发送渠道。


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() 函数的入参,添加一个新的统计数据 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() 函数,相应的单元测试都需要修改。

上面的代码改动是基于“修改”的方式来实现新功能的。如果我们遵循开闭原则,也就是“对扩展开放、对修改关闭”。那如何通过“扩展”的方式,来实现同样的功能呢?

我们先重构一下之前的 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() {
    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);
  }
}

现在,我们再来看下,基于重构之后的代码,如果再添加上面讲到的那个新功能,每秒钟接口超时请求个数超过某个最大阈值就告警,我们又该如何改动代码呢?主要的改动有下面四处。

  1. 第一处改动是:在 ApiStatInfo 类中添加新的属性 timeoutCount。
  2. 第二处改动是:添加新的 TimeoutAlertHander 类。
  3. 第三处改动是:在 ApplicationContext 类的 initializeBeans() 方法中,往 alert 对象中注册新的 timeoutAlertHandler。
  4. 第四处改动是:在使用 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 类添加单元测试,老的单元测试都不会失败,也不用修改。在这里插入图片描述
为了尽量写出扩展性好的代码,我们要时刻具备扩展意识、抽象意识、封装意识。这些“潜意识”可能比任何开发技巧都重要。

在写代码的时候后,我们要多花点时间往前多思考一下,这段代码未来可能有哪些需求变更、如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,不需要改动代码整体结构、做到最小代码改动的情况下,新的代码能够很灵活地插入到扩展点上,做到“对扩展开放、对修改关闭”。

还有,在识别出代码可变部分和不可变部分之后,我们**要将可变部分封装起来,隔离变化,提供抽象化的不可变接口,给上层系统使用。**当具体的实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即可,上游系统的代码几乎不需要修改。

23 种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则的。

在众多的设计原则、思想、模式中,最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态等)。

如何利用多态、依赖注入、基于接口而非实现编程,来实现“对扩展开放、对修改关闭”。

实际上,多态、依赖注入、基于接口而非实现编程,以及前面提到的抽象意识,说的都是同一种设计思路,只是从不同的角度、不同的层面来阐述而已。这也体现了“很多设计原则、思想、模式都是相通的”这一思想。

在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/29301.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【HMS Core】School Diary应用集成多个HMS Core服务,更好的体验华为生态系统

一、介绍 总览 通过建立本次的School Diary应用&#xff0c;您可以更好地体验华为生态系统的组成部分&#xff0c;包括认证服务、云存储和云数据库等Serverless服务。此外您还可以了解到如何使用账号服务集成应用登录功能。老师和学生两种角色的匹配过程是本应用的一大特色。…

SpringCloud微服务(九)——Ribbon负载均衡

Ribbon负载均衡服务调用 SpringCloud 已停更 github官网&#xff1a;https://github.com/netflix/ribbon Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具&#xff0c;它基于Netflix Ribbon实现。通过Spring Cloud的封装&#xff0c;可以让我们轻松地将面向服务…

高斯分布的乘积与卷积

高斯分布是一个很重要的连续分布形式&#xff0c;频繁出现各种应用场景里也可以导出很多分布&#xff0c;如在典型的线性回归中对误差 的建模就是用的标准正态分布&#xff0c;统计学的学生分布就是从正态分布中导出。随着贝叶斯统计学的广泛应用&#xff0c;相乘的高斯分布&am…

【仿真建模】第四课:AnyLogic入门基础课程 - 轨道交通仿真入门讲解

文章目录一、轨道库的概念和特点二、轨道交通仿真三、更换车头和车身样式一、轨道库的概念和特点 二、轨道交通仿真 新建模型 搭建轨道 定义轨道上的起点和终点 拖拽出一个trainSource&#xff0c;设置其车厢数量为4&#xff08;默认为11&#xff0c;车厢太多会超出轨道&…

nginx(六十)proxy模块(一)proxy_pass指令

一 proxy模块处理请求的流程 ① 流程图 说明&#xff1a; nginx从client接收的是http协议,转发给上游的也是http协议备注&#xff1a; 后续根据处理请求的流程,来讲解相关指令 二 proxy_pass ① 基本解读 说明&#xff1a; proxy_pass是一个动作指令 ② proxy_pass的…

【题型总结】找到第n个自定义数 | 丑数系列 + 神奇数字

文章目录找到第n个自定义数丑数【LC263】丑数Ⅱ【LC264】优先队列多指针超级丑数【LC313】优先队列【超时】多指针第N个神奇数字【LC878】找规律二分查找数学丑数Ⅲ【LC1201】二分查找数学总结找到第n个自定义数 因为神奇数字做了相关的题目&#xff0c;个人建议做题顺序&…

涨知识!Python 的异常信息还能这样展现

【导语】&#xff1a;在日常开发的过程中&#xff0c;当代码报错时&#xff0c;我们通常要不断打印、阅读traceback提示信息&#xff0c;来调试代码&#xff0c;这篇文章介绍了如何实现一个Exception Hooks&#xff0c;使得traceback模块的提示信息更加精确&#xff1b;同时还介…

java项目-第159期ssm超市管理系统_ssm毕业设计_计算机毕业设计

java项目-第159期ssm超市管理系统-ssm毕业设计_计算机毕业设计 【源码请到资源专栏下载】 今天分享的项目是《ssm超市管理系统》 该项目分为2个角色&#xff0c;管理员、员工。 员工登录后台主要负责商品的出入库&#xff0c;以及个人事项办理&#xff0c;比如&#xff1a; 上…

技术分享| 快对讲视频调度功能说明

随着计算机技术的日趋成熟&#xff0c;融合调度方案已经在行业信息化中普及&#xff0c;由于近几年实时音视频能力的提升&#xff0c;融合调度中的视频调度方案也在往实时性、高清方向靠拢。快对讲视频调度正是结合了视频监控&#xff0c;以及实时通信的特性&#xff0c;在市面…

传奇GOM引擎单机架设图文教程

T:准备下载好服务端&#xff08;版本&#xff09;gom引擎架设 选择GOM引擎版本 注;版本可以去论坛有免费&#xff0c;电脑还需要下载安装好客户端。 1.首先下载好版本后会有2个压缩包&#xff0c;一个是版本&#xff0c;一个是补丁&#xff0c; 简单来说架设分三部&#xff1…

Scrum 四个会议的正确召开方式

敏捷开发有一些重要的实践方法&#xff0c;可以帮助团队更快地适应敏捷开发框架。这些方法不能简单照搬执行&#xff0c;比如&#xff0c;只在瀑布开发模式下中加入 Scrum 的四个会议&#xff0c;这无法让瀑布团队转成敏捷团队。敏捷转型需要深入理解概念和思维&#xff0c;团队…

【Linux】第十四章 多线程(生产者消费者模型+POSIX信号量)

&#x1f3c6;个人主页&#xff1a;企鹅不叫的博客 ​ &#x1f308;专栏 C语言初阶和进阶C项目Leetcode刷题初阶数据结构与算法C初阶和进阶《深入理解计算机操作系统》《高质量C/C编程》Linux ⭐️ 博主码云gitee链接&#xff1a;代码仓库地址 ⚡若有帮助可以【关注点赞收藏】…

JavaSE中split方法详细原理讲解分析

文章目录方法1:split(String [regex](https://so.csdn.net/so/search?qregex&spm1001.2101.3001.7020))入门案例1入门案例2入门案例3入门案例4分隔符三个特殊位置特殊案例1特殊案例2方法2:split(String regex,int limit)limit用法:进阶案例:limit>0限制分割次数limit&l…

图神经网络关系抽取论文阅读笔记(二)

1 用于关系抽取的生成式参数图神经网络 论文&#xff1a;Graph Neural Networks with Generated Parameters for Relation Extraction&#xff08;2019 ACL&#xff09; 1.1 创新点 传统的图神经网络在进行NLP任务时&#xff0c;图的拓扑结构都是预先定义好的&#xff0c;之后再…

已解决OSError: [Errno 22] Invalid argument

已解决OSError: [Errno 22] Invalid argument 文章目录报错代码报错翻译报错原因解决方法帮忙解决报错代码 粉丝群里面的一个粉丝用Python读取文件的时候&#xff0c;发生了报错&#xff08;跑来找我求助&#xff0c;然后顺利帮助他解决了&#xff0c;顺便记录一下希望可以帮助…

PTA题目 前世档案

网络世界中时常会遇到这类滑稽的算命小程序&#xff0c;实现原理很简单&#xff0c;随便设计几个问题&#xff0c;根据玩家对每个问题的回答选择一条判断树中的路径&#xff08;如下图所示&#xff09;&#xff0c;结论就是路径终点对应的那个结点。 现在我们把结论从左到右顺序…

UnityVR一体机报错:GL_OUT_OF_MEMORY,[EGL] Unable to acquire context

一、 报错信息一览 &#xff08;1&#xff09; [EGL] Unable to acquire context: E Unity : [EGL] Unable to acquire context: EGL_BAD_SURFACE: An EGLSurface argument does not name a valid surface (window, pixel buffer or pixmap) configured for GL rendering. 解决…

AQS源码解析 4.Condition条件队列入门_手写BrokingQueue

AQS源码解析—Condition条件队列入门_手写BrokingQueue Condition 条件队列介绍 AQS 中还有另一个非常重要的内部类 ConditionObject&#xff0c;它实现了 Condition 接口&#xff0c;主要用于实现条件锁。ConditionObject 中也维护了一个队列&#xff0c;这个队列主要用于等…

动态规划算法学习三:0-1背包问题

文章目录前言一、问题描述二、DP解决步骤1、最优子结构性质2、状态表示和递推方程3、算法设计与分析4、计算最优值5、算法实现6、缺点与思考前言 一、问题描述 二、DP解决步骤 1、最优子结构性质 2、状态表示和递推方程 子问题可由两个参数确定&#xff1a;待考虑装包的物品…

【C/调试实用技巧】—作为程序员应如何面对并尝试解决Bug?

小菜坤日常上传gitee代码&#xff1a;https://gitee.com/qi-dunyan 关于C语言的所有由浅入深的知识&#xff0c;都存放在专栏【c】 ❤❤❤ 个人简介&#xff1a;双一流非科班的一名小白&#xff0c;期待与各位大佬一起努力&#xff01; 推荐网站&#xff1a;cplusplus.com 目录…