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

Posted iblade

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式之美——单一职责原则和开闭原则相关的知识,希望对你有一定的参考价值。

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

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

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

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

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


以上是关于设计模式之美——单一职责原则和开闭原则的主要内容,如果未能解决你的问题,请参考以下文章

设计模式之美--单一职责原则

设计模式原则

面向对象设计原则

「设计模式」六大原则之二:开闭职责小结

「设计模式」六大原则之二:开闭职责小结

「设计模式」六大原则之二:开闭职责小结