Strategy 模式的主体是一个 context 对象,再把逻辑中有变化的部分抽取到独立的可相互替换的 strategy 对象中,从而使 context 支持不同的策略。即 context 实现通用的逻辑,strategy 实现可替换的部分。context 与 不同的 strategy 相组合即产生了多种不同的实现。
就像雨天穿胶鞋,打篮球穿运动鞋,短跑比赛穿跑鞋。这些不同的鞋子对应的就是一系列策略,它们是同一类对象的不同变种,彼此之间可以相互替换。
面对不同的使用场景,选择对应的策略即可,这带来了更多的灵活性。首先鞋子和人不能是绑定的,这样的话,换鞋子就需要同时换掉整个人了;其次也没有任何一双鞋可以同时满足所有的使用场景。让鞋子作为可替换的插件无疑是最直观和方便的。
总的来说,策略代表了一个对象中可替换的部分。不同的策略应对同一个问题的不同变种。静态与动态分离。
比如需要实现一个 Order
对象,代表在线商城中的订单。该对象有一个 pay()
方法,负责支付行为,将用户的钱转移到商户手中。为了能够支持多种不同的支付方式,可以有以下两种选项:
- 在
pay()
方法中使用if...else
,根据不同的支付方式,完成对应的支付动作 - 将支付的具体逻辑移交给独立的 strategy 对象,用户选择支付方式后,将对应的 strategy 注入到
Order
中
对于第一种方案,当 Order 对象需要支持更多的支付方式时,就必须要修改 Order 本身的代码。这会使代码变得非常复杂,难以维护。
当使用第二种 Strategy 模式时,理论上可以支持无限多的支付方式。Order 只负责维护用户、商品条目、价格等信息,具体的支付逻辑则由另一个 Strategy 对象来实现。Order 本身不会由于支付方式的增加而发生任何变更。
实例:支持 JSON、INI 等多种格式的 config 对象
1 | mkdir strategy && cd strategy |
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
30import {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
作为参数,之后的 load
和 save
方法又使用这个 formStrategy
去执行与格式相关的序列化和反序列化操作。不同的 formStrategy
有着不同的实现,从而 Config
类可以凭借 construcotr
接收的不同参数,与不同的策略整合,灵活地应对不同的需求场景。
strategy.js
:1
2
3
4
5
6
7
8
9
10
11import 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, ' ')
}
此处的代码实现了两种不同的策略:iniStrategy
和 jsonStrategy
,分别针对不同的文件格式。它们有着一致的接口,符合策略之间可以相互替换的原则,从而都可以被前面 Config
类的 load
和 save
方法调用。
index.js
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import {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()