策略模式(Strategy Pattern)在 JS 中的应用:实现可替换的算法逻辑

编程专家讲座:策略模式(Strategy Pattern)在 JS 中的应用:实现可替换的算法逻辑

各位同仁,大家好!

今天,我们将深入探讨一个在软件设计中极为强大且实用的设计模式——策略模式(Strategy Pattern),并着重讲解它在 JavaScript 世界中的应用。随着前端和后端 JavaScript 应用的日益复杂,我们经常面临需要根据不同条件或配置执行不同算法的场景。在这种情况下,如果一味地使用大量的 if/elseswitch 语句来控制逻辑,代码将变得难以阅读、难以维护,并且违反了开放/封闭原则。策略模式正是解决这类问题的利器,它允许我们在运行时动态地替换算法,从而实现可插拔的逻辑。

1. 策略模式概述:解耦算法与上下文

1.1 什么是策略模式?

策略模式属于行为型设计模式,其核心思想是定义一系列算法,将每一个算法封装起来,并使它们可以相互替换。策略模式让算法独立于使用它的客户端而变化。

简单来说,当您有一个类,它需要根据某些条件执行不同的行为时,您可以将这些行为(算法)抽象出来,分别封装到独立的“策略”对象中。然后,这个类(我们称之为“上下文”)不再直接实现这些行为,而是持有一个对某个策略对象的引用,并将具体的行为委托给这个策略对象来执行。这样,上下文就可以在运行时根据需要切换不同的策略,而无需修改自身的代码。

1.2 为什么我们需要策略模式?

考虑一个常见的场景:一个电商平台需要根据用户的会员等级、促销活动或支付方式来计算最终价格。如果直接在订单处理逻辑中堆砌大量的 if...else if...elseswitch 语句来判断并执行不同的计算规则,代码会迅速膨胀,变得脆弱。

  • 代码难以维护: 每次新增或修改一个计算规则,都需要修改核心的订单处理逻辑。
  • 违反开放/封闭原则: 核心逻辑对扩展是关闭的(不能在不修改其代码的情况下添加新功能),对修改是开放的(每次新增规则都需要修改)。
  • 代码冗余: 不同的计算规则可能包含一些相似的逻辑,但因为纠缠在一起,难以复用。
  • 测试复杂: 难以对单一的计算规则进行独立测试,必须通过整个订单处理流程来验证。

策略模式正是为了解决这些问题而生。它将算法的定义、选择和使用过程分离,让代码更加模块化、可维护和可扩展。

1.3 策略模式的核心组件

策略模式通常包含以下三个核心组件:

  1. 策略(Strategy)接口/抽象策略:

    • 定义了一个公共接口,所有具体的策略类都必须实现这个接口。
    • 这个接口声明了上下文类用于调用具体策略的方法。
    • 在 JavaScript 中,这通常是一个隐式的契约(即一组函数签名),或者通过定义一个基类/抽象类来实现。
  2. 具体策略(Concrete Strategy):

    • 实现了策略接口/抽象策略,封装了具体的算法或行为。
    • 每个具体策略类都实现了一种特定的算法。
  3. 上下文(Context):

    • 持有一个对策略对象的引用。
    • 上下文不直接实现算法,而是将算法的执行委托给它所持有的策略对象。
    • 客户端通常通过上下文来使用策略模式,上下文负责根据需要配置(设置)具体的策略对象。

下图展示了策略模式的UML类图概念(尽管在JS中更多是对象和函数而非严格的类):

+------------------+         +---------------------+
|      Context     |         |    <<interface>>    |
|------------------|         |       Strategy      |
| - strategy: Strategy |-------> +---------------------+
|------------------|         | + executeAlgorithm()|
| + setStrategy(s) |         +----------^----------+
| + execute()      |                    |
+------------------+                    | implements
                                        |
                 +----------------------+----------------------+
                 |                      |                      |
      +---------------------+ +---------------------+ +---------------------+
      |  ConcreteStrategyA  | |  ConcreteStrategyB  | |  ConcreteStrategyC  |
      |---------------------| |---------------------| |---------------------|
      | + executeAlgorithm()| | + executeAlgorithm()| | + executeAlgorithm()|
      +---------------------+ +---------------------+ +---------------------+

2. JavaScript 中的策略模式:函数与对象的灵活运用

JavaScript 作为一门动态、函数优先的语言,为策略模式的实现提供了极大的灵活性。我们不一定需要严格的类和接口,常常可以直接利用函数或普通对象来充当策略。

2.1 JS 实现策略模式的优势

  • 函数作为一等公民: JavaScript 中的函数可以像普通值一样被传递、赋值和作为参数,这使得将算法封装为函数变得非常自然和高效。
  • 对象字面量: 简单的策略可以直接定义为包含方法的对象字面量。
  • 动态性: 运行时可以轻松地替换策略对象或函数。

2.2 基本实现示例:电商价格计算器

让我们通过一个电商场景来具体实现策略模式。假设我们需要为商品计算最终价格,根据不同的促销活动,折扣方式会有所不同:

  1. 无折扣
  2. 百分比折扣
  3. 固定金额折扣
  4. 买X送Y折扣

问题:不使用策略模式的传统实现

// 假设商品价格和数量
const itemPrice = 100;
const quantity = 2;
const totalAmount = itemPrice * quantity; // 初始总金额 200

