Typescript 重走常用设计模式(三)
2019-03-22 #Coding

本节主要涉及行为型模式:观察者模式、迭代器模式、状态模式、备忘录模式

行为型模式

观察者模式 Observer

概念

定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。

示例 UML 类图

示例代码实现

Subject 主题或称发布者接口,是被观察者,可以是类或者接口,这里使用接口。其定义了attach()detach()方法来用于添加、移除观察者对象,同时还有notifyAllObservers()用于通知观察者们;

ConcreteSubject 具体的主题,其实现了 Subject ,其内部包含一个observers成员变量用于存储观察者们以及一些变化的数据例如state,当数据状态变化时,调用notifyAllObservers()通知所有观察者;其内部还可以定义一些业务逻辑用于更改状态、发起通知,或者完全供外部调用通知方法并传递参数通知所有观察者;

Observer,观察者或称订阅者接口,声明了接到更新时的方法update()

ConcreteObserver 具体的观察者,实现了 Observer 中的update()方法,当接到更新通知是执行一定操作。

1
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
interface Subject {
attach(state: Observer): void
detach(observer: Observer): void
notifyAllObservers(): void
}

class ConcreteSubject implements Subject {
private observers: Observer[] = []
public state: number = 0
// 添加 Observer 观察者
public attach(observer: Observer) {
if (this.observers.includes(observer)) {
console.log('Observer 观察者已存在')
return
}
this.observers.push(observer)
}
// 移除 Observer 观察者
public detach(observer: Observer) {
const index = this.observers.indexOf(observer)
if (index === -1) {
console.log('Observer 观察者不存在')
return
}
this.observers.splice(index, 1)
}
// 通知全部观察者 observers,这里是简单示例,可按需传参给 观察者
public notifyAllObservers() {
this.observers.forEach((observer) => {
observer.update(this)
})
}
// 做一些业务 更新 state 在通知全部观察者,当然也可以外部调用传参等灵活应用
public doSomething() {
console.log('Subject: 进行一些业务,将 state 改为2,并通知 Observers 更新')
this.state = 2
this.notifyAllObservers()
}
}

interface Observer {
update(subject: Subject): void
}

class ConcreteObserver implements Observer {
public name: string
constructor(name: string) {
this.name = name
// 观察者也可以在构造时传入需要观察的 subject 实例直接订阅
// subject.attach(this)
}
// 订阅后 被通知会执行
public update(subject: Subject) {
const concreteSubject = subject as ConcreteSubject
console.log(`${this.name} 接到更新通知了,最新的 state 是 ${concreteSubject.state}`)
}
}

const subject = new ConcreteSubject()

// 创建 observerA 并订阅 subject
const observerA = new ConcreteObserver('observerA')
subject.attach(observerA)
// 创建 observerB 并订阅 subject
const observerB = new ConcreteObserver('observerB')
subject.attach(observerB)
// subject 执行一些业务更新 state
subject.doSomething()

// console output
// Subject: 进行一些业务,将 state 改为2,并通知 Observers 更新
// observerA 接到更新通知了,最新的 state 是 2
// observerB 接到更新通知了,最新的 state 是 2

适用场景

观察者模式使用频率也相当高,诸如 DOM 中事件绑定相关背后都是观察者模式的体现

  • 当一个对象的改变需要同时改变其他对象时
  • 当一些对象必须观察其他对象的状态变化来处理自身业务时

分析

  • 符合开放封闭原则,主题和观察者分离,无需修改原有主题代码就可以增加观察者
  • 通过定义一个消息通知机制,可以实现展现层和数据层的分离,可以有各式各样的展现层作为具体观察者
  • 注意防止观察者和主题之间循环调用,防止出现崩溃

迭代器模式 Iterator

概念

提供一种方法顺序一个聚合对象中各个元素,而又不暴露该对象内部表示。

示例 UML 类图

示例代码实现

Iterator 是抽象迭代器接口,定义了访问元素下一个和判断是否有下一个的方法,这是迭代器运行的核心

