策略模式
定义一系列算法,把它们各自封装起来,并且使它们可以相互替换。
策略模式的核心是把"变"与"不变"分离:不变的是算法的调用流程,变的是具体算法。
它是消灭 if/else 最常用的手段,也是开放封闭原则最直接的体现——新增一种策略不需要修改任何已有代码。
一、识别信号
// ❌ 每加一档奖金规则就要改这个函数
function calculateBonus(level, salary) {
if (level === 'S') return salary * 4;
if (level === 'A') return salary * 3;
if (level === 'B') return salary * 2;
return salary;
}
问题:函数随规则增多无限膨胀、无法复用单条规则、无法单独测试某一档、改动时容易误伤其他分支。
// ✅ 策略对象:每条规则独立、可测试、可复用
const bonusStrategies = {
S: salary => salary * 4,
A: salary => salary * 3,
B: salary => salary * 2,
};
function calculateBonus(level, salary) {
const strategy = bonusStrategies[level];
if (!strategy) throw new Error(`未知等级: ${level}`);
return strategy(salary);
}
// 扩展时不碰上面任何一行
bonusStrategies.C = salary => salary * 1.5;
用对象字面量存策略要注意原型链
bonusStrategies['toString'] 会拿到 Object.prototype.toString 而不是 undefined。策略 key 来自用户输入时,用 Map 或 Object.create(null) 更安全:
const strategies = new Map([['S', s => s * 4]]);
strategies.get(level);
二、完整实战:表单校验
这是策略模式在前端最典型的落地场景。
不变的部分:校验器
class Validator {
constructor() {
this.cache = []; // 待校验的函数队列
}
/**
* @param {any} value 待校验的值
* @param {string} rule 形如 'minLength:6' 的规则
* @param {string} errorMsg 校验失败时的提示
*/
add(value, rule, errorMsg) {
const args = rule.split(':'); // ['minLength', '6']
const strategyName = args.shift(); // 'minLength',剩下的是参数
const strategy = strategies[strategyName];
if (!strategy) throw new Error(`未定义的校验规则: ${strategyName}`);
this.cache.push(() => strategy(value, ...args, errorMsg));
return this; // 链式调用
}
start() {
for (const validatorFn of this.cache) {
const msg = validatorFn();
if (msg) return msg; // 只要有一条不通过就立即返回
}
return undefined; // 全部通过
}
}
变的部分:策略集合
const strategies = {
isNotEmpty(value, errorMsg) {
if (value === '' || value == null) return errorMsg;
},
minLength(value, length, errorMsg) {
if (value.length < Number(length)) return errorMsg;
},
maxLength(value, length, errorMsg) {
if (value.length > Number(length)) return errorMsg;
},
mobileFormat(value, errorMsg) {
if (!/^1[3-9]\d{9}$/.test(value)) return errorMsg;
},
};
一个流传很广的错误正则
/(^1[3|5|8][0-9]{9}$)/ // ❌
[3|5|8] 是字符类,里面的 | 不是"或",而是被当作字面字符 | 本身——这个正则实际匹配的是 1 后面跟 3、5、8 或竖线。同时它漏掉了 17x、19x 等号段。正确写法:/^1[3-9]\d{9}$/。
使用
function validateForm({ username, password, phone }) {
return new Validator()
.add(username, 'isNotEmpty', '用户名不能为空')
.add(password, 'minLength:6', '密码长度不能少于 6 位')
.add(phone, 'mobileFormat', '手机号格式不正确')
.start();
}
const errorMsg = validateForm({ username: '', password: '123', phone: '110' });
if (errorMsg) alert(errorMsg); // '用户名不能为空'
新增一条校验规则,只需要往 strategies 里加一个函数,Validator 一个字都不用改。
三、更多应用
动画缓动函数——easing 就是一组策略:
const easing = {
linear: t => t,
easeIn: t => t * t,
easeOut: t => t * (2 - t),
easeInOut: t => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
};
多种支付 / 登录方式:
const paymentStrategies = new Map([
['alipay', order => alipaySDK.pay(order)],
['wechat', order => wechatSDK.pay(order)],
['card', order => bankGateway.charge(order)],
]);
const pay = (type, order) => {
const strategy = paymentStrategies.get(type);
if (!strategy) throw new Error(`不支持的支付方式: ${type}`);
return strategy(order);
};
排序规则、价格计算规则、权限判定规则……凡是"同一件事有多种做法",都是策略的用武之地。
四、策略 vs 状态
两者结构几乎相同(都持有一组行为对象并委托),区别在谁来切换:
| 策略模式 | 状态模式 | |
|---|---|---|
| 谁决定用哪个 | 客户端主动选择 | 对象内部根据当前状态自动切换 |
| 各实现之间 | 互相不知道对方存在 | 通常知道下一个状态是谁 |
| 语义 | 做同一件事的不同算法 | 同一个对象的不同阶段 |
五、代价
什么时候不该用策略
- 分支只有两三个且永远不会增加:
isVip ? a : b就够了,包装成策略反而增加理解成本; - 各分支逻辑差异极大、参数完全不同:强行统一签名会造出一堆用不上的参数;
- 使用方必须知道所有策略的区别才能选对——这时策略只是把
if/else挪了个地方,并没有真正解耦。可以配合简单工厂来隐藏选择过程。
下一步 👉 观察者与发布订阅
