最近在搞一个商城的小程序,运营那边隔三差五就提需求,今天说“老用户推荐新用户,每人送一张满100减20的券”,明天又说“换季大促,全场8折,部分特价商品除外”。一开始我图省事,直接在代码里写if else,结果半年下来,那个负责折扣和推荐的函数膨胀得跟过年囤的腊肉一样,又粗又硬,谁看谁皱眉。后来我学了策略模式,才明白以前那种写法有多“糙”。今天就用特别接地气的方式,聊聊怎么用策略模式把推荐规则和营销折扣这些天天变的业务,收拾得服服帖帖。
一、从一个让人头疼的业务说起
先讲讲那个让我深夜崩溃的订单接口。当时业务方要求:根据用户等级、订单金额、是否有优惠券、是不是会员日,来决定最终折扣。第一版我写得很爽:
// JavaScript 示例:糟糕的条件分支版本
function getFinalPrice(user, order) {
let price = order.price;
// 判断是不是会员日
if (isMemberDay()) {
price = price * 0.9;
}
// 判断用户是不是黄金会员
if (user.level === 'gold') {
price = price * 0.85;
} else if (user.level === 'silver') {
price = price * 0.95;
}
// 如果有优惠券,再用优惠券
if (order.coupon) {
if (order.coupon.type === 'fullReduction') {
if (price >= order.coupon.threshold) {
price = price - order.coupon.amount;
}
} else if (order.coupon.type === 'discount') {
price = price * order.coupon.rate;
}
}
// 如果是推荐来的新用户,再减5块
if (order.fromRecommend && order.isNewUser) {
price = price - 5;
}
return Math.max(price, 0.01);
}
刚开始觉得挺合理,后来需求变了:要把“会员日”改成“品牌日”,品牌日里只有指定品牌参与折扣。我就在 if 里又加了个 else if。再后来,运营说不同渠道的新入口,推荐奖励还不一样,有的减5块,有的送券。我没忍住,加了 switch case。最后这个函数长了60多行,每次测试都要把所有情况比一遍,漏一个就出 bug。最可怕的是,这坨逻辑只能整体跑,没法单独测某一个规则,通通绑死在一起。
1.1 条件分支的噩梦
你肯定也遇到过类似的代码。业务规则一多,if else 就像套娃一样往外长。每加一个新规则,就得翻开原函数,小心翼翼地找地方插一行。改一行,还可能影响别的情况。比如你想加个“双十一全场半价”,结果不小心把“满减”的优先级改了,第二天客服就被用户骂哭。这种代码,专业点叫“逻辑分派混乱”,说白了就是所有规则挤在一口锅里,互相串味。
1.2 为什么需要策略模式
后来我明白了一个道理:规则本身是容易变的,但规则的结构是不变的。所谓策略模式,就是先把“规则”这个抽象帽子戴好,再让各种具体规则各自实现。以后加新规则,不需要碰老代码,只需要多写一个类。就像换手机壳,壳子型号固定,你想换哪个颜色就换哪个,不耽误手机用。策略模式的核心,就是“把一族可以互相替换的算法封装起来,让它们可以独立变化”。
二、用例子一步步走进策略模式
我们依然用 JavaScript 来演示,因为前端、后端只要用 JS 都能跑。先声明一下技术栈:Node.js 18 及以上,纯 JavaScript 实现,不用 TypeScript,也不引入额外库,方便你直接复制跑。
2.1 先搭一个策略骨架
策略模式通常有几个角色:策略接口、具体策略、上下文。JS 里面没有真正的接口,但我们可以用“约定”代替:每个策略对象都实现一个 calc 方法,输入订单信息,输出折扣后的价格或推荐结果。
// JavaScript 示例:策略的约定
// 我们定义折扣策略的统一“合约”:
// 每个策略都必须是一个对象,并且拥有 calc 方法。
// calc 接收订单对象 order,返回新的订单价格。
// 这样,后续新增策略时,只要遵守这个约定就行。
const fullReductionStrategy = {
// 满减策略:满 threshold 减 amount
name: 'fullReduction',
threshold: 100, // 满多少
amount: 20, // 减多少
calc(order) {
if (order.price >= this.threshold) {
return order.price - this.amount;
}
return order.price;
}
};
const discountStrategy = {
// 折扣策略:打 rate 折
name: 'discount',
rate: 0.85, // 8.5折
calc(order) {
return order.price * this.rate;
}
};
// 上下文类:负责执行某个策略
class DiscountContext {
constructor(strategy) {
this.strategy = strategy; // 传入一个策略对象
}
execute(order) {
// 调用策略的 calc 方法,完成计算
return this.strategy.calc(order);
}
}
// 来试试
const order = { price: 200 };
const ctx1 = new DiscountContext(fullReductionStrategy);
console.log('满减后:', ctx1.execute(order)); // 180
const ctx2 = new DiscountContext(discountStrategy);
console.log('折扣后:', ctx2.execute(order)); // 170
这样看起来,好像只是把 if else 拆成了两个对象,感觉更麻烦。但别急,策略模式的威力在“变化多”的时候才会显现。
2.2 推荐规则策略
推荐规则比折扣更复杂。比如“老用户邀请新用户”,系统要决定给老用户什么奖励、给新用户什么奖励。不同时期奖励不一样。我用策略模式来封装推荐计划。
// JavaScript 示例:推荐规则策略
// 定义一个推荐策略的抽象“形状”:必须实现 recommend 方法。
// recommend 接收用户对象和上下文信息,返回推荐结果。
class DefaultRecommend {
// 默认策略:啥也没有
recommend(user, context) {
return { reward: '无', needNotification: false };
}
}
class SpringFestivalRecommend {
// 春节活动:奖励双倍积分
recommend(user, context) {
// 检查用户是否老用户
if (context.isOldUser) {
return {
reward: '600积分',
needNotification: true, // 需要推送通知
};
}
// 新用户送优惠券
return {
reward: '新用户专享券',
needNotification: true,
};
}
}
class NewUserExclusiveRecommend {
// 拉新专场:只针对新用户
recommend(user, context) {
if (context.isNewUser) {
return {
reward: '满50减20券',
needNotification: true,
};
}
return { reward: '无', needNotification: false };
}
}
// 执行推荐的上下文
class RecommendService {
constructor(strategy) {
this.strategy = strategy;
}
executeRecommend(user, context) {
return this.strategy.recommend(user, context);
}
}
// 使用示例:
const spring = new SpringFestivalRecommend();
const service = new RecommendService(spring);
const result = service.executeRecommend({ id: 1 }, { isOldUser: true });
console.log('春节推荐结果:', result);
// 输出:{ reward: '600积分', needNotification: true }
你看,每个推荐活动就是一个策略。以后春节过了要换五一活动,直接换一个策略实例就行,原来的代码一行不动。
2.3 营销折扣策略
回到营销折扣。我们不仅要有满减、折扣,还要学会组合。比如“一件8折,两件7折,三件6折”,这是按数量变化的。再比如“品牌日指定品牌打9折”,这是按商品属性变化的。用策略模式封装起来,每个策略只管自己的逻辑。
// JavaScript 示例:复杂营销折扣策略
// 策略一:阶梯数量折扣
class QuantityDiscountStrategy {
constructor() {
this.name = 'quantityDiscount';
}
calc(order) {
const quantity = order.quantity || 1;
if (quantity >= 3) {
return order.price * 0.6; // 三件以上6折
}
if (quantity === 2) {
return order.price * 0.7; // 两件7折
}
return order.price * 0.8; // 一件8折
}
}
// 策略二:品牌日折扣
class BrandDayDiscountStrategy {
constructor(brandList, rate) {
this.brandList = brandList; // 参与活动的品牌数组,如 ['Nike','Adidas']
this.rate = rate; // 折扣率
}
calc(order) {
if (this.brandList.includes(order.brand)) {
return order.price * this.rate;
}
return order.price;
}
}
// 策略三:会员价
class MemberPriceStrategy {
constructor() {
this.name = 'memberPrice';
}
calc(order) {
if (order.userLevel === 'gold') {
return order.price * 0.85;
}
if (order.userLevel === 'silver') {
return order.price * 0.95;
}
return order.price;
}
}
// 上下文:支持设置多个策略,按顺序叠加
class PromotionEngine {
constructor() {
this.strategies = []; // 策略集合
}
addStrategy(strategy) {
this.strategies.push(strategy); // 添加策略
}
clearStrategies() {
this.strategies = []; // 清空策略
}
execute(order) {
// 注意:这里简化了,实际业务要注意折扣叠加顺序和互斥性
let finalPrice = order.price;
for (const strategy of this.strategies) {
finalPrice = strategy.calc({ ...order, price: finalPrice });
}
return finalPrice;
}
}
// 使用示例:
const engine = new PromotionEngine();
engine.addStrategy(new MemberPriceStrategy());
engine.addStrategy(new BrandDayDiscountStrategy(['Nike', 'Adidas'], 0.9));
engine.addStrategy(new QuantityDiscountStrategy());
const sampleOrder = {
price: 1000,
brand: 'Nike',
userLevel: 'silver',
quantity: 2,
};
const finalPrice = engine.execute(sampleOrder);
console.log('最终价格:', finalPrice);
// 计算过程:会员95折 1000*0.95=950 -> 品牌9折 950*0.9=855 -> 数量7折 855*0.7=598.5
这里的 PromotionEngine 就像一个流水线,把多个策略串起来。每个策略都是独立小零件,坏了一个换个新的,不影响别人。
2.4 把策略串起来
真实项目里,策略往往不是写死在代码里的,而是根据配置文件、数据库或运营后台来动态选择。比如后端有一个接口,前端传一个 promotionCode,后端拿到这个 code,查表找到对应策略。这里我用一个简单策略工厂来演示。
// JavaScript 示例:策略工厂——根据名称得到策略实例
// 所有策略都注册在一个 Map 里
const strategyRegistry = new Map();
// 注册策略的函数
function registerStrategy(name, strategy) {
strategyRegistry.set(name, strategy);
}
// 注册我们上面定义的策略
registerStrategy('fullReduction', {
threshold: 100,
amount: 20,
calc(order) {
if (order.price >= this.threshold) {
return order.price - this.amount;
}
return order.price;
}
});
registerStrategy('discount', {
rate: 0.8,
calc(order) {
return order.price * this.rate;
}
});
registerStrategy('member', {
calc(order) {
return order.userLevel === 'gold' ? order.price * 0.85 : order.price;
}
});
// 从工厂获取策略
function getStrategy(name) {
const strategy = strategyRegistry.get(name);
if (!strategy) {
throw new Error(`未找到策略: ${name}`);
}
return strategy;
}
// 使用示例:
const order = { price: 200, userLevel: 'gold' };
const strategyName = 'member'; // 这个可以从请求参数里来
const currentStrategy = getStrategy(strategyName);
const finalPrice = currentStrategy.calc(order);
console.log('会员策略价格:', finalPrice); // 170
这样,每次新增策略,只要在 registry 里注册一下,其他地方不用改。如果有多个策略组合,也可以注册一个“组合策略”,里面内置多个子策略。
三、策略模式如何落地到日常开发
前面例子已经能说明问题,但真实业务里还有一些细节要注意。
3.1 如何动态切换策略
运营后台经常改奖励规则,你不可能每次改需求都发版。别慌,策略模式可以和配置中心结合。比如把策略参数(满减的阈值、折扣率)放到数据库里,程序启动时读出来,构建成策略对象。以后运营改一下配置,刷新缓存,策略就生效了。
// JavaScript 示例:基于配置构建策略
// 假设我们从数据库查询到一条配置规则
const configFromDB = {
type: 'fullReduction',
threshold: 150,
amount: 30,
};
function createStrategyFromConfig(config) {
switch (config.type) {
case 'fullReduction':
return {
threshold: config.threshold,
amount: config.amount,
calc(order) {
if (order.price >= this.threshold) {
return order.price - this.amount;
}
return order.price;
}
};
case 'discount':
return {
rate: config.rate,
calc(order) {
return order.price * this.rate;
}
};
default:
throw new Error('不支持的策略类型');
}
}
// 使用
const strategy = createStrategyFromConfig(configFromDB);
console.log(strategy.calc({ price: 180 })); // 150
这里工厂里用 switch 只是做一个映射,不会像业务逻辑那样膨胀。每个策略内部依然自治。
3.2 策略的注册与查找
前面用了 Map 来注册。更优雅一点,可以写一个 StrategyFactory 类,负责注册和创建。关联技术叫“简单工厂”或者“工厂模式”。策略模式跟工厂模式经常一起用:工厂负责把你需要的策略“生”出来,策略负责干具体的活。
// JavaScript 示例:策略工厂类
class StrategyFactory {
constructor() {
this.strategies = {}; // 保存策略构造函数的映射
}
register(type, clazz) {
this.strategies[type] = clazz;
}
create(type, options = {}) {
const Clazz = this.strategies[type];
if (!Clazz) {
throw new Error(`没有这种策略: ${type}`);
}
// 实例化并传入配置
return new Clazz(options);
}
}
// 定义策略类
class FullReduction {
constructor(options) {
this.threshold = options.threshold;
this.amount = options.amount;
}
calc(order) {
return order.price >= this.threshold ? order.price - this.amount : order.price;
}
}
class Discount {
constructor(options) {
this.rate = options.rate;
}
calc(order) {
return order.price * this.rate;
}
}
// 初始化工厂并注册
const factory = new StrategyFactory();
factory.register('fullReduction', FullReduction);
factory.register('discount', Discount);
// 运行时从请求中拿到类型和参数
const requestType = 'fullReduction';
const requestOptions = { threshold: 200, amount: 40 };
const s = factory.create(requestType, requestOptions);
console.log(s.calc({ price: 250 })); // 210
3.3 测试起来有多爽
这是策略模式最让我喜欢的地方。以前那么一大坨 if else,测试时把所有可能情况都塞进一个测试用例里,代码丑得没法看。现在每个策略一个小文件,测试也干净利落。
// JavaScript 示例:使用 Node 内置的测试模块(node:test)来测策略
// 需要 Node.js 18 以上版本
const test = require('node:test');
const assert = require('node:assert');
// 引入策略类(这里把前面的 Discount 类拿过来)
class Discount {
constructor(options) {
this.rate = options.rate;
}
calc(order) {
return order.price * this.rate;
}
}
// 测试折扣策略
test('折扣策略应该正确打8折', () => {
const strategy = new Discount({ rate: 0.8 });
const order = { price: 100 };
assert.strictEqual(strategy.calc(order), 80);
});
// 测试满减策略
test('满减策略在金额不足时不减', () => {
const strategy = new FullReduction({ threshold: 100, amount: 20 });
const order = { price: 99 };
assert.strictEqual(strategy.calc(order), 99);
});
每个策略都是孤岛,想单测哪个就单测哪个。以后出 bug,先定位是哪个策略的问题,修好策略,重新测试,干净利索。
四、应用场景与优缺点
4.1 适合做什么
策略模式特别适合下面这些场景:
- 同一套规则,有很多“变体”。比如电商促销、优惠券、运费计算、会员权益。
- 推荐系统里的召回、排序、重排,每个环节都可能换成不同算法。
- 支付渠道选择,不同银行不同费率。
- 消息推送模板,不同平台不同文案。
总之,一旦你发现项目里有大量 if ... else if ... else 在判断“类型”然后执行不同算法,就考虑把它变成策略。
4.2 优点
- 消除巨型条件分支,代码一眼看懂。
- 符合开闭原则:增加新策略不用改旧代码,只要扩展新类。
- 每个策略独立复用,可以单独测试。
- 更好组合:策略可以像积木一样拼装。
- 方便动态切换:配合配置中心,运营改规则不用发版。
4.3 缺点
- 类或对象会变多,如果策略很少,可能过度设计。
- 使用方必须了解有哪些策略,知道选哪个。
- 策略之间的优先级和互斥关系如果没理清,可能产生叠加冲突。
- 不结合工厂,调用方还是会写 if 来选策略。
五、注意事项
策略模式不是万能药,踩坑也不少。
一、别“为了策略而策略”。如果只有两三种固定规则,几年都不变,那直接 if else 反而更直白。不要为了显得高级而把简单问题复杂化。
二、注意策略的“上下文”是共享的还是独立的。有的策略内部有状态,比如计数、缓存,那每次执行需要新建实例;如果无状态,可以复用单例。否则会出现数据串包。
三、折扣叠加顺序要讲清楚。是“先满减再打折”还是“先打折再满减”?这个顺序往往由业务方拍板,代码里要写清楚。也可以在策略类里通过元数据声明“优先级”。
四、策略之间的互斥要处理。比如“全场8折”和“特价商品不参与折扣”,特价商品这个限制不应该在折扣策略里,可以在商品数据上打标记,再让策略去判断。不要让策略变成新的“if else 集中营”。
五、策略的命名和注册要规范。项目里用约定俗成的code,比如 FULL_REDUCTION、DISCOUNT,不要写魔法字符串。建议把策略code放在统一常量文件里。
六、别把业务逻辑泄漏到外部。策略内部应该只管自己的计算,下单的其他校验最好放到独立服务里,不要让策略去访问数据库、调接口,这样测试时很难 mock。
六、总结
回过头来看,那个让我崩溃的函数,本质就是没有把“变的东西”和“不变的东西”分开。变的是各种推荐规则和折扣算法,不变的是“根据订单算出结果”这个流程。策略模式最温柔的地方,就是允许每个策略自己活成一套小生态,互不打扰。现在我做这类需求,已经形成习惯:先看有没有潜在的变化维度,如果有,就给规则戴一顶“策略”帽子,然后用工厂或者注册表管理起来。运营改需求,也只是在新策略文件里写几行逻辑,再注册一下,老代码稳如泰山。虽然初期会多写几个类,但后期省下来的测试时间、排错时间,绝对值回票价。希望这个简单但实用的模式,也能帮你从条件分支的泥潭里跳出来,写出更清爽、更不怕改的代码。
评论
围绕“策略模式替代条件分支逻辑分派的利器,在推荐规则与营销折扣场景中,业务频繁调整时如何将算法封装为可独立测试的策略族?”参与讨论