最近在搞一个商城的小程序,运营那边隔三差五就提需求,今天说“老用户推荐新用户,每人送一张满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_REDUCTIONDISCOUNT,不要写魔法字符串。建议把策略code放在统一常量文件里。

六、别把业务逻辑泄漏到外部。策略内部应该只管自己的计算,下单的其他校验最好放到独立服务里,不要让策略去访问数据库、调接口,这样测试时很难 mock。

六、总结

回过头来看,那个让我崩溃的函数,本质就是没有把“变的东西”和“不变的东西”分开。变的是各种推荐规则和折扣算法,不变的是“根据订单算出结果”这个流程。策略模式最温柔的地方,就是允许每个策略自己活成一套小生态,互不打扰。现在我做这类需求,已经形成习惯:先看有没有潜在的变化维度,如果有,就给规则戴一顶“策略”帽子,然后用工厂或者注册表管理起来。运营改需求,也只是在新策略文件里写几行逻辑,再注册一下,老代码稳如泰山。虽然初期会多写几个类,但后期省下来的测试时间、排错时间,绝对值回票价。希望这个简单但实用的模式,也能帮你从条件分支的泥潭里跳出来,写出更清爽、更不怕改的代码。