Node.js 设计模式笔记 —— State 模式

State 模式是一种特殊形式的 Strategy 模式:Context 选择的具体策略根据不同的 state 发生变化。
对于 Strategy 模式,可以基于不同的变量比如传入的参数来决定选择具体哪个策略,一旦选择确定后,直到 context 剩余的整个生命周期结束,该策略都保持不变。相反在 State 模式中,策略(或者在这里的语境下,叫做状态)在 context 的生命周期里是动态变化的,从而允许对象的行为可以根据内部状态的变化自适应地更改。
State pattern

举例来说,我们需要创建一个宾馆预定系统,由一个 Reservation 类对预定房间的行为进行建模。

考虑如下一系列事件:

  • 当 reservation 对象初次创建后,其处于未确认状态。用户可以通过一个 confirm() 方法对此次预定进行确认。但不能通过 cancel() 方法取消预订,因为此次预定还并没有被确认。可以使用 delete() 方法删除这条记录
  • 一旦该 reservation 被确认,订单处于已确认状态。confirm() 方法将不能再次被调用(不能重复确认);但该 reservation 支持通过 cancel() 方法进行取消,同时该记录无法被删除(已经有人确认预定)
  • 在 reservation 日期的前一天,订单处于已生效状态。上述所有 3 个方法都不再支持,用户只能办理入住

State pattern

参考上图,可以实现 3 种 不同的策略,他们都实现了 confirm()cancel()delete() 这几个方法。每种策略的具体逻辑由不同的状态决定。Reservation 对象只需要在每次状态切换时,激活对应的策略。

实例:failsafe socket

1
2
mkdir state && cd state
npm install json-over-tcp-2

package.json

1
2
3
4
5
6
{
"type": "module",
"dependencies": {
"json-over-tcp-2": "^0.3.5"
}
}

failsafeSocket.js

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
import {OfflineState} from './offlineState.js'
import {OnlineState} from './onlineState.js'

export class FailsafeSocket {
constructor(options) {
this.options = options
this.queue = []
this.currentState = null
this.socket = null
this.states = {
offline: new OfflineState(this),
online: new OnlineState(this)
}
this.changeState('offline')
}

changeState(state) {
console.log(`Activating state: ${state}`)
this.currentState = this.states[state]
this.currentState.activate()
}

send(data) {
this.currentState.send(data)
}
}

上述 FailsafeSocket 类主要由以下几个组件构成:

  • 构造函数 constructor 会初始化一个 queue 队列来存储 socket 离线时发送的数据,还创建了 offlineonline 两种不同的状态,分别对应离线时和在线时 socket 的不同行为
  • changeState() 方法负责不同状态的切换。它会更新当前状态 currentState 并调用该状态的 activate() 方法
  • send() 方法包含 FailsafeSocket 类的主要功能,它会基于在线或离线状态触发不同的行为。这里它将具体的操作指派给了当前激活的状态对象去实现

offlineState.js

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
import jsonOverTcp from 'json-over-tcp-2'

export class OfflineState {
constructor(failsafeSocket) {
this.failsafeSocket = failsafeSocket
}

send(data) {
this.failsafeSocket.queue.push(data)
}

activate() {
const retry = () => {
setTimeout(() => this.activate(), 1000)
}

console.log('Trying to connect...')
this.failsafeSocket.socket = jsonOverTcp.connect(
this.failsafeSocket.options,
() => {
console.log('Connection established')
this.failsafeSocket.socket.removeListener('error', retry)
this.failsafeSocket.changeState('online')
}
)
this.failsafeSocket.socket.once('error', retry)
}
}

上述模块负责定义 socket 处于离线状态时的行为。

  • send() 方法只负责将接受到的数据存储到队列中,因为此时是离线状态,队列中的数据会在 socket 在线时取出并发送
  • activate() 方法会尝试建立连接,连接失败则隔一秒重试。成功建立连接后,failsafeSocket 的状态变为在线状态,触发在线状态的 activate() 方法

onlineState.js

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
export class OnlineState {
constructor(failsafeSocket) {
this.failsafeSocket = failsafeSocket
this.hasDisconnected = false
}

send(data) {
this.failsafeSocket.queue.push(data)
this._safeWrite(data)
}

_safeWrite(data) {
this.failsafeSocket.socket.write(data, (err) => {
if (!this.hasDisconnected && !err) {
this.failsafeSocket.queue.pop()
}
})
}

activate() {
this.hasDisconnected = false
for (const data of this.failsafeSocket.queue) {
this._safeWrite(data)
}

this.failsafeSocket.socket.once('error', () => {
this.hasDisconnected = true
this.failsafeSocket.changeState('offline')
})
}
}

OnlineState 模块实现了当 socket 处于在线状态时的行为。

  • send() 方法会将数据放入队列,并立即尝试将其写入到 socket,因为此时是在线状态。若该数据成功写入,则将其从队列中移除
  • activate() 方法会在连接成功建立时触发,会尝试发送在 socket 离线时排队的所有数据并监听任意错误事件。若有错误发生,转换到离线状态,触发离线状态的 activate 方法,继续尝试建立连接

server.js

1
2
3
4
5
6
7
8
9
10
import jsonOverTcp from 'json-over-tcp-2'

const server = jsonOverTcp.createServer({port: 5000})
server.on('connection', socket => {
socket.on('data', data => {
console.log('Client data', data)
})
})

server.listen(5000, () => console.log('Server started'))

client.js

1
2
3
4
5
6
import {FailsafeSocket} from './failsafeSocket.js'

const failsafeSocket = new FailsafeSocket({port: 5000})
setInterval(() => {
failsafeSocket.send(process.memoryUsage())
}, 1000)

参考资料

Node.js Design Patterns: Design and implement production-grade Node.js applications using proven patterns and techniques, 3rd Edition