编码之道-设计模式原则
设计模式(Design Pattern)是对软件设计中普遍存在(反复出现)的各种问题所提出的解决方案。设计模式并不直接用来完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案。 --- 维基百科
对于这类抽象的的理论属于编码之"道";咱通过实践结合来加深理解
# 设计模式原则
设计模式其实是针对面向对象编程范式总结出来的解决方案,所以设计模式的原则都是围绕“类”和“接口”这两个概念来提出的,其中下面 6 个原则非常重要,因为这 6 个原则决定了设计模式的规范和标准。
# S – Single Responsibility Principle 单一职责原则
- 一个程序只做好一件事
- 如果功能过于复杂就拆分开,每个部分保持独立
应该有且仅有一个原因引起类的变更。这个原则很好理解,一个类代码量越多,功能就越复杂,维护成本也就越高。遵循单一职责原则可以有效地控制类的复杂度。
像下面这种情形经常在项目中看到,一个公共类聚集了很多不相关的函数,这就违反了单一职责原则。
class Util {
static toTime(date) {
...
}
static formatString(str) {
...
}
static encode(str) {
...
}
}
2
3
4
5
6
7
8
9
10
11
# **O – OpenClosed Principle 开放/封闭原则 **开闭原则
- 对扩展开放,对修改封闭
- 增加需求时,扩展新代码,而非修改已有代码
开闭原则指的就是对扩展开放、对修改关闭。编写代码的时候不可避免地会碰到修改的情况,而遵循开闭原则就意味着当代码需要修改时,可以通过编写新的代码来扩展已有的代码,而不是直接修改已有代码本身。
下面的伪代码是一个常见的表单校验功能,校验内容包括用户名、密码、验证码,每个校验项都通过判断语句 if-else 来控制。
function validate() {
// 校验用户名
if (!username) {
...
} else {
...
}
// 校验密码
if (!pswd){
...
} else {
...
}
// 校验验证码
if (!captcha) {
...
} else {
...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这么写看似没有问题,但其实可扩展性并不好,如果此时增加一个校验条件,就要修改 validate() 函数内容。
下面的伪代码遵循开闭原则,将校验规则抽取出来,实现共同的接口 IValidateHandler,同时将函数 validate() 改成 Validation 类,通过 addValidateHandler() 函数添加校验规则,通过 validate() 函数校验表单。这样,当有新的校验规则出现时,只要实现 IValidateHandler 接口并调用 addValidateHandler() 函数即可,不需要修改类 Validation 的代码。
class Validation {
private validateHandlers: ValidateHandler[] = [];
public addValidateHandler(handler: IValidateHandler) {
this.validateHandlers.push(handler)
}
public validate() {
for (let i = 0; i < this.validateHandlers.length; i++) {
this.validateHandlers[i].validate();
}
}
}
interface IValidateHandler {
validate(): boolean;
}
class UsernameValidateHandler implements IValidateHandler {
public validate() {
...
}
}
class PwdValidateHandler implements IValidateHandler {
public validate() {
...
}
}
class CaptchaValidateHandler implements IValidateHandler {
public validate() {
...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# L – Liskov Substitution Principle 里氏替换原则
- 子类能覆盖父类
- 父类能出现的地方子类就能出现
里氏替换原则是指在使用父类的地方可以用它的任意子类进行替换。里氏替换原则是对类的继承复用作出的要求,要求子类可以随时替换掉其父类,同时功能不被破坏,父类的方法仍然能被使用。
下面的代码就是一个违反里氏替换原则的例子,子类 Sparrow 重载了父类 Bird 的 getFood() 函数,但返回值发生了修改。那么如果使用 Bird 类实例的地方改成 Sparrow 类实例则会报错。
class Bird {
getFood() {
return '虫子'
}
}
class Sparrow extends Bird {
getFood() {
return ['虫子', '稻谷']
}
}
2
3
4
5
6
7
8
9
10
11
对于这种需要重载的类,正确的做法应该是让子类和父类共同实现一个抽象类或接口。下面的代码就是实现了一个 IBird 接口来遵循里氏替换原则。
interface IBird {
getFood(): string[]
}
class Bird implements IBird{
getFood() {
return ['虫子']
}
}
class Sparrow implements IBird {
getFood() {
return ['虫子', '稻谷']
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# I – Interface Segregation Principle 接口隔离原则
- 保持接口的单一独立
- 类似单一职责原则,这里更关注接口
不应该依赖它不需要的接口,也就是说一个类对另一个类的依赖应该建立在最小的接口上。目的就是为了降低代码之间的耦合性,方便后续代码修改。
下面就是一个违反接口隔离原则的反例,类 Dog 和类 Bird 都继承了接口 IAnimal,但是 Bird类并没有 swim函数,只能实现一个空函数 swim()。
interface IAnimal {
eat(): void
swim(): void
}
class Dog implements IAnimal {
eat() {
...
}
swim() {
...
}
}
class Bird implements IAnimal {
eat() {
...
}
swim() {
// do nothing
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# D – Dependency Inversion Principle 依赖倒置原则
- 面向接口编程,依赖于抽象而不依赖于具体
- 使用方只关注接口而不关注具体类的实现
准确说应该是避免依赖倒置,好的依赖关系应该是类依赖于抽象接口,不应依赖于具体实现。这样设计的好处就是当依赖发生变化时,只需要传入对应的具体实例即可。
下面的示例代码中,类 Passenger 的构造函数需要传入一个 Bike 类实例,然后在 start() 函数中调用 Bike 实例的 run() 函数。此时类 Passenger 和类 Bike 的耦合非常紧,如果现在要支持一个 Car 类则需要修改 Passenger 代码。
class Bike {
run() {
console.log('Bike run')
}
}
class Passenger {
construct(Bike: bike) {
this.tool = bike
}
public start() {
this.tool.run()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
如果遵循依赖倒置原则,可以声明一个接口 ITransportation,让 Passenger 类的构造函数改为 ITransportation 类型,从而做到 Passenger 类和 Bike 类解耦,这样当 Passenger 需要支持 Car 类的时候,只需要新增 Car 类即可。
interface ITransportation {
run(): void
}
class Bike implements ITransportation {
run() {
console.log('Bike run')
}
}
class Car implements ITransportation {
run() {
console.log('Car run')
}
}
class Passenger {
construct(ITransportation : transportation) {
this.tool = transportation
}
public start() {
this.tool.run()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# L – Law of Demeter 迪米特原则
一个类对于其他类知道得越少越好,就是说一个对象应当对其他对象尽可能少的了解。这一条原则要求任何一个对象或者方法只能调用该对象本身和内部创建的对象实例,如果要调用外部的对象,只能通过参数的形式传递进来。这一点和纯函数的思想相似。
下面的类 Store 就违反了迪米特原则,类内部使用了全局变量。
class Store {
set(key, value) {
window.localStorage.setItem(key, value)
}
}
2
3
4
5
一种改造方式就是在初始化的时候将 window.localstorage 作为参数传递给 Store 实例。
class Store {
construct(s) {
this._store = s
}
set(key, value) {
this._store.setItem(key, value)
}
}
new Store(window.localstorage)
2
3
4
5
6
7
8
9