一、从一个常见的“烦恼”说起

想象一下,你正在开发一个电商网站,其中有一个非常重要的功能:计算订单的最终价格。一开始,需求很简单,所有用户都享受统一的9折优惠。你可能会写出这样的代码:

// 技术栈:JavaScript (ES6+)
function calculatePrice(originalPrice) {
    // 直接打九折
    return originalPrice * 0.9;
}

// 使用示例
let price = calculatePrice(100);
console.log(`最终价格是:${price}元`); // 输出:最终价格是:90元

代码运行得很好。但没过多久,产品经理跑过来说:“我们需要支持多种促销策略!比如满100减20,或者打8折,以后可能还会有会员专属折扣……”

你可能会想,这简单,加几个if...else不就行了?于是代码变成了这样:

// 技术栈:JavaScript (ES6+)
function calculatePrice(originalPrice, promotionType) {
    if (promotionType === 'discount9') {
        return originalPrice * 0.9;
    } else if (promotionType === 'discount8') {
        return originalPrice * 0.8;
    } else if (promotionType === 'minus100_20') {
        // 满100减20的逻辑
        return originalPrice - Math.floor(originalPrice / 100) * 20;
    }
    // 未来可能还有更多的 else if...
    return originalPrice;
}

看起来功能实现了,但问题也随之而来。每增加一种新的促销方式,你都必须回来修改这个calculatePrice函数。这个函数会变得越来越臃肿,像一团理不清的毛线,难以阅读和维护。更糟糕的是,如果你不小心改动了某个策略的逻辑,可能会影响到其他看似无关的代码。这种“硬编码”的方式,让我们的代码失去了灵活性,而策略模式,正是为了解决这类“算法(或行为)频繁变化和替换”的问题而生的。

简单来说,策略模式就像是一个工具箱。你把不同的工具(算法)分门别类放好,当需要拧螺丝时,你就拿出螺丝刀;需要敲钉子时,你就拿出锤子。你不需要改造你的手,也不需要把螺丝刀和锤子焊在一起,你只需要根据情况,灵活地选择和使用合适的工具。在编程中,这个“工具箱”就是策略模式,它让你能定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换,从而让算法的变化独立于使用算法的客户。

二、策略模式的核心:把算法“打包”起来

策略模式的核心思想非常直观:将算法(或行为)从其使用场景(上下文)中分离出来。怎么分离呢?就是为每一种算法都单独创建一个“策略类”或“策略函数”。这些策略都遵循相同的接口或规范,这样它们就可以像插件一样,被我们的主程序(上下文)随时切换使用。

让我们用代码来重构刚才那个令人头疼的计价函数。首先,我们把每一种计价算法都“打包”成独立的函数或对象:

// 技术栈:JavaScript (ES6+)

// 1. 定义一系列具体的策略(算法)
// 策略A:普通折扣
const normalDiscountStrategy = {
    calculate(price) {
        return price * 0.9;
    }
};

// 策略B:疯狂折扣
const crazyDiscountStrategy = {
    calculate(price) {
        return price * 0.6;
    }
};

// 策略C:满减策略
const fullReductionStrategy = {
    calculate(price) {
        const reduction = Math.floor(price / 100) * 20;
        return price - reduction;
    }
};

// 策略D:无优惠策略
const noDiscountStrategy = {
    calculate(price) {
        return price;
    }
};

你看,我们把每一种算法都封装成了一个拥有calculate方法的独立对象。它们各司其职,互不干扰。接下来,我们需要一个“上下文”(Context)来使用这些策略。这个上下文就是原来的calculatePrice功能的升级版,它自己不负责计算,只负责“选择”和“委托”:

// 技术栈:JavaScript (ES6+)

// 2. 定义上下文(Context)类
class PriceCalculator {
    constructor(strategy) {
        // 在创建计算器时,就给它“装配”一个策略
        this.strategy = strategy;
    }

    // 提供一个方法,允许动态更换策略
    setStrategy(strategy) {
        this.strategy = strategy;
    }

    // 执行计算,实际工作委托给当前策略对象
    calculate(originalPrice) {
        if (!this.strategy || typeof this.strategy.calculate !== 'function') {
            throw new Error('请设置有效的计价策略');
        }
        return this.strategy.calculate(originalPrice);
    }
}

现在,让我们看看如何使用这个全新的、灵活的计价系统:

// 技术栈:JavaScript (ES6+)

