编程专家讲座:策略模式(Strategy Pattern)在 JS 中的应用:实现可替换的算法逻辑
各位同仁,大家好!
今天,我们将深入探讨一个在软件设计中极为强大且实用的设计模式——策略模式(Strategy Pattern),并着重讲解它在 JavaScript 世界中的应用。随着前端和后端 JavaScript 应用的日益复杂,我们经常面临需要根据不同条件或配置执行不同算法的场景。在这种情况下,如果一味地使用大量的 if/else 或 switch 语句来控制逻辑,代码将变得难以阅读、难以维护,并且违反了开放/封闭原则。策略模式正是解决这类问题的利器,它允许我们在运行时动态地替换算法,从而实现可插拔的逻辑。
1. 策略模式概述:解耦算法与上下文
1.1 什么是策略模式?
策略模式属于行为型设计模式,其核心思想是定义一系列算法,将每一个算法封装起来,并使它们可以相互替换。策略模式让算法独立于使用它的客户端而变化。
简单来说,当您有一个类,它需要根据某些条件执行不同的行为时,您可以将这些行为(算法)抽象出来,分别封装到独立的“策略”对象中。然后,这个类(我们称之为“上下文”)不再直接实现这些行为,而是持有一个对某个策略对象的引用,并将具体的行为委托给这个策略对象来执行。这样,上下文就可以在运行时根据需要切换不同的策略,而无需修改自身的代码。
1.2 为什么我们需要策略模式?
考虑一个常见的场景:一个电商平台需要根据用户的会员等级、促销活动或支付方式来计算最终价格。如果直接在订单处理逻辑中堆砌大量的 if...else if...else 或 switch 语句来判断并执行不同的计算规则,代码会迅速膨胀,变得脆弱。
- 代码难以维护: 每次新增或修改一个计算规则,都需要修改核心的订单处理逻辑。
- 违反开放/封闭原则: 核心逻辑对扩展是关闭的(不能在不修改其代码的情况下添加新功能),对修改是开放的(每次新增规则都需要修改)。
- 代码冗余: 不同的计算规则可能包含一些相似的逻辑,但因为纠缠在一起,难以复用。
- 测试复杂: 难以对单一的计算规则进行独立测试,必须通过整个订单处理流程来验证。
策略模式正是为了解决这些问题而生。它将算法的定义、选择和使用过程分离,让代码更加模块化、可维护和可扩展。
1.3 策略模式的核心组件
策略模式通常包含以下三个核心组件:
-
策略(Strategy)接口/抽象策略:
- 定义了一个公共接口,所有具体的策略类都必须实现这个接口。
- 这个接口声明了上下文类用于调用具体策略的方法。
- 在 JavaScript 中,这通常是一个隐式的契约(即一组函数签名),或者通过定义一个基类/抽象类来实现。
-
具体策略(Concrete Strategy):
- 实现了策略接口/抽象策略,封装了具体的算法或行为。
- 每个具体策略类都实现了一种特定的算法。
-
上下文(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 基本实现示例:电商价格计算器
让我们通过一个电商场景来具体实现策略模式。假设我们需要为商品计算最终价格,根据不同的促销活动,折扣方式会有所不同:
- 无折扣
- 百分比折扣
- 固定金额折扣
- 买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 函数内部包含了大量的条件判断。如果未来需要增加新的折扣类型(如满减、阶梯折扣),这个函数将不得不继续膨胀,变得越来越难以管理。
解决方案:使用策略模式
-
定义策略接口(隐式):
在 JavaScript 中,我们可以约定所有折扣策略都必须实现一个calculate(initialAmount, discountValue, itemPrice, quantity)方法(或函数),该方法接收初始金额、折扣值、单价和数量,并返回最终金额。 -
具体策略(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); } } }; -
上下文(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/else或switch语句块的膨胀,使代码结构更扁平。
- 避免了
-
提高代码的复用性:
- 策略可以被不同的上下文或在不同的场景中复用。
-
增强可测试性:
- 每个策略都是独立的单元,可以独立进行单元测试,从而更容易发现和修复问题。
-
运行时动态切换行为:
- 客户端可以在运行时根据需要选择或改变上下文的行为。
-
提升代码可读性:
- 通过将复杂的逻辑分解为更小、更专注的策略,代码意图更加清晰。
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
现在,PriceCalculatorWithFactory 的 setStrategy 方法不再直接访问 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 来实现策略,并可以结合工厂模式等其他模式来进一步优化设计。