百万PV商城实践系列 -策略模式与责任链运用实战

1,700 阅读13分钟

⚠️ 本文为掘金社区首发签约文章,未获授权禁止转载

简介

相信很多同学都听过策略和责任链两个经典的设计模式,但你知道它们具体怎么用、为什么这么用、能带来什么效果吗?很多人可能都是一头雾水。因此接下来,我会在商城实践系列的第4篇文章中,以比较常见的订单异常场景作为实践用例,带大家了解责任链模式与策略模式的特性和使用方法和场景。

这样,在后面碰到类似场景时,我们就可以照葫芦画瓢,套用一些方案来保证逻辑的扩展性和可读性。

设计模式

设计模式一共分为四种类型,分别是创建型、行为型、结构型、技巧型。而责任链模式与策略模式就是属于行为型的设计模式,是我们在开发中比较常见的设计模式,本次我会围绕这两个设计模式分别做一些叙述。

image.png

策略模式

首先,我们先来看下策略模式。那么,策略究竟是什么呢?下面,我们通过一个现实生活中的问题来直观理解一下。

小王国庆准备回家,但是车票太火爆了,所以他规划了几种不同的回家策略,也就是乘坐不同的交通工具:飞机、高铁、火车、顺风车或者汽车。最终,小王选择了乘坐1300大洋的飞机回家,而这就是我们执行的一种为了回家的一种策略。

image.png

结合上面的小故事,我们可以将策略理解为:实现目标的方案集合,以及根据情况形势的变化而制定的行动方法。策略模式的含义其实和策略相差不大,我们可以将上述的问题转换成代码:

console.log("go home");

const 我的选择 = "没买到票";

if (我的选择 === "乘坐高铁票回家") {
  console.log("乘坐高铁票回家");
} else if (我的选择 === "乘坐火车票回家") {
  console.log("乘坐火车票回家");
} else if (我的选择 === "乘坐大巴回家") {
  console.log("乘坐大巴回家");
} else if (我的选择 === "乘坐顺风车回家") {
  console.log("乘坐顺风车回家");
} else {
  console.log("回不了家了 ^^^");
}

image.png

通过不同的if else语句判断我的选择,来执行不同的业务逻辑。也就是后面的一些动作。那么,随着想法越来越多,我们的if else也会越来越多,相对应执行的逻辑块如果还写在一个函数体里面的话就会非常杂乱,那它该如何维护,以及怎么做更简单呢?

大多数有经验的同学可能会想到以下两种解决方案:

  • Switch方法拆分:将逻辑方法进行拆分,尽可能将逻辑拆分
  • 策略模式:将处理方式封装成单个策略,减少了耦合,添加了复用性。

下面,我就分别用这两种方法来实现一下逻辑的优化。

Switch方法拆分

console.log("go home");

const 我的选择 = "没买到票";

const 乘坐高铁回家处理 = () => {
  console.log("乘坐高铁回家");
};
const 乘坐火车回家处理 = () => {
  console.log("乘坐火车回家");
};
const 乘坐大巴回家处理 = () => {
  console.log("乘坐大巴回家");
};
const 乘坐顺风车回家处理 = () => {
  console.log("乘坐顺风车回家");
};

const 其他处理 = () => {
  console.log("其他,回不了家了 ^^^");
};

switch (我的选择) {
  case "乘坐高铁回家":
    乘坐高铁回家处理();
    break;

  case "乘坐火车回家":
    乘坐火车回家处理();
    break;

  case "乘坐大巴回家":
    乘坐高大巴回家处理();
    break;

  case "乘坐顺风车回家":
    乘坐顺风车回家处理();
    break;

  default:
    其他处理();
}

image.png

如上图,我将处理逻辑单独抽成对应的处理函数,然后通过不同的case执行不同的逻辑,这样做的目的是为了一个条件项中非常复杂的操作将其拆分出去,尽可能保证方法调用层是干净的。

执行策略

逻辑分块后,我们依旧还是需要入口函数通过switch case来判断逻辑,那么我们能不能将这一步省略掉,或者更加直接方式来执行呢?很显然是可以的,也就是我下面要用的策略模式的方法了。下面,我就来将上面逻辑拆分出去的代码组合成一个简单的策略:

console.log("go home");

const 我的选择 = "没买到票";

const goHomeStrategy = {
  乘坐高铁回家: () => {
    console.log("乘坐高铁回家");
  },
  乘坐火车回家: () => {
    console.log("乘坐火车回家");
  },
  乘坐大巴回家处理: () => {
    console.log("乘坐大巴回家");
  },
  乘坐顺风车回家处理: () => {
    console.log("乘坐顺风车回家");
  },

  其他: () => {
    console.log("其他,回不了家了 ^^^");
  },
};

if (goHomeStrategy[我的选择]) {
  goHomeStrategy[我的选择]();
} else {
  goHomeStrategy["其他"]();
}

通过上述代码可以看到,我将所有的处理函数都写在了一个对象当中,通过对应的key来获取对应的函数并将其执行。这一步其实就是代替了逻辑拆分中switch case的操作。通过简单的策略模式可以减少条件判断语句的逻辑,同时将处理方法的逻辑进行了解耦拆分。