// 3. 客户端使用代码
// 场景一:普通促销日,使用9折策略
let calculator = new PriceCalculator(normalDiscountStrategy);
console.log(`普通促销价:${calculator.calculate(300)}元`); // 输出:270元

// 场景二:双十一大促,临时切换为疯狂6折策略
calculator.setStrategy(crazyDiscountStrategy);
console.log(`双十一狂欢价:${calculator.calculate(300)}元`); // 输出:180元

// 场景三:处理一个满减订单
calculator.setStrategy(fullReductionStrategy);
console.log(`满减后价格:${calculator.calculate(320)}元`); // 输出:280元 (满300减60)

// 场景四:恢复原价
calculator.setStrategy(noDiscountStrategy);
console.log(`商品原价:${calculator.calculate(300)}元`); // 输出:300元

通过这样的改造,代码的结构变得清晰多了。增加一个新的促销策略,比如“第二件半价”,你只需要新增一个策略对象,而完全不用去修改PriceCalculator类或者已有的其他策略。这完美符合了设计原则中的“开闭原则”:对扩展开放,对修改关闭。

三、更优雅的实现:利用JavaScript的函数特性

在JavaScript中,函数是“一等公民”,可以像变量一样被传递和赋值。这让我们实现策略模式变得更加简洁和自然。我们甚至不需要定义那些策略对象,直接把函数当作策略来用。

// 技术栈:JavaScript (ES6+)

// 1. 直接定义一系列策略函数
function normalDiscount(price) { return price * 0.9; }
function crazyDiscount(price) { return price * 0.6; }
function fullReduction(price) { return price - Math.floor(price / 100) * 20; }
function noDiscount(price) { return price; }

// 2. 一个极其简单的上下文
function createPriceCalculator(strategyFn) {
    return {
        setStrategy(newStrategyFn) {
            strategyFn = newStrategyFn;
        },
        calculate(price) {
            return strategyFn(price);
        }
    };
}

// 3. 使用示例
let myCalculator = createPriceCalculator(normalDiscount);
console.log(`使用函数策略 - 折扣价:${myCalculator.calculate(200)}元`); // 180元

myCalculator.setStrategy(fullReduction);
console.log(`使用函数策略 - 满减价:${myCalculator.calculate(250)}元`); // 210元

这种函数式的写法更加符合JavaScript的风格,减少了模板代码,让核心逻辑一目了然。策略模式在这里的本质,其实就是**“高阶函数”**的一个典型应用——一个函数(calculate)的内部逻辑,通过接收另一个函数(策略函数)作为参数来决定。

四、策略模式的用武之地

策略模式绝不仅仅用于计算价格。在任何需要根据不同条件执行不同算法或行为的场景,它都能大显身手。让我们再看两个例子。

场景一:表单验证 一个表单可能有多种验证规则(必填、邮箱格式、手机号格式、最小长度等)。我们可以为每一种规则定义一个策略。

// 技术栈:JavaScript (ES6+)
// 表单验证策略
const validationStrategies = {
    required(value) {
        return value !== undefined && value !== null && value.toString().trim() !== '';
    },
    email(value) {
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        return emailRegex.test(value);
    },
    minLength(value, minLen) {
        return value && value.length >= minLen;
    }
};

// 验证器上下文
class FormValidator {
    constructor() {
        this.rules = []; // 存储验证规则 { strategy: fn, field: ‘字段名‘, args: [] }
    }

    addRule(field, strategyName, ...args) {
        const strategyFn = validationStrategies[strategyName];
        if (strategyFn) {
            this.rules.push({ field, strategyFn, args });
        }
        return this; // 支持链式调用
    }

    validate(formData) {
        const errors = [];
        this.rules.forEach(rule => {
            const value = formData[rule.field];
            // 执行具体的验证策略
            if (!rule.strategyFn(value, ...rule.args)) {
                errors.push(`${rule.field} 验证失败`);
            }
        });
        return errors;
    }
}

// 使用
const validator = new FormValidator();
validator
    .addRule('username', 'required')
    .addRule('username', 'minLength', 6)
    .addRule('email', 'email');

const userInput = { username: 'abc', email: 'wrong-email' };
const result = validator.validate(userInput);
console.log('表单验证结果:', result); // 输出:['username 验证失败', 'email 验证失败']

