1. 从if-else地狱到策略模式的蜕变
记得三年前我刚接手某电商平台促销系统时,面对的第一个需求就是用户等级折扣计算。当时我毫不犹豫地写下了这样的代码:
javascript复制function calculatePrice(userType, price) {
if (userType === 'NORMAL') {
return price;
} else if (userType === 'VIP') {
return price * 0.9;
} else if (userType === 'SVIP') {
return price * 0.8;
}
return price;
}
看起来简单明了,对吧?但随着业务发展,这个函数逐渐变成了一个300多行的庞然大物,里面塞满了各种嵌套的条件判断:会员等级、节日促销、新人专享、满减活动、限时折扣...每次修改一个规则,我都得小心翼翼地测试其他所有相关逻辑,生怕引发连锁反应。
关键教训:当你的函数开始出现多个处理不同业务逻辑的分支时,就是考虑策略模式的最佳时机。
2. 策略模式的核心思想解析
2.1 什么是策略模式
策略模式(Strategy Pattern)是一种行为设计模式,它定义了一系列算法,将每个算法封装起来,并使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户端。
用更通俗的话说:策略模式就是把你的各种业务规则(算法)装进不同的"小盒子"里,需要哪个就取哪个用,而不是把所有规则都混在一起。
2.2 策略模式的三大要素
- 上下文(Context):维护一个对策略对象的引用,负责将客户端请求委托给当前策略对象。
- 策略接口(Strategy Interface):定义所有支持的算法或行为的公共接口。
- 具体策略(Concrete Strategies):实现策略接口的具体算法类。
在JavaScript中,由于语言的灵活性,我们可以简化这个结构,直接用对象字面量来实现策略模式。
3. JavaScript中的策略模式实践
3.1 基础实现方案
让我们重构之前的折扣计算功能:
javascript复制// 定义所有折扣策略
const discountStrategies = {
NORMAL: price => price,
VIP: price => price * 0.9,
SVIP: price => price * 0.8,
NEW_USER: price => price * 0.7,
FESTIVAL: price => price * 0.85
};
// 上下文函数
function calculatePrice(userType, price) {
const strategy = discountStrategies[userType] || discountStrategies.NORMAL;
return strategy(price);
}
// 使用示例
console.log(calculatePrice('VIP', 100)); // 90
console.log(calculatePrice('NEW_USER', 100)); // 70
3.2 进阶:支持组合策略
实际业务中,我们经常需要组合多种优惠策略。这时可以扩展我们的实现:
javascript复制const discountStrategies = {
// 基础折扣
LEVEL: {
NORMAL: price => price,
VIP: price => price * 0.9,
SVIP: price => price * 0.8
},
// 活动折扣
EVENT: {
NEW_USER: price => price * 0.7,
FESTIVAL: price => price * 0.85
},
// 其他折扣
OTHER: {
COUPON: (price, couponValue) => price - couponValue
}
};
function calculatePrice(options) {
const { userLevel, events = [], couponValue = 0, originalPrice } = options;
// 应用等级折扣
let price = discountStrategies.LEVEL[userLevel](originalPrice);
// 应用活动折扣(取最大折扣)
events.forEach(event => {
price = Math.min(price, discountStrategies.EVENT[event](originalPrice));
});
// 应用优惠券
price = discountStrategies.OTHER.COUPON(price, couponValue);
return Math.max(price, 0); // 确保价格不为负
}
// 使用示例
console.log(calculatePrice({
userLevel: 'VIP',
events: ['FESTIVAL'],
couponValue: 10,
originalPrice: 100
})); // 75 (100*0.9=90, 100*0.85=85, 85-10=75)
4. 策略模式的五大优势
4.1 代码可维护性大幅提升
每个策略都是独立的,修改一个策略不会影响其他策略。当需要新增或修改折扣规则时,你只需要关注特定的策略函数,而不必担心会意外破坏其他业务逻辑。
4.2 业务逻辑一目了然
策略对象就像一份清晰的"价目表",开发者可以一眼看出系统支持哪些折扣规则,每个规则的具体实现是什么。这比在几百行代码中寻找if-else分支要直观得多。
4.3 便于单元测试
每个策略都可以独立测试,不需要构建复杂的测试环境。例如,测试VIP折扣策略只需要验证discountStrategies.LEVEL.VIP(100) === 90即可。
4.4 运行时策略切换
策略模式允许在运行时动态切换算法。例如,我们可以在双十一期间临时替换常规折扣策略为特别促销策略:
javascript复制// 临时添加双十一策略
discountStrategies.EVENT.DOUBLE_11 = price => price * 0.6;
// 活动结束后移除
delete discountStrategies.EVENT.DOUBLE_11;
4.5 更好的代码复用
策略可以被多个上下文共享使用。例如,会员等级折扣策略既可以在购物车中使用,也可以在订单确认页面使用,确保计算逻辑的一致性。
5. 实战中的常见问题与解决方案
5.1 策略选择逻辑复杂怎么办?
当策略选择本身就很复杂时(比如需要根据多个条件组合选择策略),可以考虑引入"策略工厂":
javascript复制class DiscountStrategyFactory {
static getStrategy(user, currentEvent) {
if (currentEvent === 'DOUBLE_11') {
return discountStrategies.EVENT.DOUBLE_11;
}
if (user.isNew) {
return discountStrategies.EVENT.NEW_USER;
}
return discountStrategies.LEVEL[user.level];
}
}
// 使用工厂选择策略
const strategy = DiscountStrategyFactory.getStrategy(user, currentEvent);
const price = strategy(originalPrice);
5.2 策略需要共享数据怎么办?
有时策略需要访问一些共享数据。我们可以通过将上下文传递给策略来实现:
javascript复制const discountStrategies = {
VIP: (price, context) => {
// 可以根据context中的其他信息调整折扣
if (context.orderCount > 10) {
return price * 0.85; // 老VIP客户额外折扣
}
return price * 0.9;
}
// ...其他策略
};
function calculatePrice(userType, price, context = {}) {
const strategy = discountStrategies[userType] || discountStrategies.NORMAL;
return strategy(price, context);
}
5.3 如何管理大量的策略?
当策略数量很多时,可以考虑以下方法:
- 按模块拆分策略对象:如将用户等级策略、促销活动策略、优惠券策略分开管理。
- 使用策略组合:允许同时应用多个策略,但要定义清晰的组合规则(如折扣叠加方式)。
- 策略配置文件:将策略配置外部化,便于非开发人员修改。
6. 策略模式的最佳实践
6.1 何时使用策略模式
- 当你有多个仅在行为上不同的相关类时
- 当一个类有多种行为,且这些行为以多个条件语句的形式出现时
- 当算法需要自由切换,或可能在运行时动态选择时
- 当需要隔离算法实现细节,避免客户端代码依赖具体实现时
6.2 实现建议
- 保持策略接口一致:所有策略应该有相同的签名,便于上下文统一调用。
- 考虑策略的无状态性:尽可能设计无状态的策略,可以共享使用同一个策略实例。
- 合理命名策略:使用业务相关的名称,如
NewUserDiscount比DiscountTypeA更清晰。 - 文档化策略规则:为每个策略添加注释,说明其适用场景和计算规则。
6.3 性能考量
虽然策略模式会引入一些额外的对象和间接调用,但在大多数情况下这种开销可以忽略不计。只有在极端性能敏感的场景下才需要考虑优化,比如:
- 缓存策略实例而非每次创建新实例
- 对于简单的策略,可以使用内联函数而非对象方法
- 避免在策略选择时进行复杂的计算
7. 策略模式与其他模式的关系
7.1 策略模式 vs 工厂模式
工厂模式关注对象的创建,而策略模式关注对象的行为。两者经常结合使用:工厂负责创建适当的策略对象,然后由上下文使用该策略。
7.2 策略模式 vs 状态模式
状态模式和策略模式的结构相似,但意图不同:
- 策略模式中,策略的选择通常由客户端显式指定
- 状态模式中,状态转换通常由状态对象内部管理
7.3 策略模式 vs 命令模式
命令模式将请求封装为对象,支持操作的参数化、队列化和日志化。而策略模式专注于算法的封装和替换。
8. 真实项目中的策略模式应用
8.1 电商促销系统
除了前面讨论的折扣计算,策略模式还可以用于:
- 运费计算策略(普通快递、加急、到店自提等)
- 积分计算策略(不同商品类别有不同的积分规则)
- 支付方式处理(信用卡、支付宝、微信支付等)
8.2 游戏开发
- 角色移动策略(行走、奔跑、潜行等)
- AI行为策略(攻击、防御、逃跑等)
- 伤害计算策略(物理伤害、魔法伤害、真实伤害等)
8.3 表单验证
- 不同字段的验证策略(必填、邮箱格式、手机号格式等)
- 验证规则可以动态组合
javascript复制const validationStrategies = {
required: value => !!value,
email: value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
minLength: (value, length) => value.length >= length
};
function validateField(fieldValue, rules) {
for (const rule of rules) {
const validator = validationStrategies[rule.type];
if (!validator(fieldValue, ...(rule.params || []))) {
return false;
}
}
return true;
}
9. 从策略模式看设计原则
策略模式很好地体现了以下几个面向对象设计原则:
9.1 开闭原则(OCP)
系统对扩展开放(可以添加新策略),对修改关闭(不需要修改现有策略或上下文)。
9.2 单一职责原则(SRP)
每个策略类只负责一个具体的算法或行为,职责单一。
9.3 依赖倒置原则(DIP)
上下文依赖于抽象的策略接口,而不是具体的策略实现。
9.4 组合优于继承
使用对象组合来复用算法行为,而不是通过继承来获得行为。
10. 策略模式的局限性
虽然策略模式有很多优点,但也有其适用场景和局限性:
- 客户端必须了解不同策略:客户端代码需要知道有哪些策略以及它们的区别。
- 策略类可能增多:如果算法很少变化,使用策略模式可能会增加不必要的复杂性。
- 策略与上下文间的通信开销:策略可能需要从上下文获取数据,增加了交互成本。
在实际项目中,我通常会遵循这样的经验法则:当发现自己在反复修改同一个函数的条件分支逻辑时,就是引入策略模式的好时机;而对于那些简单且不太可能变化的逻辑,保持简单的条件语句也未尝不可。