function calculateFinalPriceWithoutStrategy(initialAmount, discountType, discountValue) {
    let finalPrice = initialAmount;

    if (discountType === 'none') {
        // 无折扣
        finalPrice = initialAmount;
    } else if (discountType === 'percentage') {
        // 百分比折扣,discountValue 是折扣百分比(如 0.1 表示 10%)
        finalPrice = initialAmount * (1 - discountValue);
    } else if (discountType === 'fixedAmount') {
        // 固定金额折扣,discountValue 是折扣金额
        finalPrice = initialAmount - discountValue;
        if (finalPrice < 0) finalPrice = 0; // 价格不能为负
    } else if (discountType === 'buyXGetYFree') {
        // 买X送Y,简化为每X个商品赠送1个同类商品,discountValue 为X
        // 比如买2送1,discountValue = 2
        const freeItemsCount = Math.floor(quantity / (discountValue + 1)); // 假设买X送1,实际支付X件商品费用
        const payableItemsCount = quantity - freeItemsCount;
        finalPrice = itemPrice * payableItemsCount;
    } else {
        console.warn('未知折扣类型:', discountType);
        finalPrice = initialAmount;
    }

    return finalPrice;
}

console.log("--- 传统计算方式 ---");
console.log("无折扣:", calculateFinalPriceWithoutStrategy(totalAmount, 'none')); // 200
console.log("10%折扣:", calculateFinalPriceWithoutStrategy(totalAmount, 'percentage', 0.1)); // 180
console.log("减免20元:", calculateFinalPriceWithoutStrategy(totalAmount, 'fixedAmount', 20)); // 180
console.log("买2送1 (商品价格100, 购买2件):", calculateFinalPriceWithoutStrategy(100 * 2, 'buyXGetYFree', 1)); // 购买2件,买1送1,实际支付1件价格 = 100
console.log("买2送1 (商品价格100, 购买3件):", calculateFinalPriceWithoutStrategy(100 * 3, 'buyXGetYFree', 1)); // 购买3件,买1送1,实际支付2件价格 = 200
console.log("买2送1 (商品价格100, 购买4件):", calculateFinalPriceWithoutStrategy(100 * 4, 'buyXGetYFree', 1)); // 购买4件,买1送1,实际支付2件价格 = 200 (这里简化了,实际应为支付2件,得到4件)

// 假设我们购买了2件价格100的商品,初始总价200
const initialAmount = 200;
console.log("无折扣:", calculateFinalPriceWithoutStrategy(initialAmount, 'none', 0)); // 200
console.log("百分比折扣 (10%):", calculateFinalPriceWithoutStrategy(initialAmount, 'percentage', 0.1)); // 180
console.log("固定金额折扣 (减20元):", calculateFinalPriceWithoutStrategy(initialAmount, 'fixedAmount', 20)); // 180

可以看到,calculateFinalPriceWithoutStrategy 函数内部包含了大量的条件判断。如果未来需要增加新的折扣类型(如满减、阶梯折扣),这个函数将不得不继续膨胀,变得越来越难以管理。

解决方案:使用策略模式

  1. 定义策略接口(隐式):
    在 JavaScript 中,我们可以约定所有折扣策略都必须实现一个 calculate(initialAmount, discountValue, itemPrice, quantity) 方法(或函数),该方法接收初始金额、折扣值、单价和数量,并返回最终金额。

  2. 具体策略(Concrete Strategies):
    我们将每种折扣算法封装成一个独立的函数或对象。

    // 2.1 定义具体折扣策略对象
    const discountStrategies = {
        // 无折扣策略
        none: {
            calculate: (initialAmount, discountValue, itemPrice, quantity) => {
                console.log("应用:无折扣");
                return initialAmount;
            }
        },
    
        // 百分比折扣策略
        percentage: {
            calculate: (initialAmount, discountPercentage, itemPrice, quantity) => {
                console.log(`应用:百分比折扣 ${discountPercentage * 100}%`);
                if (discountPercentage < 0 || discountPercentage > 1) {
                    console.warn("折扣百分比应在0到1之间。");
                    return initialAmount;
                }
                return initialAmount * (1 - discountPercentage);
            }
        },
    
        // 固定金额折扣策略
        fixedAmount: {
            calculate: (initialAmount, fixedDiscountAmount, itemPrice, quantity) => {
                console.log(`应用:固定金额折扣 减${fixedDiscountAmount}元`);
                let finalPrice = initialAmount - fixedDiscountAmount;
                return finalPrice < 0 ? 0 : finalPrice; // 确保最终价格不为负
            }
        },
    
        // 满减折扣策略 (新增的复杂策略)
        fullReduction: {
            calculate: (initialAmount, thresholds, itemPrice, quantity) => {
                console.log(`应用:满减折扣 (满${thresholds.full}减${thresholds.reduction})`);
                if (initialAmount >= thresholds.full) {
                    return initialAmount - thresholds.reduction;
                }
                return initialAmount;
            }
        },
    
        // 买X送Y折扣策略 (这里简化实现,假设买X送1)
        buyXGetYFree: {
            calculate: (initialAmount, buyXCount, itemPrice, quantity) => {
                console.log(`应用:买${buyXCount}送1`);
                if (quantity <= buyXCount) {
                    return initialAmount; // 没达到买X的数量
                }
                const freeItemsCount = Math.floor(quantity / (buyXCount + 1)); // 简化:每买X+1件,送1件
                const payableItemsCount = quantity - freeItemsCount;
                return itemPrice * payableItemsCount;
            }
        },
    
        // 会员专属折扣 (假设会员等级越高折扣越多)
        memberDiscount: {
            calculate: (initialAmount, memberLevel, itemPrice, quantity) => {
                let discountRate = 0;
                switch (memberLevel) {
                    case 'bronze':
                        discountRate = 0.05; // 5% off
                        break;
                    case 'silver':
                        discountRate = 0.1; // 10% off
                        break;
                    case 'gold':
                        discountRate = 0.15; // 15% off
                        break;
                    default:
                        discountRate = 0;
                }
                console.log(`应用:${memberLevel}会员折扣 ${discountRate * 100}%`);
                return initialAmount * (1 - discountRate);
            }
        }
    };
  3. 上下文(Context)类:
    创建一个 PriceCalculator 类,它将持有一个对当前折扣策略的引用,并提供一个方法来执行计算。

    // 2.2 定义上下文类
    class PriceCalculator {
        constructor(itemPrice, quantity) {
            this.itemPrice = itemPrice;
            this.quantity = quantity;
            this.initialAmount = itemPrice * quantity;
            this.currentStrategy = discountStrategies.none; // 默认策略为无折扣
            this.discountValue = null; // 策略可能需要的额外参数
        }
    
        // 设置折扣策略的方法
        setStrategy(strategyName, discountValue = null) {
            const strategy = discountStrategies[strategyName];
            if (!strategy || !strategy.calculate || typeof strategy.calculate !== 'function') {
                console.error(`未知或无效的折扣策略: ${strategyName}. 使用默认无折扣策略.`);
                this.currentStrategy = discountStrategies.none;
                this.discountValue = null;
                return;
            }
            this.currentStrategy = strategy;
            this.discountValue = discountValue;
        }
    
        // 执行计算的方法
        calculateFinalPrice() {
            // 将计算委托给当前设置的策略
            return this.currentStrategy.calculate(
                this.initialAmount,
                this.discountValue,
                this.itemPrice,
                this.quantity
            );
        }
    }