以上就是策略模式的介绍,通过我回家的小实例能够快速的理解它的含义。

责任链模式

说完了策略模式,我们再来看看责任链模式。

链是什么呢?简单来说,我们可以把它想象成一根被一环环连接起来的铁链。

image.png

责任链模式(Chain of Responsibility)使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象能够处理它。

我们可以将责任链看作是一个链条,这个链条的结果是完成一件事情,那么链条的每个节点都属于一个节点,每个节点都是一个过程,只有完成了这个节点才会进入到下一个节点。

image.png

下面,我也通过一个简单实例带大家了解下责任链是怎么工作的。

责任链实践

下面这个功能非常简单,我们前端无非就是根据后端返回的文字或者说返回的状态进行一个判断。但是对于后端来说每一件商品都需要去针对一些判断条件去做对应的条件判断来返回一个标识给你,如果流程一旦复杂,页面上的条件判断语句就会多了起来。同时它们还有一定的判断优先序列,那么我们上述的策略模式就已经不通用了,我们就需要用到下面的责任链方案来处理这种有一定优先次序的逻辑判断语句了。

image.png

创建责任链链条

下面我们就来创建一些处理函数,用于处理判断商品条件的判断函数,每一个函数之中只做一件事情,就是判断商品的状态,在这里我就以最简单的逻辑进行举例子。

/**
 * 处理节点 已经下架
*/
function checkProductSuccess(status){
  if (status === 1) {
    return '正常商品'
  }
  return 'next'
}

/**
 * 处理节点 已经售罄
*/
function checkProductSellOut(status){
  if (status === 2) {
    return '商品已售罄'
  }
  return 'next'
}

/**
 * 处理节点 商品库存不足
*/
function checkProductNotEnough(status){
  if (status === 3) {
    return '商品库存不足'
  }
  return 'next'
}

当我们创建了一批处理函数的时候,我们这么将其串联起来呢?下面就要通过一个简单的类来完成它们之间的串联,方便我们对它进行调用。

class Chain {
  handler: Function | null = null
  nextChain!: Chain
  constructor(handler: Function){
      this.handler = handler;
  }
  setNextChain(nextChain: Chain){
      this.nextChain = nextChain;
  }
  handleRequest(){
      let result = this.handler?.apply(this, arguments)
      console.log(result, 'result')
      if( result === 'next' ){
          this.nextChain && this.nextChain.handleRequest.apply(this.nextChain, arguments)
      }
  }
}

当有了Chain类的话,我通过setNextChain可以设置当前处理任务的下一次解析任务该处理什么,这样的话就可以比较灵活的安排每个链条节点应该处理的事情。下面,我就对其进行绑定,然后进行状态解析

// 实例化处理函数为责任链节点

const checkProductOffshelvesChain = new Chain(checkProductOffshelves)

const checkProductSellOutChin = new Chain(checkProductSellOut)

const checkProductNotEnoughChain = new Chain(checkProductNotEnough)

const checkProductSuccessChain = new Chain(checkProductSuccess)

// 串联节点

checkProductOffshelvesChain.setNextChain(checkProductSellOutChin)

checkProductSellOutChin.setNextChain(checkProductNotEnoughChain)

checkProductNotEnoughChain.setNextChain(checkProductSuccessChain)

// 开始派发任务

checkProductOffshelvesChain.handleRequest(1)

如下图,每经过一个节点,就看是否能够处理,如果不能的话返回next标识,然后Chain类的handleRequest方法进行判断,当前handler的执行结果是不是next,如果是的话则进行下一个策略执行,反之则处理完成。

image.png

订单异常提示实践

当了解了两个设计模式的概念后,我就以提交订单的交易链路来做为一个实例,带大家一起来进行一个场景实践吧。下面,我就以商城项目中常见的提交订单失败的异常提示来做演示,带大家将设计模式作用于业务是如何处理的。

如下是我实现的一个简单效果,用于处理提交订单信息失败的原因与展示。

image.png

提交订单失败会触发一些场景,但是这些场景可能显示的操作方案也不一样,如下思维图简单描述一下常见的错误情况:

image.png

针对这些方案,都有可能是在提交订单失败时出现的,我们以商品级别的错误信息提示来划分下出现于底部的操作按钮具体场景:

image.png

这个功能如何实现呢?下面,我就带大家一起来做一下。

对应操作按钮生成

首先,整个操作都通过useErrorHandle自定义hook进行封装,传入的参数分别是一个状态码与一个错误操作策略的集合。在内部会根据传入的状态码渲染对应的按钮配置与执行对应的策略。

/** @name 当前按钮的内容 */

const RenderJSON = useErrorHandle(30001, errorActionStrategy)

如下,每一个状态码都对应了一个schema的配置,通过内部的buttons字段来生成显示上的按钮。

