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

Strategy 模式的主体是一个 context 对象,再把逻辑中有变化的部分抽取到独立的可相互替换的 strategy 对象中,从而使 context 支持不同的策略。即 context 实现通用的逻辑,strategy 实现可替换的部分。context 与 不同的 strategy 相组合即产生了多种不同的实现。
Strategy

就像雨天穿胶鞋,打篮球穿运动鞋,短跑比赛穿跑鞋。这些不同的鞋子对应的就是一系列策略,它们是同一类对象的不同变种,彼此之间可以相互替换。
面对不同的使用场景,选择对应的策略即可,这带来了更多的灵活性。首先鞋子和人不能是绑定的,这样的话,换鞋子就需要同时换掉整个人了;其次也没有任何一双鞋可以同时满足所有的使用场景。让鞋子作为可替换的插件无疑是最直观和方便的。
总的来说,策略代表了一个对象中可替换的部分。不同的策略应对同一个问题的不同变种。静态与动态分离。

比如需要实现一个 Order 对象,代表在线商城中的订单。该对象有一个 pay() 方法,负责支付行为,将用户的钱转移到商户手中。为了能够支持多种不同的支付方式,可以有以下两种选项:

  • pay() 方法中使用 if...else,根据不同的支付方式,完成对应的支付动作
  • 将支付的具体逻辑移交给独立的 strategy 对象,用户选择支付方式后,将对应的 strategy 注入到 Order

对于第一种方案,当 Order 对象需要支持更多的支付方式时,就必须要修改 Order 本身的代码。这会使代码变得非常复杂,难以维护。
当使用第二种 Strategy 模式时,理论上可以支持无限多的支付方式。Order 只负责维护用户、商品条目、价格等信息,具体的支付逻辑则由另一个 Strategy 对象来实现。Order 本身不会由于支付方式的增加而发生任何变更。

实例:支持 JSON、INI 等多种格式的 config 对象

1
2
3
mkdir strategy && cd strategy
npm install ini
npm install object-path

package.json

1
2
3
4
5
6
7
{
"type": "module",
"dependencies": {
"ini": "^3.0.0",
"object-path": "^0.11.8"
}
}

config.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
import {promises as fs} from 'fs'
import objectPath from 'object-path'

export class Config {
constructor(formatStrategy) {
this.data = {}
this.formatStrategy = formatStrategy
}

get(configPath) {
return objectPath.get(this.data, configPath)
}

set(configPath, value) {
return objectPath.set(this.data, configPath, value)
}

async load(filePath) {
console.log(`Deserializing from ${filePath}`)
this.data = this.formatStrategy.deserialize(
await fs.readFile(filePath, 'utf-8')
)
}

async save(filePath) {
console.log(`Serializing to ${filePath}`)
await fs.writeFile(filePath,
this.formatStrategy.serialize(this.data))
}
}

其中构造函数 constructor 接收一个具体的策略对象 formStrategy 作为参数,之后的 loadsave 方法又使用这个 formStrategy 去执行与格式相关的序列化和反序列化操作。不同的 formStrategy 有着不同的实现,从而 Config 类可以凭借 construcotr 接收的不同参数,与不同的策略整合,灵活地应对不同的需求场景。

strategy.js

1
2
3
4
5
6
7
8
9
10
11
import ini from 'ini'

export const iniStrategy = {
deserialize: data => ini.parse(data),
serialize: data => ini.stringify(data)
}

export const jsonStrategy = {
deserialize: data => JSON.parse(data),
serialize: data => JSON.stringify(data, null, ' ')
}

此处的代码实现了两种不同的策略:iniStrategyjsonStrategy,分别针对不同的文件格式。它们有着一致的接口,符合策略之间可以相互替换的原则,从而都可以被前面 Config 类的 loadsave 方法调用。

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {Config} from './config.js'
import {jsonStrategy, iniStrategy} from './strategy.js'

async function main() {
const iniConfig = new Config(iniStrategy)
await iniConfig.load('samples/conf.ini')
iniConfig.set('book.nodejs', 'design patterns')
await iniConfig.save('samples/conf_mod.ini')

const jsonConfig = new Config(jsonStrategy)
await jsonConfig.load('samples/conf.json')
jsonConfig.set('book.nodejs', 'design patterns')
await jsonConfig.save('samples/conf_mod.json')
}

main()

参考资料

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