使用示例:

console.log("n--- 策略模式计算方式 ---");

const productPrice = 100;
const productQuantity = 2;
const calculator = new PriceCalculator(productPrice, productQuantity);

console.log(`商品单价: ${productPrice}, 数量: ${productQuantity}, 初始总价: ${calculator.initialAmount}`);

// 1. 应用无折扣
calculator.setStrategy('none');
console.log("最终价格 (无折扣):", calculator.calculateFinalPrice()); // 200

// 2. 应用百分比折扣 (10% off)
calculator.setStrategy('percentage', 0.1);
console.log("最终价格 (10% 折扣):", calculator.calculateFinalPrice()); // 180

// 3. 应用固定金额折扣 (减20元)
calculator.setStrategy('fixedAmount', 20);
console.log("最终价格 (减20元):", calculator.calculateFinalPrice()); // 180

// 4. 应用满减折扣 (满150减30)
calculator.setStrategy('fullReduction', { full: 150, reduction: 30 });
console.log("最终价格 (满150减30):", calculator.calculateFinalPrice()); // 170 (初始200,满足150,减30)

// 5. 应用买X送Y折扣 (买1送1)
const calculatorBuyXGetY = new PriceCalculator(100, 3); // 购买3件,单价100,初始300
console.log(`n商品单价: ${calculatorBuyXGetY.itemPrice}, 数量: ${calculatorBuyXGetY.quantity}, 初始总价: ${calculatorBuyXGetY.initialAmount}`);
calculatorBuyXGetY.setStrategy('buyXGetYFree', 1); // 买1送1
console.log("最终价格 (买1送1,购买3件):", calculatorBuyXGetY.calculateFinalPrice()); // 200 (支付2件价格)

const calculatorBuyXGetY_2 = new PriceCalculator(100, 4); // 购买4件,单价100,初始400
console.log(`n商品单价: ${calculatorBuyXGetY_2.itemPrice}, 数量: ${calculatorBuyXGetY_2.quantity}, 初始总价: ${calculatorBuyXGetY_2.initialAmount}`);
calculatorBuyXGetY_2.setStrategy('buyXGetYFree', 1); // 买1送1
console.log("最终价格 (买1送1,购买4件):", calculatorBuyXGetY_2.calculateFinalPrice()); // 200 (支付2件价格)

// 6. 应用会员折扣 (白银会员)
const memberCalculator = new PriceCalculator(productPrice, productQuantity); // 初始200
console.log(`n商品单价: ${memberCalculator.itemPrice}, 数量: ${memberCalculator.quantity}, 初始总价: ${memberCalculator.initialAmount}`);
memberCalculator.setStrategy('memberDiscount', 'silver');
console.log("最终价格 (白银会员):", memberCalculator.calculateFinalPrice()); // 180

// 动态切换策略
console.log("n--- 动态切换策略 ---");
const dynamicCalculator = new PriceCalculator(productPrice, productQuantity);
dynamicCalculator.setStrategy('percentage', 0.2); // 先设置20%折扣
console.log("当前价格 (20%折扣):", dynamicCalculator.calculateFinalPrice()); // 160

dynamicCalculator.setStrategy('fixedAmount', 50); // 运行时切换为减50元
console.log("切换后价格 (减50元):", dynamicCalculator.calculateFinalPrice()); // 150

通过这个例子,我们可以清晰地看到策略模式的优势:

  • 分离关注点: 每种折扣算法都独立封装在一个策略对象中,PriceCalculator 上下文只负责调用当前策略,而不关心其内部实现。
  • 易于扩展: 如果需要添加新的折扣类型(例如“第二件半价”),我们只需要创建一个新的策略对象并实现其 calculate 方法,无需修改 PriceCalculator 类。
  • 消除条件语句: PriceCalculator 内部不再有复杂的 if/else if/else 结构来判断折扣类型。
  • 可测试性: 每个折扣策略都可以独立进行单元测试。

3. 进阶应用场景

策略模式的应用范围非常广泛,凡是涉及根据不同条件执行不同行为的场景,都可以考虑使用它。

3.1 数据验证

在表单提交或数据处理中,我们经常需要对数据进行多种验证(必填、最小长度、邮箱格式、数字范围等)。

传统方式:

function validateUser(user) {
    if (!user.name) return "Name is required.";
    if (user.name.length < 3) return "Name must be at least 3 characters.";
    if (!user.email) return "Email is required.";
    if (!/S+@S+.S+/.test(user.email)) return "Invalid email format.";
    if (user.age && (user.age < 18 || user.age > 99)) return "Age must be between 18 and 99.";
    return null; // No errors
}

策略模式实现:

