策略模式

定义一系列算法,把它们各自封装起来,并且使它们可以相互替换。

策略模式的核心是把"变"与"不变"分离:不变的是算法的调用流程,变的是具体算法。

它是消灭 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 挪了个地方,并没有真正解耦。可以配合简单工厂来隐藏选择过程。

下一步 👉 观察者与发布订阅

上次更新:
贡献者: Joe