likes
comments
collection
share

【typescript工具库】处理复杂逻辑? | 不如试一试职责链模式

作者站长头像
站长
· 阅读数 16

前言

我发现网上有很多人都在说职责链模式,但是却没有人将其抽象出来,或者说抽象的程度并不能达到足以上生产环境的程度。刚好项目中有需要用到,正好做一个业务场景的复盘

项目背景

先简单说一下用到这设计模式的需求的背景,也算是我们这个项目的复盘吧,我们当时需要做一个平台,类似于文件管理和模型管理。然后我们需要判断上传文件类型.其中有xml,普通文件(video,img),model,图纸(dwg格式),然后进行不同的操作。在这期间需要跟bimface 和 阿里oss 以及我们研究院的后端进行交互。由于业务隐私问题,这里我用的是几个版本前上传xml的解析图(部分)

【typescript工具库】处理复杂逻辑? | 不如试一试职责链模式

我们有一个请求次数 = num(task)。而这个task 的数量有足足1000以上。也就是说有1000+的请求在这个上传过程中需要被处理的。这一部分的处理如果大家感兴趣的话以后会讲。我举这一个例子是为了说明说在这个链路中,只要有一个流程或者一个promise请求失败了,那么就整体失败了。并且这个xml图还只是上传与xml逻辑中的一环。

我们看到单单xml上传这里一系列交互过程,涉及到的请求已经非常多了。假如我们用常规逻辑进行判断,不难想象到后期维护起来有多酸爽。那么我们怎么构建整个链路的处理逻辑呢。职责链就可以有用武之地了。不妨我们写一个工具类

职责链模式

简介

职责链模式是一种行为设计模式,它允许你将请求沿着处理链传递,每个处理器都有机会处理请求或将其传递给下一个处理器。

实现职责链模式

数据结构

我们要实现一个职责链,我们首先应该想到的是,我们的代码的数据结构需要怎么进行定义。我们可以发现我们职责链的逻辑是沿着处理链传递的,学过算法的朋友应该反应的过来,这其实是一个链表。

链表的最简示例如下

  class ListNode {
      val: number
      next: ListNode | null
      constructor(val?: number, next?: ListNode | null) {
          this.val = val
          this.next = next
      }
  }

好的,接下来就是如何组织代码了,这里我们用 ChainData 代替 val(通过dataSet进行设置),然后用 asyncNext代替我们上面的next方法.这是数据结构的问题。

节点的值

我们接着聊一下数据的传递吧,我们在链表中单个链表中各个节点所存储的数据不一样。这里我们也是但是区别在于我们这个需要应用于业务,因此我们的数据为了持久化,各个节点存储的数据应该是增量的。

节点跳转

我们在一个事件处理完后,同步的代码我们可以通过return 值直接监听到,异步的逻辑我们在完成后却不能直接监听到,因此我们在使用的时候需要手动指定回调。也就是asyncNext方法。我们在定义class中也需要简单判断一下是不是同步方法。看看是否执行继续下一个处理器

错误事件 | 自定义事件

这一部分其实基于我们的发布者订阅者模式,关于事件,除了错误事件,还有完成事件和进度条事件之类的可以加上去。这一部分也会暴露给使用者

最后使用 TypeScript 来实现一个职责链模式吧。这个模式包含以下核心部分:

  • Chain 类:用于创建职责链。
  • ChainData :每一个链表中的数据。
  • emit 方法:用于触发事件。
  • dataSet 方法:用于初始化数据。
  • asyncNext 方法:用于执行下一个处理器。
  • nodeSet 方法:用于设置下一个处理器。
  • passRequest 方法:用于执行当前处理器并决定是否继续下一个处理器。

职责链代码

type OrderResult = 'chainNext' | any;

type OrderHandler = (...args: any[]) => OrderResult;
type emitNameType = 'finish' | 'error';
type ChainDataType = {
  eventBus?:{
    finish:Array<Function>
    error:Array<Function>
  }
  [key:string]:any
}
/**
 * @des 链式调用数据
 */
class Chain {
  private fn: OrderHandler;
  private nodeNext: Chain | null;
  // 一般会被当作初始数据
  ChainData: ChainDataType;
  constructor(fn: OrderHandler) {
    this.fn = fn;
    this.ChainData = {};
    this.nodeNext = null;
  }

