策略
策略模式使称为上下文的对象能够通过将可变部分提取到称为策略的单独的、可互换的对象中来支持其逻辑的变化。 上下文实现了一系列算法的通用逻辑,而策略实现了可变部分,允许上下文根据不同的因素(例如输入值、系统配置或用户偏好)调整其行为。
策略通常是一系列解决方案的一部分,并且所有解决方案都实现上下文所需的相同接口。 下图就是我们刚才描述的情况:
/image-2024-05-07-14-58-55-447.png)
图 9.1 展示了上下文对象如何将不同的策略插入到其结构中,就好像它们是一台机器的可替换部件一样。 想象一辆汽车; 它的轮胎可以认为是它适应不同路况的策略。 由于其防滑钉,我们可以安装冬季轮胎以在雪路上行驶,同时我们可以决定安装高性能轮胎以主要在高速公路上进行长途旅行。 一方面,我们不想改变整个汽车来实现这一点,另一方面,我们也不希望汽车有八个轮子,以便它可以在所有可能的道路上行驶。
我们很快就会明白这种模式有多么强大。 它不仅有助于分离给定问题中的关注点,而且还使我们的解决方案具有更好的灵活性并适应同一问题的不同变化。
策略模式在支持组件行为变化需要复杂的条件逻辑(大量 if…else 或 switch 语句)或混合同一系列的不同组件的所有情况下特别有用。 想象一个名为 Order 的对象,它代表电子商务网站上的在线订单。 该对象有一个名为 pay() 的方法,正如它所说,该方法完成订单并将资金从用户转移到在线商店。
为了支持不同的支付系统,我们有以下几种选择:
-
在 pay() 方法中使用 if…else 语句,根据所选支付选项完成操作
-
将支付逻辑委托给策略对象,该策略对象实现用户选择的特定支付网关的逻辑
在第一个解决方案中,我们的 Order 对象无法支持其他支付方式,除非修改其代码。 此外,当支付选项数量增加时,这可能会变得相当复杂。 相反,使用策略模式使 Order 对象能够支持几乎无限数量的支付方式,并将其范围限制为仅管理用户的详细信息、购买的商品和相对价格,同时将完成支付的工作委托给 另一个物体。
现在让我们用一个简单、现实的例子来演示这个模式。
多格式配置对象
让我们考虑一个名为 Config 的对象,它保存应用程序使用的一组配置参数,例如数据库 URL、服务器的侦听端口等。 Config 对象应该能够提供一个简单的接口来访问这些参数,而且还提供一种使用持久存储(例如文件)导入和导出配置的方法。 我们希望能够支持不同的格式来存储配置,例如 JSON、INI 或 YAML。
通过应用我们所学到的策略模式,我们可以立即识别 Config 对象的变量部分,这是允许我们序列化和反序列化配置的功能。 这将通过我们的战略来实施。
让我们创建一个名为 config.js 的新模块,然后定义配置管理器的通用部分:
import { promises as fs } from 'fs'
import objectPath from 'object-path'
export class Config {
constructor (formatStrategy) { // (1)
this.data = {}
this.formatStrategy = formatStrategy
}
get (configPath) { // (2)
return objectPath.get(this.data, configPath)
}
set (configPath, value) { // (2)
return objectPath.set(this.data, configPath, value)
}
async load (filePath) { // (3)
console.log(`Deserializing from ${filePath}`)
this.data = this.formatStrategy.deserialize(
await fs.readFile(filePath, 'utf-8')
)
}
async save (filePath) { // (3)
console.log(`Serializing to ${filePath}`)
await fs.writeFile(filePath,
this.formatStrategy.serialize(this.data))
}
}
这就是前面代码中发生的情况:
-
在构造函数中,我们创建一个名为 data 的实例变量来保存配置数据。 然后我们还存储 formatStrategy,它代表我们将用来解析和序列化数据的组件。
-
我们提供了两个方法 set() 和 get(),通过利用名为 object-path (nodejsdp.link/object-path) 的库,使用点分路径表示法(例如 property.subProperty)来访问配置属性 。
-
load() 和 save() 方法是我们分别将数据的反序列化和序列化委托给我们的策略的地方。 这是 Config 类的逻辑根据构造函数中作为输入传递的 formatStrategy 进行更改的地方。
正如我们所看到的,这种非常简单和整洁的设计允许 Config 对象在加载和保存数据时无缝支持不同的文件格式。 最好的部分是,支持这些不同格式的逻辑没有在任何地方进行硬编码,因此只要采取正确的策略,Config 类就可以在不进行任何修改的情况下适应几乎任何文件格式。
为了演示这一特性,现在让我们在名为 strategy.js 的文件中创建几个格式策略。 让我们从使用 INI 文件格式解析和序列化数据的策略开始,INI 文件格式是一种广泛使用的配置格式(更多信息请参见:nodejsdp.link/ini-format)。
对于该任务,我们将使用一个名为 ini (nodejsdp.link/ini) 的 npm 包:
import ini from 'ini'
export const iniStrategy = {
deserialize: data => ini.parse(data),
serialize: data => ini.stringify(data)
}
没有什么真正复杂的! 我们的策略只是实现约定的接口,以便可以被 Config 对象使用。
同样,我们要创建的下一个策略允许我们支持 JSON 文件格式,该格式在 JavaScript 和一般 Web 开发生态系统中广泛使用:
export const jsonStrategy = {
deserialize: data => JSON.parse(data),
serialize: data => JSON.stringify(data, null, ' ')
}
现在,为了向您展示所有内容是如何组合在一起的,让我们创建一个名为 index.js 的文件,并尝试使用不同的格式加载和保存示例配置:
import { Config } from './config.js'
import { jsonStrategy, iniStrategy } from './strategies.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()
我们的测试模块揭示了策略模式的核心属性。 我们只定义了一个 Config 类,它实现了配置管理器的公共部分,然后,通过使用不同的序列化和反序列化数据策略,我们创建了支持不同文件格式的不同 Config 类实例。
我们刚刚看到的示例仅向我们展示了选择策略时可能的替代方案之一。 其他有效的方法可能如下:
-
创建两个不同的策略系列:一个用于反序列化,另一个用于序列化。 这将允许读取一种格式并保存为另一种格式。
-
动态选择策略:根据所提供文件的扩展名,Config 对象可以维护映射扩展名→策略,并使用它为给定扩展名选择正确的算法。
正如我们所看到的,我们有多种选择来选择要使用的策略,正确的一种仅取决于您的要求以及您想要获得的功能和简单性方面的权衡。
此外,模式本身的实现也可能有很大差异。 例如,在最简单的形式中,上下文和策略都可以是简单的函数:
function context(strategy) {...}
尽管这看起来微不足道,但在 JavaScript 等编程语言中,它不应该被低估,在 JavaScript 中,函数是一等公民,并且与成熟的对象一样使用。
然而,在所有这些变化之间,不变的是模式背后的想法。 与往常一样,实现可能会略有变化,但驱动模式的核心概念始终相同。
策略模式的结构可能看起来与适配器模式类似。 然而,两者之间存在实质性差异。 适配器对象不会向适配器添加任何行为; 它只是使其在另一个界面下可用。 这还可能需要实现一些额外的逻辑来将一个接口转换为另一个接口,但该逻辑仅限于此任务。 然而,在策略模式中,上下文和策略实现算法的两个不同部分,因此两者都实现某种逻辑,并且对于构建最终算法(组合在一起时)都是必不可少的。 |
in the wild
Passport (nodejsdp.link/passportjs) 是 Node.js 的身份验证框架,它允许 Web 服务器支持不同的身份验证方案。 借助 Passport,我们可以轻松地为我们的 Web 应用程序提供 Facebook 登录或 Twitter 登录功能。 Passport 使用策略模式将身份验证过程中使用的通用逻辑与可以更改的部分(即实际的身份验证步骤)分开。 例如,我们可能希望使用 OAuth 来获取访问令牌来访问 Facebook 或 Twitter 个人资料,或者只是使用本地数据库来验证用户名/密码对。 对于 Passport 来说,这些都是完成身份验证过程的不同策略,正如我们可以想象的那样,这使得库能够支持几乎无限数量的身份验证服务。 查看 nodejsdp.link/passportstrategies 支持的不同身份验证提供程序的数量,以了解策略模式的功能。