Appearance
5.1 策略模式 (Strategy Pattern)
欢迎来到行为型模式的第一站!我们将学习一个旨在优雅地处理多分支逻辑、告别 if-else 地狱的强大模式——策略模式。
核心定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。此模式让算法的变化独立于使用算法的客户端。
一、生活中的场景:诸葛亮的锦囊妙计
在《三国演义》,刘备去东吴娶亲前,诸葛亮给了赵云三个锦囊,并嘱咐他按时机拆开:
- 第一个锦囊(策略 A):到达南徐,大张旗鼓造势,让婚事弄假成真。
- 第二个锦囊(策略 B):乐不思蜀时,谎报曹操攻打荆州,催促刘备回程。
- 第三个锦囊(策略 C):路上被追杀时,向孙夫人求救。
在这个故事中:
- 赵云 (Context / 上下文):是策略的使用者。他负责根据当前的情况(
context),决定使用哪个策略。 - 三个锦囊 (Strategies / 策略族):每一个锦囊都封装了一个独立的、可执行的计策(算法)。
- 计策是可替换的:在不同的时间点,赵云使用了不同的锦囊,解决了不同的问题。
策略模式的核心思想就是:将这些“计策”(算法)从“使用者”(赵云)的代码中剥离出来,让它们成为一个个独立、可互换的“锦囊”。
二、没有策略模式的代码:冗长的 if-else
假设我们要为一家公司计算员工的年终奖。规则如下:
- 绩效为 S 级,年终奖为 4 倍工资。
- 绩效为 A 级,年终奖为 3 倍工资。
- 绩效为 B 级,年终奖为 2 倍工资。
- 绩效为 C 级,年终奖为 1 倍工资。
很多人会下意识地写出这样的代码:
javascript
function calculateBonus(level, salary) {
if (level === "S") {
return salary * 4;
}
if (level === "A") {
return salary * 3;
}
if (level === "B") {
return salary * 2;
}
if (level === "C") {
return salary * 1;
}
}
console.log(calculateBonus("S", 10000)); // 40000
console.log(calculateBonus("A", 8000)); // 24000这段代码的问题,我们在全书开篇就讨论过:
- 违反单一职责:
calculateBonus函数承担了所有计算逻辑,非常臃肿。 - 违反开放/封闭原则:如果新增一个
S+级(年终奖 6 倍工资),或者修改 S 级的计算规则,你必须深入函数内部进行修改。在复杂的系统中,这非常危险。 - 复用性差:如果另一个地方也需要 S 级的计算逻辑,你只能复制粘贴代码。
三、使用策略模式重构
我们的目标是:把 if-else 里的每一块逻辑都封装成一个独立的“锦囊”。
第 1 步:封装策略 (Strategies)
我们将每一个计算规则都封装成一个独立的函数。
javascript
// strategies 是一个包含了所有策略的对象
const strategies = {
S: function (salary) {
return salary * 4;
},
A: function (salary) {
return salary * 3;
},
B: function (salary) {
return salary * 2;
},
C: function (salary) {
return salary * 1;
},
};第 2 步:创建上下文 (Context)
Context 负责接收客户端的请求,并委托给具体的策略对象去执行。
javascript
function calculateBonus(level, salary) {
// 查找并调用对应的策略
if (strategies[level]) {
return strategies[level](salary);
}
// 如果没有对应的策略,可以返回一个默认值或抛出错误
return salary;
}第 3 步:客户端调用
客户端的使用方式和之前完全一样,但我们的 calculateBonus 内部结构已经发生了翻天覆地的变化。
javascript
console.log(calculateBonus("S", 10000)); // 40000
console.log(calculateBonus("A", 8000)); // 24000现在,让我们看看它的扩展性有多好。如果 HR 决定新增一个 S+ 级别:
javascript
// 我们完全不需要修改 calculateBonus 函数!
// 只需要在 strategies 中增加一个新的策略即可。
strategies["S+"] = function (salary) {
return salary * 6;
};
console.log(calculateBonus("S+", 15000)); // 90000看,我们通过增加代码,而不是修改代码,就实现了功能的扩展。这完美地体现了“开放/封闭原则”。
四、优缺点与适用场景
优点
- 优雅地避免
if-else:让代码结构更清晰,职责更分明。 - 符合开放/封闭原则:策略可以自由地增加、删除、修改,而无需改动上下文的逻辑。
- 高复用性:每个策略都是一个独立的功能单元,可以在任何需要的地方被复用。
- 可组合性:多个策略可以组合起来,实现更复杂的逻辑。
缺点
- 策略膨胀:如果策略非常多,会导致需要维护大量的策略类或对象。
- 客户端必须了解策略:客户端需要知道有哪些策略可供选择,并自行决定在何种情况下使用哪种策略。
适用场景
策略模式在 JavaScript 中的应用极其广泛:
- 表单验证:将不同的验证规则(如“是否为空”、“是否是邮箱格式”、“最小长度”)封装成策略,按需组合使用。
- 多种价格计算:如普通会员价、VIP 会员价、节日折扣价等。
- 动画缓动函数:
linear(线性)、ease-in(缓入)、ease-out(缓出) 等不同的动画算法。 - Web 框架中的中间件 (Middleware) 机制,在某种程度上也体现了策略模式和责任链模式的思想。