网络日志

职责链模式在项目中的运用

最近的工作是在做后台管理系统中的采购订单,大体流程是客户先选择供应商、然后从供应商提供的商品中进行选择,选择后,可对价格、数量、金额等字段进行编辑,最终形成一个包含多个商品对象的数组,每个商品对象具有很多与该商品相关的属性,大体如下:

const productList = [
      {
        ItemId: 2000, // 商品ID
        Name: 'xxx', // 品名
        IsFresh: true, // 是否为生鲜商品
        PurcPrice: 20, // 档案进价
        Price: 21, // 价格
        CostPrice: 20, // 加权平均价
        Qty: 30, // 数量
        Amount: 630, // 小计金额
        LargeQty: 30 // 箱数
        // 单位、自编码、 ......
      }
      // ......
    ]

客户编辑完后,最后点击保存按钮,调用接口生成一个采购订单。保存时,需要对客户选择的商品进行一些校验,现在有三个校验规则:

  1. 商品的小计金额(Amount)不能超过99999999,
  2. 判断采购设置中是否启用了:“采购价格高于加权平均价时,进行提示”,如果启用了,当存在某一商品的采购价格(Price)高于加权平均价(CostPrice)时,弹框提示
  3. 采购设置中不启用:“生鲜商品允许价格高于商品档案进价”,那么当一个商品为生鲜商品,且价格高于档案进价时,弹框提示

发现上面的三个校验是相互独立的,有些校验一旦不满足就不能保存(1、3),有些条件不满足也依然可以保存,让用户自己选择(2)。为了让这几个校验的过程能够相互独立,刚好可以使用职责链模式对代码进行管理。

参阅《javascript设计模式与开发实践》一书中对职责链模式的定义:使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

下面是具体的实现方式:

  /**
   * 职责链模式
   */
  export class Chain {
    constructor(fn) {
      this.fn = fn
      this.successor = null
    }
  
    setNextSuccessor(successor) {
      return (this.successor = successor)
    }
  
    async passRequest() {
      const res = await this.fn.apply(this, arguments)
      if (res === 'nextSuccessor' && this.successor) {
        return await this.successor.passRequest.apply(this.successor, arguments)
      }
      return res
    }
  }

  /**
   * 使用职责链模式进行保存前的校验
   * @param {Object} data 需要校验的数据
   */
  async function validate(data) {
    // 校验:小计金额最大值限制
    function validateAmountMax() {
      const index = data.findIndex((item) => item.Amount >= 99999999)
      if (index > -1) {
        return new Promise((resolve) => {
          const modal = Modal.confirm({
            title: '提示',
            content: '小计金额不能超过99999999',
            icon: createVNode(ExclamationCircleOutlined),
            okText: '确定',
            cancelText: createVNode(),
            async onOk() {
              nextTick(() => modal.destroy())
              resolve(false)
            }
          })
        })
      }
      return 'nextSuccessor'
    }

    // 校验:采购价格是否高于加权平均价
    function validateInPriceHigherThanAutoPrice() {
      // 判断采购设置中是否启用了:“采购价格高于加权平均价时,进行提示”,如果启用了,
      // 当存在某一商品的采购价格高于加权平均价时,弹框提示
      if (purchaseSettings.value.InPriceHigherThanAutoPrice == 'Y') {
        const needTip = data.some((item) => item.Price > item.CostPrice)
        
        if (needTip) {
          return new Promise((resolve) => {
            const modal = Modal.confirm({
              title: '确认',
              content: '存在商品的当前价格高于加权平均价,是否继续保存?',
              icon: createVNode(ExclamationCircleOutlined),
              okText: '确定',
              cancelText: '取消',
              async onOk() {
                nextTick(() => modal.destroy())
                resolve('nextSuccessor')
              },
              async onCancel() {
                nextTick(() => modal.destroy())
                resolve(false)
              }
            })
          })
        }
      }
      return 'nextSuccessor'
    }

    // 校验:生鲜商品允许价格高于商品档案进价
    function validateFreshAllowInPriceHigherThanItemPrice() {
      // 采购设置中不启用:“生鲜商品允许价格高于商品档案进价”,那么当一个商品为生鲜商品,
      // 且价格高于档案进价时,弹框提示
      if (purchaseSettings.value.FreshAllowInPriceHigherThanItemPrice == 'N') {
        const needTip = data.some((item) => item.IsFresh && item.Price > item.PurcPrice)
        if (needTip) {
          return new Promise((resolve) => {
            const modal = Modal.confirm({
              title: '提示',
              content: '采购设置设置了不允许生鲜商品价格高于商品档案进价,请重新输入价格!',
              icon: createVNode(ExclamationCircleOutlined),
              okText: '确定',
              cancelText: createVNode(),
              async onOk() {
                nextTick(() => modal.destroy())
                resolve(false)
              }
            })
          })
        }
      }
      return true
    }

    // 将三个校验函数包装成职责链节点
    const valAmount = new Chain(validateAmountMax)
    const valCostPrice = new Chain(validateInPriceHigherThanAutoPrice)
    const valFresh = new Chain(validateFreshAllowInPriceHigherThanItemPrice)
    // 指定节点在职责链中的顺序
    valAmount.setNextSuccessor(valCostPrice).setNextSuccessor(valFresh)
    // 把请求传递给第一个节点
    return await valAmount.passRequest()
  }

  // 保存
  async function save(data) {
    if (!(await validate(data))) return false

    // ....... 调接口进行保存
  }