面向对象设计原则之S.O.L.I.D

作为一名优秀的工程师,最重要的不是招式, 而是内功, 像武侠小说里的高手, 一旦有了几十年的内功, 学什么武功都能快人一步

那编程里如何提升内功, 主要就是体现在编程思想上了,

逐层提升你编程思想

比如从一开始的最传统的面向对象 到 面向切面

面向对象 -> 工厂模式 -> 面向切面

从 “oop 的静态抽象” 演变为 “aop的动态抽象”

提升思想

SOLID原则的基本概念

让我们来看下 SOLID 的概念

程序设计领域, SOLID (单一一功能、开闭原则、⾥里里⽒氏替换、接⼝口隔离以及依赖反转)是由罗伯特·C·⻢马丁在21世纪早期 引⼊入的记忆术⾸首字⺟母缩略略字,指代了了⾯面向对象编程和⾯面向对象设计的五个基本原则。当这些原则被一一起应⽤用时,它们使得一一个程序员开发一一个容易易进⾏行行软件维护和扩展的系统变得更更加可能SOLID被典型的应⽤用在测试驱动开发上,并且是敏敏捷开发以及⾃自适应软件开发的基本原则的重要组成部分。

SOLID这几个字母代表什么意义?

S (单一一功能原则)

Single Responsibility Principle

单一一功能原则 :单一一功能原则 认为对象应该仅具有一一种单一一功能的概念。
换句句话说就是让一一个类只做一一种类型责任,当这个类需要承担其他类型的责任的时候,就需要分解这个类。
在所有的SOLID原则中,这是⼤大多数开发⼈人员感到最能完全理理解的一一条。严格来说,这也可能是违反最频繁的一一条原则了了。
单一一责任原则可以看作是低耦合、⾼内聚在面向对象原则上的引申,将责任定义为引起变化的原因,以提高内聚性来减少引起变化的原因。
责任过多,可能引起它变化的原因就越多,这将导致责任依赖,相互之间就产⽣生影响,
从⽽而极⼤大的损伤其内聚性和耦合度。单一一责任,通常意味着单一一的功能,因此不不要为一一个模块实 现过多的功能点,以保证实体只有一一个引起它变化的原因。

缩减一下就是:

让每个函数和每个类都只做一件小事,职责单一,解耦和

O (开闭原则)

Open Close Principle

开闭原则(ocp) 认为“软件体应该是对于扩展开放的,但是对于修改封闭的”的概念。
软件实体应该是可扩展,而不不可修改的。也就是说,对扩展是开放的,而对修改是封闭的(“开”指的就是类、模块、函数都应该具有可扩展性,“闭”指的是它们不不应该被修改。也就是说你可以新增功能但不能去修改源码。)。这个原则是诸多面向对象编程原则中最抽象、最难理理解的一个。

对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。对修改封闭,意味着类一旦设计完成,就可以独⽴立完成其⼯工作,⽽而不要对类进⾏行行任何修改。可以使用变化和不变来说明:封装不变部分,开放变化部分,一般使用接口继承实现⽅方式来实现“开放”应对变化。

说大白话就是:你不不是要变化吗?,那么我就让你继承实现一个对象,用一个接口来抽象你的职责,你变化越多,继承实现的子类就越多。

让我们来看下一个简单的 Ts demo

糟糕的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Animal {

constructor(name: any) {
super(name)
this.name = name
}
eat() {
if(this.name == '小猫') {
console.log(this.name + '吃猫粮')
} else if( this.name == "小狗") {
console.log(this.name + '吃狗粮')
}
}
}

这种糟糕的实现不但使程序的可扩展性降低, 而且每次新增一种动物都需要修改之前的代码, 容易造成隐患, 这和编程思想极大的违背

利用 OCP 思想

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Animal 抽象类
abstract class Animal {
public name: string;
constructor(name: string) {
this.name = name;
}
abstract eat(): any;
}

// 实现Animal抽象类
class Dog extends Animal {
constructor(name: any) {
super(name)
}
eat() {
console.log(this.name + '吃狗粮')
}
}
var d = new Dog('小狗')
d.eat()

上面的代码中, 首先使用abstract 定义抽象类和抽象方法,提供其他类继承的基类
我们定义只要是Animal就必须要有eat方法

于是, 我们创建了一个Dog 继承Animal ,
这个Dog就不一般了, 它规定了必须重新实现 eat 方法, 并且可以扩展自己的方法

当我们将来如果需要一只 Cat
我们一样只需要让他继承Animal抽象类, 然后实现一个自己的eat方法

L (里⽒氏替换原则)

Liskov Substitution Principle

里氏替换原则: 里氏替换原则认为“程序中的对象应该是可以在不改变程序正确性的前提下提前被它的子类所替换的”的概念。