ConcreteIterator 是具体的迭代器,实现了 Iterator,其中 index 作为游标来记录当前位置

Aggregate 是抽象聚合类接口,可以理解为一种 Collection 集合,声明了聚合内部需要返回一个 具体的迭代器实例,类似一个迭代器工厂

ConcreteAggregate 是具体的聚合类,实现了 Aggregate 内部存储管理聚合数据,并返回一个 ConcreteIterator 具体迭代器

实际前端应用中,可能都会简化这个结构,例如不需要 Aggregate 和 ConcreteAggregate,直接创建 Iterator 来进行数据遍历;将遍历顺序改为倒序等等

1
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// Iterator 迭代器接口
interface Iterator<T> {
hasNext(): boolean
next(): T | null
}
// ConcreteIterator 具体的迭代器
class ConcreteIterator<T> implements Iterator<T> {
protected list: Array<T>
protected index: number
constructor(list: Array<T>) {
this.list = list
this.index = 0
}

public hasNext() {
if (this.index > this.list.length - 1) {
return false
}
return true
}
public next() {
if (this.hasNext()) {
return this.list[this.index++]
}
return null
}
}

// Aggregate 聚合类抽象接口,可以理解为 Collection 集合
// 内部管理集合的数据,并返回一个 具体的迭代器实例,类似一个迭代器工厂
interface Aggregate<T> {
createIterator(): Iterator<T>
}
// 具体的聚合类 管理数据并返回一个迭代器实例
class ConcreteAggregate<T> implements Aggregate<T> {
public list: Array<T>
constructor(list: Array<T>) {
this.list = list
}
createIterator() {
return new ConcreteIterator(this.list)
}
}

// 创建一个新的数据集合
const collection = new ConcreteAggregate(['h','e','l','l','o'])
const iterator = collection.createIterator()
// 使用迭代器来遍历数据
while (iterator.hasNext()) {
console.log(iterator.next())
}

// console output
// h
// e
// l
// l
// o

在这里不得不提下 ES6 中的 Iterator,其主要目的是:

  • 为诸多的有序集合数据类型提供一个统一的遍历接口
  • 为 for…of 语法实现循环遍历

ES6 的 Iterator 存储在所符合数据结构的 Symbol.iterator 属性上,其存储的是个函数,可以用来判断是否为有序集合可以 for…of 遍历。

比如 执行 String.prototype.[Symbol.iterator]() 返回一个迭代器

1
2
3
4
5
String.prototype[Symbol.iterator]
ƒ [Symbol.iterator]() { [native code] }

String.prototype[Symbol.iterator]()
StringIterator {}

其返回迭代器上也实现了next()方法,其中包含 valuedone,用来返回值和判断是否遍历结束

1
2
String.prototype[Symbol.iterator]().next()
{value: undefined, done: true}

具备 Iterator 的数据结构有:

Array, String, Map, Set, TypedArray, 函数的 arguments 入参对象, NodeList

Generator 中也实现了 Iterator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function* testGenerator() {
yield 'test'
yield 'this'
return 'end'
}
const newTest = testGenerator()
newTest[Symbol.iterator]
// ƒ [Symbol.iterator]() { [native code] }
// 也实现了 Iterator 接口

newTest.next()
{value: "test", done: false}
newTest.next()
{value: "this", done: false}
newTest.next()
{value: "end", done: true}

// 也可以 for of 循环去执行
for (let item of testGenerator()) {
console.log(item)
}
VM4093:2 test
VM4093:2 this

适用场景

  • 想要为不同的或者未知的数据结构创造一个统一的遍历接口,同时不修改暴露内部数据结构
  • 想要对聚合对象增加多种迭代遍历方法时

分析

  • 符合单一职责原则,各个迭代器都完成自己的独立职责,诸多的遍历代码可以被封装为独立的类
  • 符合开放封闭原则,无需修改原有数据聚合对象,可增量创建不同的迭代器
  • 迭代器数量随着目标数据聚合类的增加,一定程度上增加了系统复杂性