// 3.1.1 定义验证策略
const validationStrategies = {
    isNonEmpty: {
        validate: (value) => value !== null && value.trim() !== '',
        message: '不能为空。'
    },
    minLength: {
        validate: (value, min) => value.length >= min,
        message: (min) => `长度不能少于 ${min} 个字符。`
    },
    isEmail: {
        validate: (value) => /S+@S+.S+/.test(value),
        message: '邮箱格式不正确。'
    },
    isNumber: {
        validate: (value) => !isNaN(Number(value)) && isFinite(value),
        message: '必须是数字。'
    },
    inRange: {
        validate: (value, min, max) => Number(value) >= min && Number(value) <= max,
        message: (min, max) => `必须在 ${min} 到 ${max} 之间。`
    }
};

// 3.1.2 定义上下文 (Validator)
class Validator {
    constructor() {
        this.rules = {}; // 存储每个字段的验证规则
    }

    // 添加验证规则
    // fieldName: 字段名称 (e.g., 'name', 'email')
    // strategyName: 策略名称 (e.g., 'isNonEmpty', 'minLength')
    // args: 传递给策略的额外参数 (e.g., minLength 的 '3')
    addRule(fieldName, strategyName, ...args) {
        if (!this.rules[fieldName]) {
            this.rules[fieldName] = [];
        }
        const strategy = validationStrategies[strategyName];
        if (!strategy || typeof strategy.validate !== 'function') {
            console.warn(`未知或无效的验证策略: ${strategyName} for field ${fieldName}`);
            return;
        }
        this.rules[fieldName].push({ strategy, args });
    }

    // 验证数据
    validate(data) {
        const errors = {};
        for (const fieldName in this.rules) {
            const fieldRules = this.rules[fieldName];
            const value = data[fieldName];
            for (const { strategy, args } of fieldRules) {
                if (!strategy.validate(value, ...args)) {
                    let errorMessage = strategy.message;
                    if (typeof errorMessage === 'function') {
                        errorMessage = errorMessage(...args);
                    }
                    errors[fieldName] = (errors[fieldName] || []).concat(`${fieldName}${errorMessage}`);
                    // 可以在这里选择是收集所有错误还是遇到第一个错误就停止
                    // break; // 如果只想显示每个字段的第一个错误
                }
            }
        }
        return Object.keys(errors).length === 0 ? null : errors;
    }
}

// 使用示例
console.log("n--- 数据验证 (策略模式) ---");
const userValidator = new Validator();
userValidator.addRule('username', 'isNonEmpty');
userValidator.addRule('username', 'minLength', 5);
userValidator.addRule('email', 'isNonEmpty');
userValidator.addRule('email', 'isEmail');
userValidator.addRule('age', 'isNumber');
userValidator.addRule('age', 'inRange', 18, 60);

const userData1 = {
    username: 'john_doe',
    email: '[email protected]',
    age: 30
};
const errors1 = userValidator.validate(userData1);
console.log("UserData1 验证结果:", errors1); // null (无错误)

const userData2 = {
    username: 'jo', // Too short
    email: 'invalid-email', // Invalid format
    age: 15 // Out of range
};
const errors2 = userValidator.validate(userData2);
console.log("UserData2 验证结果:", errors2);
/*
Output for userData2:
{
  username: [ 'username长度不能少于 5 个字符。' ],
  email: [ 'email邮箱格式不正确。' ],
  age: [ 'age必须在 18 到 60 之间。' ]
}
*/

const userData3 = {
    username: 'alice',
    email: '', // Empty
    age: 'abc' // Not a number
};
const errors3 = userValidator.validate(userData3);
console.log("UserData3 验证结果:", errors3);
/*
Output for userData3:
{
  email: [ 'email不能为空。' ],
  age: [ 'age必须是数字。', 'age必须在 18 到 60 之间。' ]
}
*/

这个验证器非常灵活,可以轻松地添加新的验证规则,或者为不同的表单组合不同的验证规则,而无需修改 Validator 类的核心逻辑。

3.2 支付处理

一个电商系统需要支持多种支付方式:信用卡、PayPal、支付宝、微信支付等。每种支付方式的接口和处理流程可能都不同。

策略模式实现:

// 3.2.1 定义支付策略
const paymentStrategies = {
    creditCard: {
        processPayment: (amount, paymentDetails) => {
            console.log(`使用信用卡支付 ${amount} 元`);
            // 模拟信用卡支付逻辑,可能调用第三方API
            if (paymentDetails.cardNumber && paymentDetails.expiry && paymentDetails.cvv) {
                console.log(`信用卡号: **** **** **** ${paymentDetails.cardNumber.slice(-4)}`);
                return { success: true, message: '信用卡支付成功。' };
            }
            return { success: false, message: '信用卡信息不完整。' };
        }
    },
    paypal: {
        processPayment: (amount, paymentDetails) => {
            console.log(`使用PayPal支付 ${amount} 元`);
            // 模拟PayPal支付逻辑
            if (paymentDetails.paypalAccount) {
                console.log(`PayPal账户: ${paymentDetails.paypalAccount}`);
                return { success: true, message: 'PayPal支付成功。' };
            }
            return { success: false, message: 'PayPal账户信息缺失。' };
        }
    },
    alipay: {
        processPayment: (amount, paymentDetails) => {
            console.log(`使用支付宝支付 ${amount} 元`);
            // 模拟支付宝支付逻辑
            if (paymentDetails.alipayId) {
                console.log(`支付宝ID: ${paymentDetails.alipayId}`);
                // 实际会生成二维码或跳转到支付宝APP
                return { success: true, message: '支付宝支付待扫码确认。' };
            }
            return { success: false, message: '支付宝ID缺失。' };
        }
    },
    wechatPay: {
        processPayment: (amount, paymentDetails) => {
            console.log(`使用微信支付 ${amount} 元`);
            // 模拟微信支付逻辑
            if (paymentDetails.wechatOpenId) {
                console.log(`微信OpenID: ${paymentDetails.wechatOpenId}`);
                // 实际会生成二维码或跳转到微信APP
                return { success: true, message: '微信支付待确认。' };
            }
            return { success: false, message: '微信OpenID缺失。' };
        }
    }
};

