设计模式的设计原则
2021 algorithm设计模式的简介与一些经典的设计原则,其中包括 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)
- 一个类或者模块只负责完成一个职责
- 开放封闭原则(OCP)
- 对扩展开放,对更改封闭
- 里氏替换原则(LSP)
- 子类必须能够替换它们的基类
- 接口隔离原则(ISP)
- 不应该强迫客户程序依赖它们不用的方法
- 依赖倒置原则(DIP)
- 高层模块(稳定)不应该依赖于底层模块(变化),二者都应该依赖于抽象(稳定)
- 抽象(稳定)不应该依赖于实现细节(变化),实现细节应该依赖于抽象(稳定)
- 组合重用原则:优先使用对象组合,而不是类继承
- 类继承通常为“白箱复用”,对象组合通常为“黑箱复用”
- 继承在某种程度上破坏了封装性,子类父类耦合度高
- 而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低
- 封装变化点
- 使用封装来创建对象之间的分界层,让设计者可以分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合
- 针对接口编程,而不是针对实现编程
- 不将变量类型声明为某个特定的具体类,而是声明为某个接口
- 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口
- 减少系统中各部分的依赖关系,从而实现“”高内聚,松耦合“的类型设计方案
单一职责原则
单一职责原则(SRP),Single Responsibility Principle,即一个类或者模块只负责完成一个职责。
我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构。
如何判断类的职责是否足够单一?
- 类中的代码行数,函数或者属性过多;
- 类依赖的其他类过多,或者依赖类的其他类过多;
- 私有方法过多;
- 比较难给类起一个合适的名字;
- 类中的大量的方法都是集中类中的某几个属性;
开闭原则
开闭原则(OCP),Open Close Principle,即对扩展开发,对修改关闭。
详细表述就是,添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块,类,方法等),而非修改已有代码(修改模块,类,方法等)。
里氏替换原则
里氏替换原则(LSP),Liskov Substitution Principle。子类对象能够替换程序中的父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。
多态与里氏替换原则的区别?
多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。
违背里氏替换原则
- 子类违背父类声明要实现的功能
- 子类违背父类对输入、输出、异常的约定
- 子类违背父类注释中所罗列的任何特殊说明
可以通过拿父类的单元测试来验证子类的代码,如果某些单元测试运行失败,就有可能说明,子类的设计实现没有完全遵守父类的约定,子类可能违背了里氏替换原则。
接口隔离原则
接口隔离原则(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.
。翻译成中文就是:高层模块不要依赖低层模块,高层模块和低层模块应该通过抽象来相互依赖。抽象不要依赖具体实现细节,具体实现细节依赖抽象
。
在调用链上,调用者属于高层,被调用者属于低层。但在平时的业务代码开发中,高层模块依赖底层模块是没有任何问题的,这条原则主要还是用来指导框架层面的设计。
参考链接
- 李建忠:C++设计模式
- github: clean-code-javascript#solid
- 极客时间:设计模式之美