设计原则 - 单一原则、开闭原则、里式替换原则

设计原则 - 单一原则、开闭原则、里式替换原则

文章是学习 《设计模式之美》- 王争 的总结

1 引言

设计原则是设计模式重要的思想和原则,需要清楚原则的定义和原则设计的初衷,能解决哪些问题,有哪些应用场景。本篇是复习5大设计原则 SOLID 的前三个 SRP(单一职责原则)、OCP(开闭原则)、LSP(里式替换原则)。

2 概述

单一职责原则 SRP

指:一个类或模块只负责完成一个职责(或者功能)

如何判断类的职责是否单一?有几条可以参考的判断原则:

  • 类的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;
  • 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;
  • 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设计为 public 方法,供多的类使用,从而提高代码的复用性;
  • 比较难给类起一个合适的名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得不够清晰;
  • 类中大量的方法都是集中操作类中的几个属性,比如,在 UserInfo 类中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来。

误区:避免类的职责在任何时候都设计的越单一。

单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性,同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,从此来实现代码的高内聚、低耦合。但是如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。

开闭原则 OCP

对扩展开放,对修改关闭。添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。关于定义,我们有两点要注意。第一点是,开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。第二点是,同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”。

如何做到 “对扩展开放,对修改关闭?

我们要时刻具备扩展意识、抽象意识、封装意识。在写代码的时候,我们要多花点时间思考一下,这段代码未来可能有哪些需求变更,如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,在不改动代码整体结构、做到最小代码改动的情况下,将新的代码灵活地插入到扩展点上。

很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的的。特别是 23 种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则的。最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。

举例API 接口监控代码 Alert.ts

/**
 * API 接口监控代码
 */

interface AlertRule {
  getMatchedRule(api: string);
}
interface Notification {
  notify(level: NotificationEmergencyLevel, msg: string);
}
enum NotificationEmergencyLevel {
  URGENCY = 0,
  SEVERE = 1,
}

class Alert {
  private rule: AlertRule;
  private notification: Notification;

  constructor(rule: AlertRule, notification: Notification) {
    this.rule = rule;
    this.notification = notification;
  }

  public check(
    api: string,
    requestCount: number,
    errorCount: number,
    durationOfSecond: number
  ): void {
    const tps = requestCount / durationOfSecond;
    if (tps > this.rule.getMatchedRule(api).getMaxTps()) {
      this.notification.notify(NotificationEmergencyLevel.URGENCY, '…');
    }
    if (errorCount > this.rule.getMatchedRule(api).getMaxErrorCount()) {
      this.notification.notify(NotificationEmergencyLevel.SEVERE, '…');
    }
  }
}

根据 OCP 改进之后的代码:

/**
 * OCP 改进后的API 接口监控代码
 */

interface AlertRule {
  getMatchedRule(api: string);
}
interface Notification {
  notify(level: NotificationEmergencyLevel, msg: string);
}

enum NotificationEmergencyLevel {
  URGENCY = 0,
  SEVERE = 1,
}

class Alert {
  private alertHandlers: AlertHandler[] = [];
  public addAlertHandler(alertHandler: AlertHandler) {
    this.alertHandlers.push(alertHandler);
  }
  public check(apiStatInfo: ApiStatInfo) {
    for (const handler of this.alertHandlers) {
      handler.check(apiStatInfo);
    }
  }
}

class ApiStatInfo {
  private _api: string;
  public get api(): string {
    return this._api;
  }
  public set api(value: string) {
    this._api = value;
  }
  private _requestCount: number;
  public get requestCount(): number {
    return this._requestCount;
  }
  public set requestCount(value: number) {
    this._requestCount = value;
  }
  private _errorCount: number;
  public get errorCount(): number {
    return this._errorCount;
  }
  public set errorCount(value: number) {
    this._errorCount = value;
  }
  private _durationOfSecond: number;
  public get durationOfSecond(): number {
    return this._durationOfSecond;
  }
  public set durationOfSecond(value: number) {
    this._durationOfSecond = value;
  }
}

abstract class AlertHandler {
  protected rule: AlertRule;
  protected notification: Notification;
  constructor(rule: AlertRule, notification: Notification) {
    this.rule = rule;
    this.notification = notification;
  }
  public abstract check(apiStatInfo: ApiStatInfo);
}

class TpsAlertHandler extends AlertHandler {
  constructor(rule: AlertRule, notification: Notification) {
    super(rule, notification);
  }
  public check(apiStatInfo: ApiStatInfo) {
    const tps = apiStatInfo.requestCount / apiStatInfo.durationOfSecond;
    if (tps > this.rule.getMatchedRule(apiStatInfo.api.getMaxTps())) {
      this.notification.notify(NotificationEmergencyLevel.URGENCY, '...');
    }
  }
}

class ErrorAlertHandler extends AlertHandler {
  constructor(rule: AlertRule, notification: Notification) {
    super(rule, notification);
  }
  public check(apiStatInfo: ApiStatInfo): void {
    if (
      apiStatInfo.errorCount >
      this.rule.getMatchedRule(apiStatInfo.api.getMaxTps())
    ) {
      this.notification.notify(NotificationEmergencyLevel.SEVERE, '...');
    }
  }
}

// 重构之后的 Alert 使用举例
class ApplicationContext {
  private alertRule: AlertRule;
  private notification: Notification;
  private alert: Alert;

  public initializeBeans() {
    this.alertRule = new AlertRule();
    this.notification = new Notification();
    this.alert = new Alert();
    alert.addAlertHandler(
      new TpsAlertHandler(this.alertRule, this.notification)
    );
    alert.addAlertHandler(
      new ErrorAlertHandler(this.alertRule, this.notification)
    );
  }

  public getAlert(): Alert {
    return this.alert;
  }

  private static instace: ApplicationContext = new ApplicationContext();
  constructor() {
    ApplicationContext.instace.initializeBeans();
  }
  public static getInstance(): ApplicationContext {
    return this.instace;
  }
}

const apiStatInfo: ApiStatInfo = new ApiStatInfo();
ApplicationContext.getInstance().getAlert().check(apiStatInfo);


里式替换原则 LSP

If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。

子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

里式替换原则是用来指导,继承关系中子类该如何设计的一个原则。理解里式替换原则,最核心的就是理解“design by contract,按照协议来设计”这几个字。父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。

理解这个原则,我们还要弄明白里式替换原则跟多态的区别。虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,用来指导继承关系中子类该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性

以下是违反里式替换原则的情况:

  • 子类违背父类声明要实现的功能(实现逻辑不一样,比如按不同字段来排序了)
  • 子类违背父类对输入、输出、异常的约定(输入参数格式不一致、返回值不同、异常类型不同等)
  • 子类违背父类注释中所罗列的任何特殊说明

本人自动发布于:https://github.com/giscafer/blog/issues/47

相关文章