状态模式 State

概念

允许一个对象在其内部状态改变的时候改变它的行为,对象看起来似乎修改了自身所属的类。

示例 UML 类图

示例代码实现

Context 是上下文环境类,保存了一个对具体状态 State 实例的引用,并将和状态相关的行为委托给这个状态实例,当状态改变时其对应的行为方法也在发生对应的改变;

State 是抽象状态类,抽象了状态应该实现的不同方法,并保存了对 Context 的引用供子类使用,同时一些公用的状态方法可以写在这里;

NormalState… 这些是具体状态类,他们继承了 State,不同状态各自实现自己的状态方法,而且还能根据 Context 的引用来触发状态改变;

1
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// Context 上下文环境类,示例被实例化成一个蓝光播放器 
class Context {
private state: State
constructor() {
this.state = new NoDiskState(this)
}
public changeState(state: State) {
console.log(`State 变更为: ${state.constructor.name}`)
this.state = state
}

public insertDisc() {
console.log('insertDisc 插入光盘')
this.changeState(new NormalState(this))
}
public removeDisc() {
this.state.removeDisc()
}
public play() {
this.state.play()
}
public playNext() {
this.state.playNext()
}
}

// 抽象状态类
abstract class State {
protected context: Context
constructor(context: Context) {
this.context = context
}
public abstract play(): void
public abstract playNext(): void
public abstract removeDisc(): void
}
// 正常状态
class NormalState extends State {
public play() {
console.log('NormalState: 开始播放')
}
public playNext() {
console.log('NormalState: 开始播放 下一个节目')
}
public removeDisc() {
console.log('NormalState: 弹出光盘')
this.context.changeState(new NoDiskState(this.context))
}
}
// 无光盘状态
class NoDiskState extends State {
public play() {
console.log('NoDiskState: 请插入光盘')
}
public playNext() {
console.log('NoDiskState: 无法播放下一首,请插入光盘')
}
public removeDisc() {
console.log('NoDiskState: 已无光盘,请插入光盘')
}
}
const bluRayPlayer = new Context()
bluRayPlayer.play()
bluRayPlayer.insertDisc()
bluRayPlayer.play()
bluRayPlayer.playNext()
bluRayPlayer.removeDisc()
bluRayPlayer.play()

// console output
// NoDiskState: 请插入光盘
// insertDisc 插入光盘
// State 变更为: NormalState
// NormalState: 开始播放
// NormalState: 开始播放 下一个节目
// NormalState: 弹出光盘
// State 变更为: NoDiskState
// NoDiskState: 请插入光盘

适用场景

状态模式的应用场景在日常开发中也很常见,大部分地方都有状态的概念,比如:网站文章状态、文件的上传状态等。

  • 对象需要根据自身状态进行不同的行为,而且状态类型也相当多,具体状态的代码实现也可能频繁变更
  • 大量的条件语句处理状态时,状态有较多的重复代码,状态模式可以将其抽离到抽象类里

分析

  • 符合单一职责原则,每个状态具体的行为代码相互独立
  • 符合开放封闭原则,增量增加状态无需修改原有状态和上下文
  • 注意状态切换相关结构设计,因可以在具体状态中进行状态切换,可能会导致整体结构混乱、链路不清晰

备忘录模式 Memento

概念

在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。

示例 UML 类图

示例代码实现

Originator 为原发器,可以创建一个需要记录自身状态,生成备忘快照的实例,多把编辑器内容区、可视化工具组件等需要保持状态的类设计成原发器;

Memento 为备忘录,用来存储 Originator 中对应的状态等,其根据原发器需要存储的结构进行设计,额外存储一些时间戳等;

Caretaker 为负责人,可以理解为历史记录管理器,将 Memento 有序化存储,并按需取出、展示 Memento

