讲解

去机场:赶时间打车,省钱坐地铁,行李多叫货拉拉——「怎么去」是一族可互换的算法,选哪个看场景,到了机场这件事本身不变。策略模式就是定义一族算法,把它们分别封装起来,使它们可以互相替换,让算法的变化独立于使用算法的客户。

它的反面教材还是那个:if (type === 'student') 七折 else if (type === 'member') 九折 else if ... 折扣规则每加一种,这个函数就长一截,而且「计算流程」和「具体算法」焊死在一起。策略模式把每种算法收进自己的类(或函数),上下文持有一个策略引用、调用时执行;换算法 = 换策略对象,上下文的代码不变——开闭原则在「算法」维度的落地。

在 TypeScript/JavaScript 里,策略模式有一个重要的语言级简化:函数是一等公民,很多场景下「策略」就是一个函数,根本不需要接口 + 类的正装。数组的 sort((a, b) => a - b) 传的就是一个比较策略;Array.map 传的是变换策略。判据很简单:策略只有一个方法、无状态、实现就几行——传函数;策略有多个协作方法、要携带配置状态、要参与继承体系——用接口 + 类。示例里两种都演示。

策略模式的常见栖息地:排序/比较规则、折扣与计价、支付方式、压缩算法、校验规则集、路由匹配。它和工厂方法经常搭档:工厂负责「按配置选出哪个策略」,策略负责「选出来之后怎么用」。

示例

折扣计算:先看「接口 + 类」的正装写法,再看「函数即策略」的 TS 简化:

import assert from 'node:assert/strict';

// 正装:策略是接口,适合多方法、有状态、成体系的算法族
interface DiscountStrategy {
  apply(price: number): number;
}

class StudentDiscount implements DiscountStrategy {
  apply(price: number): number {
    return Math.round(price * 0.7 * 100) / 100;
  }
}

class MemberDiscount implements DiscountStrategy {
  apply(price: number): number {
    return Math.round(price * 0.9 * 100) / 100;
  }
}

// 上下文只依赖策略接口
function checkout(price: number, strategy: DiscountStrategy): number {
  return strategy.apply(price);
}

assert.equal(checkout(100, new StudentDiscount()), 70);
assert.equal(checkout(100, new MemberDiscount()), 90);

// 简化:函数即策略,适合单方法、无状态的小算法
type DiscountFn = (price: number) => number;
const noDiscount: DiscountFn = (p) => p;
const doubleEleven: DiscountFn = (p) => (p >= 300 ? p - 50 : p);

assert.equal(noDiscount(100), 100);
assert.equal(doubleEleven(320), 270);
assert.equal(doubleEleven(100), 100);

console.log('学生价:' + checkout(100, new StudentDiscount()) + ',满减价:' + doubleEleven(320));

checkout 对「怎么算折扣」一无所知——这就是算法与使用方的分离。注意「函数即策略」不是偷懒版而是官方推荐姿势:TypeScript 的类型别名 (price: number) => number 本身就是一份完整契约,还顺手接入了全部现成函数(包括第三方库的)。只有当你的策略需要「apply 之外还有 describe、validate 等多个协作方法」时,接口 + 类的正装才值回票价。

常见坑

  • 上下文里残留算法分支:checkout 里还在 if (type === ...) 选分支,说明策略没有真正接管;选择逻辑应收敛到工厂或装配处。
  • 策略携带易变状态:策略实例被多个上下文共享时,内部的可变字段会造成串单;策略优先设计成无状态的。
  • 为一个方法建五个类:能用一个函数表达的策略硬拆成接口 + 实现 + 注册 + 工厂,是「为模式而模式」的典型症状。
  • 策略间有大量重复代码不抽象:多个策略共享「先算原价再打折」的骨架时,应该引入模板方法或把公共部分提为工具函数,别复制粘贴。
  • 客户端必须知道所有策略才能选:客户端 if-else 选策略等于把分派逻辑又写回去了;选择交给配置 + 工厂。

小结

策略模式把一族算法封装成可互换的对象(或函数),上下文面向抽象调用;TS 里优先函数策略,复杂算法族再上接口。下一章:固定流程骨架、子类填充步骤的模板方法模式。