设计模式的简介与一些经典的设计原则,其中包括 SOLID 等。

如何解决复杂性

分解

人们面对复杂性有一个常用的做法:即分而治之。将复杂问题分解为多个简单的问题。

抽象

更高层次来讲,人们处理复杂性有一个通用的技术,即抽象。由于不能掌握全部的复杂对象,我们选择忽视它的非本质细节,而去处理泛化和理想化了的对象模型。

软件设计的目的:复用,用抽象思维替代分解思维。

Line {
  point x;
  point y;
}

Rect {
  point a;
  point b;
  point c;
  point d;
}

function Shape(S) {
  // 画线条
  if (S === Line) {...}
  // 画矩形
  if (S === Rect) {...}
}

用分治的思想实现一个画形状的方法,我们在 Shape 里面对需要实现的形状一一进行实现,当我们需要新增一个圆形,我们不仅需要实现圆形,还要修改 Shape 方法。

Circle {
  point x;
  radius r;
}

function Shape(S) {
  // 画线条
  if (S === Line) {...}
  // 画矩形
  if (S === Rect) {...}
  // 画圆形
  if (S === Circle) {...}
}

如果我们通过抽象实现的方式,则在进行扩展变动的时候则不用修改 Shape 方法.

Line {
  point x;
  point y;
  draw() {
    // 画线条
  }
}

Rect {
  point a;
  point b;
  point c;
  point d;
  draw() {
    // 画矩形
  }
}

function Shape(S) {
  // 内部实现
  S.draw();
}

// 拓展变动则不需要修改 Shape 形状
Circle {
  point x;
  radius r;
  draw() {
    // 画圆形的实现
  }
}

设计原则

SOLID 原则,即单一责任原则,开放封闭原则,里氏替换原则,接口隔离原则和依赖倒置原则组成。

  • 单一责任原则(SRP)
    1. 一个类或者模块只负责完成一个职责
  • 开放封闭原则(OCP)
    1. 对扩展开放,对更改封闭
  • 里氏替换原则(LSP)
    1. 子类必须能够替换它们的基类
  • 接口隔离原则(ISP)
    1. 不应该强迫客户程序依赖它们不用的方法
  • 依赖倒置原则(DIP)
    1. 高层模块(稳定)不应该依赖于底层模块(变化),二者都应该依赖于抽象(稳定)
    2. 抽象(稳定)不应该依赖于实现细节(变化),实现细节应该依赖于抽象(稳定)
  • 组合重用原则:优先使用对象组合,而不是类继承
    1. 类继承通常为“白箱复用”,对象组合通常为“黑箱复用”
    2. 继承在某种程度上破坏了封装性,子类父类耦合度高
    3. 而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低
  • 封装变化点
    1. 使用封装来创建对象之间的分界层,让设计者可以分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合
  • 针对接口编程,而不是针对实现编程
    1. 不将变量类型声明为某个特定的具体类,而是声明为某个接口
    2. 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口
    3. 减少系统中各部分的依赖关系,从而实现“”高内聚,松耦合“的类型设计方案

单一职责原则

单一职责原则(SRP),Single Responsibility Principle,即一个类或者模块只负责完成一个职责。

我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构。

如何判断类的职责是否足够单一?

  • 类中的代码行数,函数或者属性过多;
  • 类依赖的其他类过多,或者依赖类的其他类过多;
  • 私有方法过多;
  • 比较难给类起一个合适的名字;
  • 类中的大量的方法都是集中类中的某几个属性;

开闭原则

开闭原则(OCP),Open Close Principle,即对扩展开发,对修改关闭。

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

里氏替换原则

里氏替换原则(LSP),Liskov Substitution Principle。子类对象能够替换程序中的父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。

多态与里氏替换原则的区别?

多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。

违背里氏替换原则

  1. 子类违背父类声明要实现的功能
  2. 子类违背父类对输入、输出、异常的约定
  3. 子类违背父类注释中所罗列的任何特殊说明

可以通过拿父类的单元测试来验证子类的代码,如果某些单元测试运行失败,就有可能说明,子类的设计实现没有完全遵守父类的约定,子类可能违背了里氏替换原则。

接口隔离原则

接口隔离原则(ISP),Interface Segregation Principle,客户端不应该被强迫依赖它不需要的接口。

当接口为 API 接口集合时,如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口。

如果接口理解为单个接口或函数,那么接口隔离原则可以理解为:函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现。

接口隔离原则与单一职责原则的区别

单一职责原则针对的是模块、类、接口的设计。接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考角度也是不同的。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。

依赖倒置原则

控制反转

控制反转(IOC),英文翻译是:Inversion Of Control。控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。

依赖注入

依赖注入(DI),英文翻译是:Dependency Injection,一句话概括就是:不通过 new() 的方法在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。

依赖反转原则(DIP),Dependency Inversion Principle,也叫依赖倒置原则。英文描述是 High-level modules shouldn't depend on low-level modules. Both modules should depend on abstractions. In addition, abstraction shouldn't depended on details. Details depned on abstractions.。翻译成中文就是:高层模块不要依赖低层模块,高层模块和低层模块应该通过抽象来相互依赖。抽象不要依赖具体实现细节,具体实现细节依赖抽象

在调用链上,调用者属于高层,被调用者属于低层。但在平时的业务代码开发中,高层模块依赖底层模块是没有任何问题的,这条原则主要还是用来指导框架层面的设计。

参考链接