1
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// 原发器 可以理解为 Editor 等
class Originator {
private state: string
constructor() {
this.state = ''
}
public saveMemento() {
return new Memento(this.state)
}
public restoreMemento(memento: Memento) {
this.state = memento.getState()
console.log(`Originator: 恢复数据为 ${this.state}`)
}
public writeSomething(str: string) {
this.state += str
console.log(`Originator: 修改数据为 ${this.state}`)
}
}

// 备忘录 只用来存储和获取状态,根据 Originator 设计
class Memento {
private state: string
private date: string
constructor(state: string) {
this.state = state
this.date = new Date().toLocaleString()
}
public getState() {
return this.state
}
public getDate() {
return this.date
}
}

// 负责人 可以理解为历史记录管理器,记录 Memento,恢复等
class Caretaker {
private originator: Originator
private mementos: Memento[]
constructor(originator: Originator) {
this.originator = originator
this.mementos = []
}
public save() {
this.mementos.push(this.originator.saveMemento())
}
public undo() {
if (this.mementos.length === 0) {
return
}
const memento = this.mementos.pop() as Memento
this.originator.restoreMemento(memento)

}
public showHistory() {
console.log(`Caretaker: 修改历史:`);
this.mementos.forEach((memento) => {
console.log(`${memento.getDate()} - ${memento.getState()}`)
})
}
}

const editor = new Originator()
const historyAdmin = new Caretaker(editor)

historyAdmin.save()
editor.writeSomething('123')

historyAdmin.save()
editor.writeSomething('hello')

historyAdmin.showHistory()
historyAdmin.undo()

// 原发器 可以理解为 Editor 等,需要记录自身状态,生成备忘快照的类,
class Originator {
private state: string
constructor() {
this.state = ''
}
public saveMemento() {
return new Memento(this.state)
}
public restoreMemento(memento: Memento) {
this.state = memento.getState()
console.log(`Originator: 恢复数据为 ${this.state}`)
}
public writeSomething(str: string) {
this.state += str
console.log(`Originator: 修改数据为 ${this.state}`)
}
}

// 备忘录 只用了存储和获取状态,根据 Originator 设计
class Memento {
private state: string
private date: string
constructor(state: string) {
this.state = state
this.date = new Date().toLocaleString()
}
public getState() {
return this.state
}
public getDate() {
return this.date
}
}

// 负责人 可以理解为历史记录管理器,记录 Memento,恢复等
class Caretaker {
private originator: Originator
private mementos: Memento[]
constructor(originator: Originator) {
this.originator = originator
this.mementos = []
}
public save() {
this.mementos.push(this.originator.saveMemento())
}
public undo() {
if (this.mementos.length === 0) {
return
}
const memento = this.mementos.pop() as Memento
this.originator.restoreMemento(memento)

}
public showHistory() {
console.log(`Caretaker: 修改历史:`);
this.mementos.forEach((memento) => {
console.log(`${memento.getDate()} - ${memento.getState()}`)
})
}
}

const editor = new Originator()
const historyAdmin = new Caretaker(editor)

historyAdmin.save()
editor.writeSomething('123')

historyAdmin.save()
editor.writeSomething('hello')

historyAdmin.showHistory()
historyAdmin.undo()

// console output
// Originator: 修改数据为 123
// Originator: 修改数据为 123hello
// Caretaker: 修改历史:
// 2019-3-20 8:38:04 ├F10: PM┤ -
// 2019-3-20 8:38:04 ├F10: PM┤ - 123
// Caretaker: 恢复至上一次编辑内容
// Originator: 恢复数据为 123

适用场景

各类涉及到编辑内容的程序都涉及到撤销操作,延伸到游戏存档等都可以涉及到备忘录模式思想的应用

  • 目标对象需要记录状态快照,按需恢复至之前状态时可以恢复至之前的状态

分析

  • 提供了一种恢复机制,新状态无效或者出问题时恢复至之前的状态
  • 可以在不破坏对象封装的前提下创建对象的快照
  • 注意控制备忘录存储大小,比如设置可撤销步骤上限,防止过渡消耗资源
  • Javascript 中考虑 Immutable 不可变数据,防止快照中状态被改变

参考

相关文章