// 3.2.2 定义上下文 (PaymentProcessor)
class PaymentProcessor {
    constructor() {
        this.currentStrategy = null;
    }

    setPaymentStrategy(strategyName) {
        const strategy = paymentStrategies[strategyName];
        if (!strategy || typeof strategy.processPayment !== 'function') {
            throw new Error(`未知或无效的支付策略: ${strategyName}`);
        }
        this.currentStrategy = strategy;
    }

    executePayment(amount, paymentDetails) {
        if (!this.currentStrategy) {
            throw new Error('未设置支付策略。');
        }
        return this.currentStrategy.processPayment(amount, paymentDetails);
    }
}

// 使用示例
console.log("n--- 支付处理 (策略模式) ---");
const processor = new PaymentProcessor();
const orderAmount = 250.75;

// 使用信用卡支付
processor.setPaymentStrategy('creditCard');
const ccResult = processor.executePayment(orderAmount, {
    cardNumber: '1234567890123456',
    expiry: '12/25',
    cvv: '123'
});
console.log("信用卡支付结果:", ccResult);

// 使用PayPal支付
processor.setPaymentStrategy('paypal');
const paypalResult = processor.executePayment(orderAmount, {
    paypalAccount: '[email protected]'
});
console.log("PayPal支付结果:", paypalResult);

// 使用支付宝支付
processor.setPaymentStrategy('alipay');
const alipayResult = processor.executePayment(orderAmount, {
    alipayId: 'alipay_user_123'
});
console.log("支付宝支付结果:", alipayResult);

// 尝试使用不存在的策略
try {
    processor.setPaymentStrategy('bankTransfer');
} catch (error) {
    console.error("设置支付策略失败:", error.message);
}

这个例子将每种支付方式的复杂逻辑封装在各自的策略中,PaymentProcessor 只负责接收请求并转发给当前的策略,极大地简化了支付系统的扩展和维护。

3.3 日志记录

一个应用可能需要在不同的环境中以不同的方式记录日志:开发环境输出到控制台,生产环境写入文件或发送到远程日志服务。