子类必须能够替换他们的基类。即: 子类应该可以替换任何基类能够出现的地方, 并且经过替换以后, 代码还能正常工作。 另外, 不应该在代码中出现if/else之类对子类类型进行判断的条件。 里氏替换原则LSP使使代码符合开闭原则的一个重要保证。 正是由于子类的替换性才使得父类型的模块在无需修改的情况下就可以扩展。在很多情况下, 在设计初期我们类之间的关系不是很明确, LSP则给了我们一个判断和设计类之间关系的基准: 需不需要继承, 以及怎样设计继承关系。

当一个子类的实例应该能够替换任何其超类的实例时, 它们之间才具有is-A关系。 继承对于OCP, 就相当于多态性对于里氏替换原则。子类可以代替基类, 客户使用基类, 他们不需要知道派生类所做的事情。这是一个针对行为职责可替代的原则, 如果S是T的子类型, 那么S对象就应该在不改变任何抽象属性情况下替换所有T对象。

缩减一下就是:

老爸能干的事, 儿子必须都能干, 并且还要干的能多, 只要有儿子在, 老爸不在现场都没关系

I (接⼝隔离原则)

Interface Segregation Principle

接口隔离原则: 接口隔离原则认为“多个特定客户端接口要好于一个宽泛用途的接口”的概念。

不能强迫用户去依赖那些他们不能使用的接口。 换句话说, 使用多个专门的接口比使用单一的总接口总要好(JavaScript几乎没有接口的概念, 所以使用ts)。 注意: 在代码中应用 ISP 并不一定意味着服务就是绝对安全的。 仍然需要采用良好的编码实践, 以确保正确的验证与授权。

这个原则起源于施乐公司, 他们需要建立了一个新的打印机系统, 可以执行诸如装订的印刷品一套,传真多种任务。此系统软件创建从底层开始编制, 并实现了这些任务功能, 但是不断增长的软件功能却使软件本身越来越难适应变化和维护。 每一次改变,即使是最小的变化, 有人可能需要近一个小时的重新编译和重新部署。这几乎不可能再继续发展, 所以他们聘请罗伯特(Robert)帮助他们。他们首先设计了一个主要类Job,几乎能够用于实现所有任务功能。 只要调用Job类的一个方法就可以实现一个功能, Job类就变动非常大, 是一个胖模型啊, 对于客户端如果只需要一个打印功能, 但是其他无关打印的方法功能也和其耦合, ISP原则建议在客户端和Job类之间增加一个接口层, 对于不同功能有不同的接口, 比如打印功能就是Print接口, 然后将大的Job类切分为继承不同接口的子类, 这样有一个Print Job类 等等。

缩减一下就是:

每一个业务都要准备一个接口, 每个不同功能的接口再继承自公共的基础接口

D (依赖反转原则)

Dependency Inversion Principle

依赖倒置原则(Dependency Inversion Principle, DIP)规定: 代码应当取决于抽象概念, 而不是具体实现。

高层模块不应该依赖于底层模块, 二者都应该依赖于抽象
抽象不应该依赖于细节, 细节应该依赖于抽象(总结解耦)

类可能依赖于其他类来执行其工作, 但是, 他们不应当依赖于该类的特定具体实现, 而应当是它的抽象。 这个原则实在是太重要了, 社会的分工化, 标准化都是这个设计原则的体现。显然, 这一概念会大大提高系统的灵活性。 如果类只是关心他们支持特定锲约而不是特定类型的组件, 就可以快速而轻松地修改这些低级服务的功能, 同时最大限度地降低对系统其余部分地影响。

举个例子:

你家里需要洗衣机, 不可能把整个洗衣机的生产流程都给实现了, 搬到自己家里, 当然时去超市商场购买一台洗衣机。 你需要电饭煲,也不可能把电饭煲地生产工艺实现了, 也是去商场买现成地。

此时商场就是一个容器, 商场帮我们去各个工厂采购这些商品, 当我们需要某件物品时就可以去商场购买现成地商品

依赖反转原则

依赖注入

当某个⻆角⾊色要另一个角色协助时,通常由调用者来创建被调用者的实例。现在创建实例由容器来完成然后注⼊调用者。

注⼊入过程

如果需要调用另一个对象协助时,无须在代码中创建被调用者,而是依赖于外部的注⼊

依赖反转有两种方式

  1. 设值注入(在类上方设置字段)
  2. 构造注入 (在constructor中接收需要地参数)

依赖反转原则

DI(依赖注⼊入)

依赖注⼊入(Dependency Injection)

为一个方法应该遵从“依赖于抽象而不是一个实例” 的概念。依赖注⼊是该原则的一种实现⽅方式。


谢谢最可爱的贝玺
你的支持将鼓励我继续创作