  /**
   * @des 触发某一个事件
   * @param name
   * @param data 给function的值
   */
  emit = (name: emitNameType, data: any) => {
    if(this.ChainData.eventBus){
      if (this.ChainData.eventBus[name]) {
        this.ChainData.eventBus[name].forEach((element: Function) => {
          element(data);
        });
      } else {
        throw new Error('没有这个事件');
      }
    }
  };

  /**
   * @des 初始化数据
   * @param data
   */
  dataSet(data: ChainDataType): void {
    this.ChainData = data;
  }
  /**
   * @des settimeout的 异步
   * this.fn(...args) 不会返回值那么就要放到后面去 | 避免 return 判断。因为不会return 值
   * @param args
   * @returns
   */
  asycNext(): OrderResult {
    if (this.nodeNext) {
      this.nodeNext.dataSet(this.ChainData);
      return this.nodeNext.passRequest();
    }
    return this.fn();
  }

  nodeSet(nodeNext: Chain): void {
    this.nodeNext = nodeNext;
  }

  passRequest(): OrderResult {
    // 执行这个方法
    const res = this.fn();
    if (res === 'chainNext') {
      if (this.nodeNext) {
        // 所有的函数都要用 chain 方法包起来,否则没有 this.nodeNext
        this.nodeNext.dataSet(this.ChainData);
        return this.nodeNext.passRequest();
      }
    }
    if (this.nodeNext) {
      this.nodeNext.dataSet(this.ChainData);
    }

    return res;
  }
}

使用示例

  • 第一步:校验数据
  • 第二步:业务逻辑
  • 第三步:结束事件
export interface ExtendedChainData extends Chain{
  ChainData:{
    init:{
      xml_id:number,
      model_id:number
    }
    res:any
  }
}

/**
 * @des 异步数据
 * @returns 
 */
const sleep = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        code: 200,
        data:[
          {id:1,name:"小明"}
        ]
      });
    }, 1000);
  });
};

/**
 * @des 初始化数据
 * @param this 
 * @returns 
 */
function initPost(this:ExtendedChainData) {
  console.log('数据校验:');
  if(!this.ChainData.init){
    this.emit("error","使用者触发error事件")
    return
  }
  return 'chainNext';
}


/**
 * @des 异步逻辑
 * @param this 
 */
async function asyncPost(this:ExtendedChainData) {
  let data = await sleep() 
  this.ChainData.res = data
  this.asycNext()
}


/**
 * @des 异步逻辑
 * @param this 
 */
 async function lastPost(this:ExtendedChainData) {
  this.emit("finish","使用者触发finish事件")
}



// 1. 初始化 链条
const chainInitPost = new Chain(initPost);
const chainAsyncPost = new Chain(asyncPost);
const chainLastPost = new Chain(lastPost);
// 2. 设置关系 注意异步 用await 处理逻辑后调用 asyncNext 方法
chainInitPost.nodeSet(chainAsyncPost);
chainAsyncPost.nodeSet(chainLastPost);
// 3. 开始执行。从一开始的链条开始执行
chainInitPost.dataSet({
  init:{
    xml_id:"1111111111",
    model_id:"22222222"
  },
  eventBus:{
    finish:[
      (e:any)=>{
        console.log(e)
        console.log(chainInitPost.ChainData.res)
      }
    ],
    error:[
      (e:any)=>{
        console.log(e)  
      }
    ]
  }
})
chainInitPost.passRequest();

输出

数据校验:
使用者触发finish事件
{ code: 200, data: [ { id: 1, name: '小明' } ] }

总结一下

可以看到我们的职责链模式在我们使用的使用需要注意下面几件事情

  • 首先是用chain初始化链条
  • 接下来是设置chain的关系,也就是需要通过nodeSet方法指定chain的下一个节点是什么
  • 接下来是初始化chain的初始数据(这里的初始数据可以在function中用this来进行获取)。然后我们需要初始化需要自定义 发布者订阅者模式中 on的 事件,这里由我们的使用者手动进行指定
  • 最后通过 passRequest 开始执行链条

代码链接:github.com/yilaikesi/u…