// 3.3.1 定义日志策略
const loggerStrategies = {
    consoleLogger: {
        log: (level, message, timestamp = new Date().toISOString()) => {
            console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}`);
        },
        error: (message, timestamp = new Date().toISOString()) => {
            console.error(`[${timestamp}] [ERROR] ${message}`);
        },
        warn: (message, timestamp = new Date().toISOString()) => {
            console.warn(`[${timestamp}] [WARN] ${message}`);
        }
    },
    // 模拟文件日志,实际可能需要Node.js的fs模块
    fileLogger: {
        log: (level, message, timestamp = new Date().toISOString()) => {
            const logEntry = `[${timestamp}] [${level.toUpperCase()}] ${message}n`;
            // fs.appendFileSync('app.log', logEntry); // 生产环境实际的文件写入
            console.log(`[FILE_LOG] ${logEntry.trim()}`); // 模拟输出到控制台
        },
        error: (message, timestamp = new Date().toISOString()) => {
            const logEntry = `[${timestamp}] [ERROR] ${message}n`;
            // fs.appendFileSync('error.log', logEntry);
            console.error(`[FILE_ERROR_LOG] ${logEntry.trim()}`);
        },
        warn: (message, timestamp = new Date().toISOString()) => {
            const logEntry = `[${timestamp}] [WARN] ${message}n`;
            // fs.appendFileSync('app.log', logEntry);
            console.warn(`[FILE_WARN_LOG] ${logEntry.trim()}`);
        }
    },
    // 模拟远程日志服务,实际可能发送HTTP请求
    remoteLogger: {
        log: (level, message, timestamp = new Date().toISOString()) => {
            const logData = { level, message, timestamp, source: 'webapp' };
            // fetch('/api/log', { method: 'POST', body: JSON.stringify(logData) });
            console.log(`[REMOTE_LOG_SENT] ${JSON.stringify(logData)}`);
        },
        error: (message, timestamp = new Date().toISOString()) => {
            const logData = { level: 'error', message, timestamp, source: 'webapp' };
            // fetch('/api/log', { method: 'POST', body: JSON.stringify(logData) });
            console.error(`[REMOTE_ERROR_SENT] ${JSON.stringify(logData)}`);
        },
        warn: (message, timestamp = new Date().toISOString()) => {
            const logData = { level: 'warn', message, timestamp, source: 'webapp' };
            // fetch('/api/log', { method: 'POST', body: JSON.stringify(logData) });
            console.warn(`[REMOTE_WARN_SENT] ${JSON.stringify(logData)}`);
        }
    }
};

// 3.3.2 定义上下文 (Logger)
class Logger {
    constructor(initialStrategyName = 'consoleLogger') {
        this.setStrategy(initialStrategyName);
    }

    setStrategy(strategyName) {
        const strategy = loggerStrategies[strategyName];
        if (!strategy || typeof strategy.log !== 'function') {
            console.warn(`未知或无效的日志策略: ${strategyName}. 切换回控制台日志.`);
            this.currentStrategy = loggerStrategies.consoleLogger;
            return;
        }
        this.currentStrategy = strategy;
        console.log(`日志策略已切换为: ${strategyName}`);
    }

    log(message) {
        this.currentStrategy.log('info', message);
    }

    error(message) {
        this.currentStrategy.error(message);
    }

    warn(message) {
        this.currentStrategy.warn(message);
    }
}

// 使用示例
console.log("n--- 日志记录 (策略模式) ---");
const appLogger = new Logger();

appLogger.log("应用程序启动成功。");
appLogger.warn("检测到潜在问题。");
appLogger.error("关键服务连接失败!");

appLogger.setStrategy('fileLogger'); // 运行时切换日志策略
appLogger.log("用户数据已保存。");
appLogger.error("数据库写入失败!");

appLogger.setStrategy('remoteLogger');
appLogger.log("前端事件捕获。");

通过策略模式,我们可以轻松地在开发、测试和生产环境中切换不同的日志记录行为,而无需修改应用程序中调用日志记录的代码。

3.4 排序算法

需要对数据集合进行不同方式的排序(升序、降序、按特定属性排序)。

// 3.4.1 定义排序策略
const sortStrategies = {
    // 默认升序
    asc: {
        sort: (arr) => [...arr].sort((a, b) => a - b)
    },
    // 降序
    desc: {
        sort: (arr) => [...arr].sort((a, b) => b - a)
    },
    // 按指定属性升序
    byPropertyAsc: {
        sort: (arr, prop) => [...arr].sort((a, b) => {
            if (a[prop] < b[prop]) return -1;
            if (a[prop] > b[prop]) return 1;
            return 0;
        })
    },
    // 按指定属性降序
    byPropertyDesc: {
        sort: (arr, prop) => [...arr].sort((a, b) => {
            if (a[prop] > b[prop]) return -1;
            if (a[prop] < b[prop]) return 1;
            return 0;
        })
    }
};

// 3.4.2 定义上下文 (Sorter)
class Sorter {
    constructor(initialStrategyName = 'asc') {
        this.setStrategy(initialStrategyName);
    }

    setStrategy(strategyName) {
        const strategy = sortStrategies[strategyName];
        if (!strategy || typeof strategy.sort !== 'function') {
            console.warn(`未知或无效的排序策略: ${strategyName}. 切换回默认升序.`);
            this.currentStrategy = sortStrategies.asc;
            return;
        }
        this.currentStrategy = strategy;
    }

    executeSort(arr, ...args) {
        return this.currentStrategy.sort(arr, ...args);
    }
}

// 使用示例
console.log("n--- 排序算法 (策略模式) ---");
const numbers = [5, 2, 8, 1, 9, 3];
const objects = [
    { name: 'Alice', age: 30 },
    { name: 'Bob', age: 25 },
    { name: 'Charlie', age: 35 }
];

const sorter = new Sorter();

// 默认升序
console.log("原始数字数组:", numbers);
console.log("升序排序:", sorter.executeSort(numbers)); // [1, 2, 3, 5, 8, 9]

// 降序
sorter.setStrategy('desc');
console.log("降序排序:", sorter.executeSort(numbers)); // [9, 8, 5, 3, 2, 1]

// 按年龄升序
console.log("n原始对象数组:", objects);
sorter.setStrategy('byPropertyAsc');
console.log("按年龄升序:", sorter.executeSort(objects, 'age'));
// [{ name: 'Bob', age: 25 }, { name: 'Alice', age: 30 }, { name: 'Charlie', age: 35 }]

// 按年龄降序
sorter.setStrategy('byPropertyDesc');
console.log("按年龄降序:", sorter.executeSort(objects, 'age'));
// [{ name: 'Charlie', age: 35 }, { name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }]

// 尝试使用不存在的策略
sorter.setStrategy('randomSort'); // 会警告并切换回默认升序
console.log("尝试随机排序 (回退到升序):", sorter.executeSort(numbers));

这个例子展示了如何将不同的排序逻辑封装为策略,使得 Sorter 上下文可以灵活地处理各种排序需求。

4. 策略模式的优势与考量

4.1 策略模式的显著优势

  • 符合开放/封闭原则 (Open/Closed Principle – OCP):

    • 对扩展开放:可以轻松添加新的策略,而无需修改现有代码。
    • 对修改封闭:上下文类无需因为新策略的引入而修改。这是策略模式最核心的优势之一。
  • 符合单一职责原则 (Single Responsibility Principle – SRP):

    • 每个策略只负责一个算法。
    • 上下文只负责管理策略的切换和执行,不包含具体的算法逻辑。
    • 职责分离使得代码更清晰、更易于理解和维护。
  • 消除大量的条件语句:

    • 避免了 if/else if/elseswitch 语句块的膨胀,使代码结构更扁平。
  • 提高代码的复用性:

    • 策略可以被不同的上下文或在不同的场景中复用。
  • 增强可测试性:

    • 每个策略都是独立的单元,可以独立进行单元测试,从而更容易发现和修复问题。
  • 运行时动态切换行为:

    • 客户端可以在运行时根据需要选择或改变上下文的行为。
  • 提升代码可读性:

    • 通过将复杂的逻辑分解为更小、更专注的策略,代码意图更加清晰。

4.2 策略模式的潜在劣势与考量

  • 增加对象数量:

    • 每增加一个算法,就需要增加一个对应的策略对象。当算法数量非常多时,可能会导致类的数量剧增,增加项目的复杂性。
  • 客户端需要了解所有策略:

    • 客户端通常需要知道所有可用的具体策略,以便选择合适的策略传递给上下文。这可以通过结合工厂模式来缓解,让工厂负责创建和提供策略对象。
  • 过度设计 (Over-engineering) 的风险:

    • 如果算法逻辑非常简单,或者预计未来不会有太多变化,引入策略模式可能会增加不必要的复杂性和样板代码。在这种情况下,简单的条件语句可能更直接。
    • 在考虑使用策略模式时,应评估算法变化的可能性和复杂性。
  • 性能开销(微乎其微):

    • 额外的函数调用和对象创建可能会带来微小的性能开销,但在绝大多数现代应用中,这种开销几乎可以忽略不计。

5. 策略模式与其他设计模式的关系

策略模式并不是孤立存在的,它经常与其他设计模式结合使用,以发挥更大的作用。

5.1 策略模式与工厂模式 (Factory Pattern)

  • 结合点: 客户端通常需要知道所有具体策略的名称才能选择它们。当策略的数量很大或者策略的创建逻辑比较复杂时,这会成为一个负担。
  • 解决方案: 可以使用工厂模式来封装策略对象的创建过程。客户端不再直接创建策略对象,而是通过工厂来获取。工厂可以根据传入的参数(如策略类型字符串)返回对应的具体策略实例。
  • 好处: 进一步解耦了客户端和具体策略,客户端只需要知道工厂的接口和策略的标识符,而无需关心具体策略类的名称和创建细节。

示例:结合工厂模式的折扣计算器

// 5.1.1 定义策略(同上)
const discountStrategiesFactory = {
    none: {
        calculate: (initialAmount, discountValue, itemPrice, quantity) => {
            console.log("工厂策略:无折扣");
            return initialAmount;
        }
    },
    percentage: {
        calculate: (initialAmount, discountPercentage, itemPrice, quantity) => {
            console.log(`工厂策略:百分比折扣 ${discountPercentage * 100}%`);
            return initialAmount * (1 - discountPercentage);
        }
    },
    fixedAmount: {
        calculate: (initialAmount, fixedDiscountAmount, itemPrice, quantity) => {
            console.log(`工厂策略:固定金额折扣 减${fixedDiscountAmount}元`);
            let finalPrice = initialAmount - fixedDiscountAmount;
            return finalPrice < 0 ? 0 : finalPrice;
        }
    }
    // ... 其他策略
};

// 5.1.2 策略工厂
class DiscountStrategyFactory {
    static getStrategy(strategyName) {
        const strategy = discountStrategiesFactory[strategyName];
        if (!strategy || typeof strategy.calculate !== 'function') {
            console.warn(`工厂模式:未知或无效的折扣策略: ${strategyName}. 返回默认无折扣策略.`);
            return discountStrategiesFactory.none;
        }
        return strategy;
    }
}

// 5.1.3 上下文(与之前相同,但设置策略时通过工厂获取)
class PriceCalculatorWithFactory {
    constructor(itemPrice, quantity) {
        this.itemPrice = itemPrice;
        this.quantity = quantity;
        this.initialAmount = itemPrice * quantity;
        this.currentStrategy = DiscountStrategyFactory.getStrategy('none'); // 默认策略
        this.discountValue = null;
    }

    setStrategy(strategyName, discountValue = null) {
        this.currentStrategy = DiscountStrategyFactory.getStrategy(strategyName);
        this.discountValue = discountValue;
    }

    calculateFinalPrice() {
        return this.currentStrategy.calculate(
            this.initialAmount,
            this.discountValue,
            this.itemPrice,
            this.quantity
        );
    }
}

// 使用示例
console.log("n--- 策略模式与工厂模式结合 ---");
const productPriceF = 120;
const productQuantityF = 3;
const calculatorF = new PriceCalculatorWithFactory(productPriceF, productQuantityF);
console.log(`初始总价: ${calculatorF.initialAmount}`); // 360

calculatorF.setStrategy('percentage', 0.15); // 15% off
console.log("最终价格 (15% 折扣):", calculatorF.calculateFinalPrice()); // 360 * 0.85 = 306

calculatorF.setStrategy('fixedAmount', 50); // 减50元
console.log("最终价格 (减50元):", calculatorF.calculateFinalPrice()); // 360 - 50 = 310

calculatorF.setStrategy('unknownStrategy'); // 使用不存在的策略
console.log("最终价格 (未知策略,回退到无折扣):", calculatorF.calculateFinalPrice()); // 360

现在,PriceCalculatorWithFactorysetStrategy 方法不再直接访问 discountStrategies 对象,而是委托给 DiscountStrategyFactory,从而将策略的创建/获取逻辑进一步抽象。

5.2 策略模式与模板方法模式 (Template Method Pattern)

  • 区别与联系:
    • 策略模式: 关注 “做什么” (封装不同的算法)。它通过将整个算法替换掉来改变行为。
    • 模板方法模式: 关注 “如何做” (定义算法的骨架,将某些步骤延迟到子类)。它通过允许子类覆盖特定步骤来改变行为,但算法的总体结构保持不变。
  • 结合使用: 如果不同的策略中存在一些共同的、固定的步骤,而只有少数步骤是可变的,那么可以在策略内部使用模板方法模式。例如,所有支付策略可能都有“验证凭证”和“记录交易日志”的固定步骤,但“实际扣款”步骤则各不相同。此时,可以定义一个抽象支付策略,其中包含模板方法,具体的支付策略继承并实现其可变步骤。

6. JavaScript 中的实现变体

在 JavaScript 中,策略模式的实现可以非常灵活,不拘泥于传统的面向对象类结构。

6.1 使用纯函数作为策略

这是最简洁也最“JavaScript-native”的方式。每个策略就是一个函数。

// 策略集合
const mathOperations = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b,
    multiply: (a, b) => a * b,
    divide: (a, b) => {
        if (b === 0) throw new Error("Cannot divide by zero.");
        return a / b;
    }
};

// 上下文
class Calculator {
    constructor() {
        this.currentOperation = mathOperations.add; // 默认加法
    }

    setOperation(operationName) {
        const operation = mathOperations[operationName];
        if (typeof operation !== 'function') {
            throw new Error(`Invalid operation: ${operationName}`);
        }
        this.currentOperation = operation;
    }

    execute(a, b) {
        return this.currentOperation(a, b);
    }
}

// 使用
console.log("n--- JS纯函数策略 ---");
const calc = new Calculator();
console.log("默认加法 (5 + 3):", calc.execute(5, 3)); // 8

calc.setOperation('multiply');
console.log("乘法 (5 * 3):", calc.execute(5, 3)); // 15

calc.setOperation('divide');
console.log("除法 (10 / 2):", calc.execute(10, 2)); // 5

这种方式特别适合策略逻辑简单、不需要维护内部状态的情况。

6.2 使用 ES6 Class 作为策略

当策略需要维护一些内部状态,或者需要更复杂的初始化逻辑时,使用 ES6 Class 是一个不错的选择。

// 策略接口 (隐式)
// class DiscountStrategy {
//     calculate(initialAmount, discountValue, itemPrice, quantity) {
//         throw new Error("Method 'calculate()' must be implemented.");
//     }
// }

// 具体策略类
class NoDiscountStrategy {
    calculate(initialAmount) {
        console.log("Class策略:无折扣");
        return initialAmount;
    }
}

class PercentageDiscountStrategy {
    constructor(percentage) {
        this.percentage = percentage;
    }
    calculate(initialAmount) {
        console.log(`Class策略:百分比折扣 ${this.percentage * 100}%`);
        return initialAmount * (1 - this.percentage);
    }
}

class FixedAmountDiscountStrategy {
    constructor(amount) {
        this.amount = amount;
    }
    calculate(initialAmount) {
        console.log(`Class策略:固定金额折扣 减${this.amount}元`);
        let finalPrice = initialAmount - this.amount;
        return finalPrice < 0 ? 0 : finalPrice;
    }
}

// 策略映射(结合工厂模式)
const classDiscountStrategies = {
    none: NoDiscountStrategy,
    percentage: PercentageDiscountStrategy,
    fixedAmount: FixedAmountDiscountStrategy
};

// 上下文
class PriceCalculatorWithClassStrategy {
    constructor(itemPrice, quantity) {
        this.itemPrice = itemPrice;
        this.quantity = quantity;
        this.initialAmount = itemPrice * quantity;
        this.currentStrategy = new NoDiscountStrategy(); // 默认策略实例
    }

    setStrategy(strategyName, ...args) {
        const StrategyClass = classDiscountStrategies[strategyName];
        if (!StrategyClass) {
            console.warn(`Class策略:未知或无效的折扣策略: ${strategyName}. 使用默认无折扣策略.`);
            this.currentStrategy = new NoDiscountStrategy();
            return;
        }
        this.currentStrategy = new StrategyClass(...args); // 实例化策略
    }

    calculateFinalPrice() {
        return this.currentStrategy.calculate(this.initialAmount);
    }
}

// 使用
console.log("n--- JS Class策略 ---");
const productPriceC = 80;
const productQuantityC = 4; // 初始总价 320
const calculatorC = new PriceCalculatorWithClassStrategy(productPriceC, productQuantityC);

calculatorC.setStrategy('none');
console.log("最终价格 (无折扣):", calculatorC.calculateFinalPrice()); // 320

calculatorC.setStrategy('percentage', 0.2); // 20% off
console.log("最终价格 (20% 折扣):", calculatorC.calculateFinalPrice()); // 256

calculatorC.setStrategy('fixedAmount', 70); // 减70元
console.log("最终价格 (减70元):", calculatorC.calculateFinalPrice()); // 250

这种方式提供了更强的封装性和可维护性,特别是在策略逻辑复杂或需要内部状态时。

6.3 使用 ES6 Modules 组织策略

随着项目规模的增长,将每个策略放在一个独立的模块文件中是最佳实践,可以提高代码的可维护性和可复用性。


// discount-strategies/NoDiscountStrategy.js
/*
export class NoDiscountStrategy {
    calculate(initialAmount) {
        console.log("模块策略:无折扣");
        return initialAmount;
    }
}
*/

// discount-strategies/PercentageDiscountStrategy.js
/*
export class PercentageDiscountStrategy {
    constructor(percentage) {
        this.percentage = percentage;
    }
    calculate(initialAmount) {
        console.log(`模块策略:百分比折扣 ${this.percentage * 100}%`);
        return initialAmount * (1 - this.percentage);
    }
}
*/

// discount-strategies/index.js (策略集合和导出)
/*
import { NoDiscountStrategy } from './NoDiscountStrategy.js';
import { PercentageDiscountStrategy } from './PercentageDiscountStrategy.js';
// ... 导入其他策略

export const discountStrategiesModule = {
    none: NoDiscountStrategy,
    percentage: PercentageDiscountStrategy,
    // ...
};
*/

// main.js (使用)
/*
import { discountStrategiesModule } from './discount-strategies/index.js';

class PriceCalculatorWithModules {
    // ... 构造函数和方法同上,但在 setStrategy 中使用 discountStrategiesModule
    setStrategy(strategyName, ...args) {
        const StrategyClass = discountStrategiesModule[strategyName];
        if (!StrategyClass) {
            this.currentStrategy = new discountStrategiesModule.none();
            return;
        }
        this.currentStrategy = new StrategyClass(...args);
    }
    // ...
}
*/

通过模块化,每个策略文件职责单一,易于查找、修改和测试。`index.js` 文件作为策略的入口,方便上下文统一导入和使用。

### 7. 结语

策略模式是设计模式中的基石之一,它通过将算法从其使用上下文中分离出来,提供了一种优雅的、可替换的算法逻辑实现方式。无论是在前端的交互逻辑、数据验证,还是在后端的业务规则、支付处理、日志记录等场景,策略模式都能显著提升代码的灵活性、可维护性和可扩展性。掌握并熟练运用策略模式,将使您的 JavaScript 代码更加健壮、更具弹性,更好地应对不断变化的需求。在实践中,我们应根据具体场景和策略的复杂程度,灵活选择使用纯函数、对象字面量或 ES6 Class 来实现策略,并可以结合工厂模式等其他模式来进一步优化设计。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注