一个电商价格计算案例,带你学会前端开发中的责任链模式

责任链模式是前端架构中最经典的"链式处理"思想。它不像观察者或策略模式那样频繁出现在业务代码中,但一旦你理解了它,就会发现 Redux 中间件、Express 中间件、DOM 事件冒泡、甚至表单校验都可以用同一个模型来解释。

责任链模式的核心思想是:将多个处理者连接成一条链,请求沿着链依次传递,直到某个处理者处理它或链走完。 在前端开发中,它特别适合拆分"胖逻辑"------比如电商商品价格计算,往往涉及会员折扣、优惠券、满减、新人优惠等层层叠加的规则。本文从一个真实的电商价格计算场景出发,带你走进前端开发中的责任链模式。

一、一个"胖"价格计算函数

假设有一个商品价格计算的需求:

  1. 计算基础总价:单价 × 数量
  2. 使用优惠券:满 200 减 30
  3. 会员折扣:普通会员 9.5 折,高级会员 9 折

最直接的实现方式是把所有规则塞进一个函数里:

javascript 复制代码
function calculatePrice(price, quantity, coupon, memberLevel) {
  // 1. 基础总价
  let total = price * quantity;

  // 2. 使用优惠券
  if (coupon === '满200减30' && total >= 200) {
    total -= 30;
  }

  // 3. 会员折扣
  if (memberLevel === 'vip') {
    total *= 0.9;
  } else if (memberLevel === 'normal') {
    total *= 0.95;
  }

  return total;
}

目前只有两条规则,函数还算看得过去。但产品很快会加新需求:长租优惠 (租期超过 6 个月打 9 折)、新人满减限时促销......

每加一条规则,都要在这个函数里继续塞 if-else。很快,calculatePrice 就会膨胀成一个几百行的"胖函数",难以阅读、难以测试、牵一发而动全身。

责任链模式正是为了解决这个问题:把每条计算规则拆成一个独立的处理节点,用链把它们串起来。每个节点只关心自己的逻辑,处理完后把结果传递给下一个节点。

你可能会觉得这和策略模式很像------都是把一堆逻辑拆成独立单元。但它们的核心区别在于:

  • 策略模式是"选一个" :多个算法并列,调用方根据条件选择其中一个执行。比如支付方式------用户选微信就调微信支付,选支付宝就调支付宝支付,两者不会同时执行。
  • 责任链模式是"走流程" :多个处理步骤串行,请求依次经过每个处理者。比如价格计算------一个商品可能同时享受长租优惠、优惠券和会员折扣,这些规则是叠加的,不是互斥的。

策略是"并联选择",责任链是"串联加工"。 价格计算中,各种优惠规则需要层层叠加,所以用责任链;支付渠道选择中,同一笔订单只需要一种支付方式,所以用策略。

二、用函数组合实现责任链

责任链模式的核心动作是:依次调用函数,每个函数接收上一个函数的输出,处理后再传给下一个。 这和 reduce 的行为完全吻合。

1. 定义每条规则为纯函数

javascript 复制代码
// 每个规则函数签名为: (prevTotal, context) => newTotal
// prevTotal:上一步计算的结果
// context:计算所需的上下文信息(数量、会员等级等)

// 规则1:计算基础总价
function basePrice(prevTotal, { price, quantity }) {
  return price * quantity;
}

// 规则2:优惠券(满200减30)
function couponRule(total, { coupon }) {
  if (coupon === '满200减30' && total >= 200) {
    return total - 30;
  }
  return total;
}

// 规则3:会员折扣
function memberDiscount(total, { memberLevel }) {
  if (memberLevel === 'vip') return total * 0.9;
  if (memberLevel === 'normal') return total * 0.95;
  return total;
}

每条规则都是一个无副作用的纯函数,只做一件事,可以独立测试、独立修改。

2. 组合器:把规则函数串成链

javascript 复制代码
// 责任链组合器:依次执行 rules,每个 rule 接收上一个的返回值
function createPriceChain(...rules) {
  return (context) => {
    return rules.reduce((total, rule) => rule(total, context), 0);
  };
}

这里 reduce 的初始值是 0,第一条规则 basePrice 会基于 context 计算出真正的初始价格。

3. 使用

javascript 复制代码
const calculate = createPriceChain(basePrice, couponRule, memberDiscount);

const result = calculate({
  price: 100,
  quantity: 3,
  coupon: '满200减30',
  memberLevel: 'vip',
});

console.log(result); // (100*3 - 30) * 0.9 = 243

新增一条"长租优惠"规则,只需要新增一个函数,插入到链中即可,完全不用修改已有规则:

javascript 复制代码
// 新增长租优惠规则:租期 >= 6 个月,打 9 折
function longTermDiscount(total, { months }) {
  if (months >= 6) return total * 0.9;
  return total;
}

// 新链:基础价 → 长租优惠 → 优惠券 → 会员折扣
const calculateV2 = createPriceChain(
  basePrice,
  longTermDiscount,   // 插入新规则
  couponRule,
  memberDiscount
);

这正是开闭原则的体现------对扩展开放,对修改封闭

三、封装成自定义 Hook:usePriceCalculator

在实际项目中,计算逻辑通常放在组件内,而规则链的配置可能来自后端或全局配置。我们可以封装一个 usePriceCalculator Hook,让组件直接用。

typescript 复制代码
// hooks/usePriceCalculator.ts
import { useMemo } from 'react';

