Skip to content

5.1 策略模式 (Strategy Pattern)

欢迎来到行为型模式的第一站!我们将学习一个旨在优雅地处理多分支逻辑、告别 if-else 地狱的强大模式——策略模式。

核心定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。此模式让算法的变化独立于使用算法的客户端。

一、生活中的场景:诸葛亮的锦囊妙计

在《三国演义》,刘备去东吴娶亲前,诸葛亮给了赵云三个锦囊,并嘱咐他按时机拆开:

  1. 第一个锦囊(策略 A):到达南徐,大张旗鼓造势,让婚事弄假成真。
  2. 第二个锦囊(策略 B):乐不思蜀时,谎报曹操攻打荆州,催促刘备回程。
  3. 第三个锦囊(策略 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

这段代码的问题,我们在全书开篇就讨论过:

  1. 违反单一职责calculateBonus 函数承担了所有计算逻辑,非常臃肿。
  2. 违反开放/封闭原则:如果新增一个 S+ 级(年终奖 6 倍工资),或者修改 S 级的计算规则,你必须深入函数内部进行修改。在复杂的系统中,这非常危险。
  3. 复用性差:如果另一个地方也需要 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

看,我们通过增加代码,而不是修改代码,就实现了功能的扩展。这完美地体现了“开放/封闭原则”。

四、优缺点与适用场景

优点

  1. 优雅地避免 if-else:让代码结构更清晰,职责更分明。
  2. 符合开放/封闭原则:策略可以自由地增加、删除、修改,而无需改动上下文的逻辑。
  3. 高复用性:每个策略都是一个独立的功能单元,可以在任何需要的地方被复用。
  4. 可组合性:多个策略可以组合起来,实现更复杂的逻辑。

缺点

  1. 策略膨胀:如果策略非常多,会导致需要维护大量的策略类或对象。
  2. 客户端必须了解策略:客户端需要知道有哪些策略可供选择,并自行决定在何种情况下使用哪种策略。

适用场景

策略模式在 JavaScript 中的应用极其广泛:

  • 表单验证:将不同的验证规则(如“是否为空”、“是否是邮箱格式”、“最小长度”)封装成策略,按需组合使用。
  • 多种价格计算:如普通会员价、VIP 会员价、节日折扣价等。
  • 动画缓动函数linear (线性)、ease-in (缓入)、ease-out (缓出) 等不同的动画算法。
  • Web 框架中的中间件 (Middleware) 机制,在某种程度上也体现了策略模式和责任链模式的思想。