场景二:数据压缩与导出 一个系统可能需要支持将数据导出为不同的格式(JSON, CSV, XML),或者用不同的算法压缩(ZIP, GZIP, RAR)。每种格式或算法都是一个独立的策略。

// 技术栈:JavaScript (ES6+)
// 数据导出策略
const exportStrategies = {
    toJSON(data) {
        console.log(`正在将数据转换为美观的JSON字符串...`);
        return JSON.stringify(data, null, 2);
    },
    toCSV(data) {
        console.log(`正在将数据转换为CSV格式...`);
        // 简化实现,假设data是对象数组
        const headers = Object.keys(data[0]).join(',');
        const rows = data.map(item => Object.values(item).join(','));
        return [headers, ...rows].join('\n');
    },
    toXML(data) {
        console.log(`正在将数据转换为XML格式...`);
        // 简化实现
        return `<data>${JSON.stringify(data)}</data>`;
    }
};

// 导出器上下文
class DataExporter {
    constructor(strategy = 'toJSON') {
        this.setStrategy(strategy);
    }

    setStrategy(strategyName) {
        this.strategyFn = exportStrategies[strategyName];
    }

    export(data, fileName) {
        if (!this.strategyFn) {
            throw new Error('未设置导出策略');
        }
        const content = this.strategyFn(data);
        console.log(`内容已生成,准备保存为文件:${fileName}`);
        // 此处模拟文件保存,实际中可能涉及Blob和下载链接创建
        console.log('文件内容预览:\n', content.slice(0, 100) + '...');
        return content;
    }
}

// 使用
const data = [{ name: '张三', age: 25 }, { name: '李四', age: 30 }];
const exporter = new DataExporter('toCSV');
exporter.export(data, 'users.csv');

// 动态切换为JSON导出
exporter.setStrategy('toJSON');
exporter.export(data, 'users.json');

五、策略模式的优缺点与使用注意事项

就像任何工具一样,策略模式有其擅长的领域,也有其局限性。

优点:

  1. 极高的灵活性:算法可以自由切换,这是它最大的优点。就像给程序装上了可插拔的组件。
  2. 优秀的扩展性:增加新策略时,无需修改上下文或其他策略的代码,符合“开闭原则”。
  3. 良好的隔离性:每个策略类/函数只关注自己的算法逻辑,代码职责单一,易于理解和维护。
  4. 可以避免多重条件判断:它将原本可能冗长的if-elseswitch-case语句分散到各个策略中,使上下文代码更简洁。

缺点:

  1. 策略类/函数数量可能爆炸:如果算法只有寥寥几种且几乎不会变化,使用策略模式反而会增加系统的复杂度和类的数量。
  2. 客户端必须了解所有策略:使用方需要知道有哪些策略,以及它们之间的区别,才能做出正确的选择。这在一定程度上增加了使用成本。
  3. 通信开销:策略模式通常会增加程序中的对象数量。所有策略都需要实现相同的接口,这可能通过间接调用带来微小的性能开销,但在绝大多数场景下可忽略不计。

注意事项:

  1. 不要滥用:如果算法很稳定,很少发生变化,那么简单的条件判断可能更直接、更清晰。策略模式是为了应对“变化”而生的。
  2. 策略的创建与管理:当策略数量很多时,可以考虑结合工厂模式来创建策略对象,或者将策略的配置信息放在外部(如配置文件),使选择过程更动态化。
  3. 与状态模式的区别:初学者容易混淆策略模式和状态模式。简单来说,策略模式是客户端主动选择不同的算法来完成同一个任务;而状态模式是对象内部状态改变时,自动触发行为的改变,客户端通常不感知状态的存在。策略模式是“怎么做”的替换,状态模式是“是什么”的转换。

六、总结

策略模式是一种非常实用且强大的设计模式,它通过“分离变化”来提升代码的健壮性和可维护性。它将那些未来可能变化的算法部分抽取出来,封装成独立的、可互换的模块。在JavaScript这种灵活的语言中,我们可以用对象,也可以用更轻量的函数来实现它。

记住它的核心:定义算法家族,分别封装,让它们之间可以互相替换。此模式让算法的变化,不会影响到使用算法的客户。

当你下次再面对一堆令人头疼的if-else分支,并且预感到这些分支还会不断增长时,不妨停下来想一想:“这里是不是可以用策略模式来优化一下?” 学会识别并使用这种模式,能让你的代码在面对需求变更时更加从容不迫,真正写出易于扩展和维护的软件。