const schemaJSON: Record<string, SchemaJSONValue> = {
  30001: {
    description: "购物车页面,商品信息变更且无可在购买商品",
    buttons: [
      {
        type: "confirm",
        text: "我知道了",
        options: {
          actionType: "closePopup",
        },
      },
    ],
  },

  30002: {
    description: "购物车页面,商品信息变更,但还存在可购买商品信息",
    buttons: [
      {
        type: "cancel",
        text: "关闭",
        options: {
          actionType: "closePopup",
        },
      },
      {
        type: "confirm",
        text: "继续结算",

        options: {
          actionType: "nextCheckout",
        },
      },
    ],
  },
};

在开始时,我们传入了一个自定义的状态码,而下面我就通过这个状态码拿到相匹配的配置,然后通过map的形式生成好对应render的显示视图。

在这之前,通过React.createElement将我们左右两侧的按钮实例化,通过buttons中的type字段来做按钮的选取展示。在后面的话就只需要往schema中添加对应的配置就可以生产新需求的按钮视图逻辑了。

const RenderJSON = useMemo(() => {
  const createButtonElement = (item: SchemaButton) => {
    const cancelButton = createElement(
      Button,
      {
        block: true,
        className: "checkout-popup__body__actions__cancel",
        color: "warning",
        shape: "round",
        onClick: errorHandleClick(item.options),
      },
      item.text
    );

    const confirmButton = createElement(
      Button,
      {
        block: true,
        color: "linear-gradient(135deg, #FF8A39 0%, #FC622C 100%);",
        shape: "round",
        className: "checkout-popup__body__actions__confirm",
        onClick: errorHandleClick(item.options),
      },
      item.text
    );

    return item.type === "cancel" ? cancelButton : confirmButton;
  };

  const currentJSON = schemaJSON[errorCode];
  return currentJSON.buttons.map((item: SchemaButton) => {
    return createButtonElement(item);
  });
}, [errorCode]);

策略模式处理按钮逻辑

通过上面的代码,我们大致上可以将页面上的按钮做一个简单的配置生成。但是,如何统一处理它们的点击事件呢?

如下图,我大致上将操作事件归为以下几个:

image.png

通过策略模式,我将其描述为以下的对象,key是事件的标识名称,value是事件具体执行。然后将errorActionStrategy传入useErrorHandle当中。

/** @name 策略模式的集合 */
const errorActionStrategy = {
  /** @name 关闭弹窗的策略 */
  closePopup: () => {
    setOpenStatus(false);
    console.log("我执行了:关闭弹窗的策略");
  },

  /** @name 继续结算的策略 */
  nextCheckout: () => {
    console.log("我执行了:继续结算的策略");
  },

  /** @name 清除失效商品的策略 */
  clearInvalidProduct: () => {
    console.log("我执行了:关闭弹窗的策略");
  },

  /** @name 更换收货地址的策略 */
  changeAddress: () => {
    console.log("我执行了:关闭弹窗的策略");
  },
  
  /** @name 返回购物车,返回上一页 */

  backPage: () => {
    router.go(-1)
    console.log("我执行了:返回策略");
  }
  
};

useErrorHandle中,我们刚刚通过schema object来配置了buttons,同时在配置的结构中也包含了对策略标识的配置,在options中通过actionType字段来进行标识,所属的按钮执行哪条策略模式。

interface SchemaButton {
  type: 'cancel' | 'confirm'
  options: {
    actionType: 'closePopup' 
                | 'nextCheckout' 
                | 'clearInvalidProduct' 
                | 'backPage' 
                | 'changeAddress'
  },
  text: string

当有了actionType的时候,绑定onClick事件时将options传递过去,然后引用对应的策略模式。

/**
 * 错误提醒弹窗提示处理函数
 * @param options JSON操作配置
 * @returns void
 */
 const onErrorHandleClick = (options: SchemaButton["options"]) => () => {
  console.log("errorHandleClick", errorActionStrategy);
  if (errorActionStrategy[options.actionType]) {
    errorActionStrategy[options.actionType]();
  }
};


// tsx
{
  onClick: onErrorHandleClick(item.options)
}

至此,当我们点击按钮时,对应的actionType会触发相应的策略事件。

那么,后续如果出现其他的错误类型,我们只需要对schema进行配置的同时,添加对应的actionType。在不影响旧逻辑的情况下对业务做增量,尽最大可能做一些解耦的操作。

资源

总结

本文主要讲述了责任链模式策略模式的使用,对于日常开发来说是比较常用的两个设计模式,不仅仅可以优化我们复杂的条件判断语句,同时也可以针对业务逻辑做部分解藕,方便后续做出扩展与维护

有优点必然有缺点,不管是策略模式还是责任链模式如果使用不当的话也同样会造成一些难以预料的问题。究其根本,我们需要在特定的侧重点上将设计模式的优点作用于我们复杂凌乱的代码上。

本文设计模式实践仅供参考,如果你有更好的实现方式也可以采取更加有效的方式处理当前的业务逻辑,同时也需要考虑后面迭代内容是否能够做一些解耦以及更加简单的方式。如果觉得不错,对你有帮助的话,可以点个👍,给我加个油。如果对前端电商项目想了解更多的Yoyo们可以关注本专栏

近期好文

尾注

本文首发于:掘金技术社区
类型:签约文章
作者:wangly19
收藏于专栏:# 百万PV商城实践系列
公众号: ItCodes 程序人生