type PriceRule = (total: number, context: Record<string, any>) => number;

export function usePriceCalculator(rules: PriceRule[]) {
  // 使用 useMemo 缓存组合后的计算函数,规则数组不变则不重新创建
  const calculate = useMemo(() => {
    return (context: Record<string, any>) => {
      return rules.reduce((total, rule) => rule(total, context), 0);
    };
  }, [rules]);

  return { calculate };
}

在 React 组件中使用

tsx 复制代码
// components/PriceDisplay.tsx
import { usePriceCalculator } from '@/hooks/usePriceCalculator';

// 导入或定义规则函数(可来自配置文件)
const rules = [basePrice, longTermDiscount, couponRule, memberDiscount];

function PriceDisplay({ product, quantity, coupon, memberLevel, months }) {
  const { calculate } = usePriceCalculator(rules);

  const totalPrice = calculate({
    price: product.price,
    quantity,
    coupon,
    memberLevel,
    months,
  });

  return <div>最终价格:¥{totalPrice.toFixed(2)}</div>;
}

如果规则是动态的(比如不同活动页使用不同规则),只需传入不同的 rules 数组即可:

tsx 复制代码
// 普通活动页
const normalRules = [basePrice, couponRule, memberDiscount];

// 长租活动页
const longTermRules = [basePrice, longTermDiscount, couponRule, memberDiscount];

function ActivityPage({ isLongTerm }) {
  const { calculate } = usePriceCalculator(isLongTerm ? longTermRules : normalRules);
  // ...
}

四、为什么这个 Hook 是责任链模式?

  • 多个处理者(规则函数)连接成一条链rules 数组就是链。
  • 请求(context)沿着链依次传递reduce 遍历每个规则,将上一次的结果传给下一个规则。
  • 某个规则可以"处理"并修改结果 :每个规则都可以基于 totalcontext 返回新的 total,也可以选择不做任何修改(返回原值)。
  • 发送者(组件)与接收者(规则函数)解耦 :组件只负责调用 calculate(context),不关心内部有哪些规则、顺序如何。

五、进阶:支持"短路"的责任链

有时候我们需要某个规则在满足条件后截断链,不再执行后续规则(比如"限时秒杀价"直接覆盖所有规则)。可以在规则函数中返回一个特殊标记。

javascript 复制代码
const STOP = Symbol('stop');

function flashSaleRule(total, { isFlashSale, flashPrice }) {
  if (isFlashSale) {
    return flashPrice; // 直接返回秒杀价,不执行后续规则
  }
  return total;
}

修改组合器,让 reduce 在遇到 STOP 时跳出:

javascript 复制代码
function createPriceChain(...rules) {
  return (context) => {
    let total = 0;
    for (const rule of rules) {
      total = rule(total, context);
      // 如果规则返回了特殊标记,停止后续规则
      if (total === STOP) break;
    }
    return total;
  };
}

不过,对于价格计算场景,通常不需要截断,每个规则都应该有序叠加。这种"短路"机制更适用于表单校验链(遇到第一个错误就停)。

六、总结

责任链模式的核心思想是链式传递,依次处理,它并不依赖于类。在函数式编程中,我们可以用以下方式实现:

实现方式 适用场景
reduce + 规则函数数组 数据依次变换(价格计算、数据清洗)
for...of + 短路返回 校验链、权限检查链
自定义 Hook 封装 React/Vue 组件中复用责任链逻辑
useReducer + 策略对象 状态管理中的分发链

useReducer 本身就是一种"内置的责任链"。每个 case 是一个处理者,action 沿着 switch 链依次匹配,匹配到就处理并停止。

责任链模式和粗略模式的区别,可以用一个简单的类比来区分:

  • 策略模式 :你去餐厅吃饭,菜单上有川菜、粤菜、日料。你主动选择一种菜系,厨房只按这种菜系的做法给你做菜。各菜系之间互不干扰。
  • 责任链模式 :你去医院看病,先挂号,然后去诊室,医生开了药,你去药房取药。你的"请求"依次经过多个窗口,每个窗口处理自己的那部分,然后交棒给下一个窗口。你不需要选择走哪条路------流程是固定的。

当你发现代码里出现一连串的 if-else 或嵌套的 try-catch,且每个分支都判断"是否能处理"时,就是责任链模式该上场的时候。

相关推荐
__sjfzllv___8 小时前
在职前端Leader学习/转行 AI Agent -DAY10
前端
沉迷学习日益消瘦8 小时前
8 UI 组件库与设计系统:Token 驱动 + 暗色模式 + 4 种主题色
前端
CodeSheep9 小时前
有这4个迹象,你就该离职了!
前端·后端·程序员
用户059540174469 小时前
把大模型记忆回归测试从手工检查换成 Playwright+VCR,误判率从 40% 降到 0
前端·css
倾颜9 小时前
pnpm 负责依赖,Turbo 负责任务:一次小型 Monorepo 治理的边界取舍
前端·next.js
程序员爱钓鱼9 小时前
Rust 元组 Tuple 详解:组合不同类型的数据
前端·后端·rust
IT_陈寒9 小时前
React的useEffect为什么经常执行两次?
前端·人工智能·后端
国服第二切图仔9 小时前
12-Web工具
大数据·前端
爱分享的程序猿-Clark9 小时前
【前端分享】大前端监控体系搭建实战
前端