代码越写越乱?那是因为你没用责任链

39,352 阅读10分钟

前言

在开始学习责任链之前,先看一下在开发中常见的问题。下面是前端用来处理 API 错误码的代码:

 const httpErrorHandler = (error) => {
   const errorStatus = error.response.status;
   if (errorStatus === 400) {
     console.log('你是不是提交了什么奇怪的东西?');
   }
   
   if (errorStatus === 401) {
     console.log('需要先登陆!');
   }
   
   if (errorStatus === 403) {
     console.log('是不是想偷摸干坏事?');
   }
   
   if (errorStatus === 404) {
     console.log('这里什么也没有...');
   }
};

当然实际项目中不可能只有一行 console,这是为了说明原理的简化版。

代码中的 httpErrorHandler 会接收 API 的响应错误,并对错误的状态码做不同的处理,所以代码中需要很多 if(或者 switch)判断当前需要要执行什么,当你要对新的错误添加处理代码时,就必须要到 httpErrorHandler 中修改代码。

虽然免不了要经常修改代码,但是这样做可能会导致几个问题,下面根据 SOLID 的 单一职责(Single responsibility)和开放封闭(open/close)这两个原则来说明:

单一职责(Single responsibility)

简单的说,单一职责就是只做一件事情。而前面的 httpErrorHandler 方法以使用的角度来说,是把错误对象交给它,让它按照错误码做对应的处理。看上去好像是在做“错误处理”这个单一的事情,但是从实现的角度上来说,它把不同错误的处理逻辑全部写在了 httpErrorHandler 中,这就会导致可能在只想要修改对错误码为 400 的逻辑时,但是不得不阅读一大堆不相关的代码。

开放封闭原则(open/close)

开放封闭原则是指对已经写好的核心逻辑就不要再去改动,但同时要能够因需求的增加而扩充原本的功能,也就是开放扩充功能,同时封闭修改原本正确的逻辑。再回过头来看 httpErrorHandler,如果需要增加一个对错误码 405 的处理逻辑(要扩充新功能),那就需要修改 httpErrorHandler 中的代码(修改原本正确的逻辑),这也很容易造成原来正确执行的代码出错。

既然 httpErrorHandler 破绽这么多,那该怎么办?

解决问题

分离逻辑

先让 httpErrorHandler 符合单一原则。首先把每个错误的处理逻辑分别拆成方法:


const response400 = () => {
  console.log('你是不是提交了什么奇怪的东西?');
};

const response401 = () => {
  console.log('需要先登陆!');
};

const response403 = () => {
  console.log('是不是想偷摸干坏事?');
};

const response404 = () => {
  console.log('这里什么也没有...');
};

const httpErrorHandler = (error) => {
  const errorStatus = error.response.status;
  if (errorStatus === 400) {
    response400();
  }
   
  if (errorStatus === 401) {
    response401();
  }
   
  if (errorStatus === 403) {
    response403();
  }
   
  if (errorStatus === 404) {
    response404();
  }
};

虽然只是把每个区块的逻辑拆成方法,但这已经可以让我们在修改某个状态码的错误处理时,不用再到 httpErrorHandler 中阅读大量的代码了。

仅仅是分离逻辑这个操作同时也让 httpErrorHandler 符合了开放封闭原则,因为在把错误处理的逻辑各自拆分为方法的时候,就等于对那些已经完成的代码进行了封装,这时当需要再为 httpErrorHandler 增加对 405 的错误处理逻辑时,就不会影响到其他的错误处理逻辑的方法(封闭修改),而是另行创建一个新的 response405 方法,并在 httpErrorHandler 中加上新的条件判断就行了(开放扩充新功能)。

现在的 httpErrorHandler 其实是策略模式(strategy pattern),httpErrorHandler 用了统一的接口(方法)来处理各种不同的错误状态,在本文的最后会再次解释策略模式和责任链之间的区别。

责任链模式(Chain of Responsibility Pattern)

责任链的实现原理很简单,就是把所有方法串起来一个一个执行,并且每个方法都只做自己要做的事就行了,例如 response400 只在遇到状态码为 400 的时候执行,而 response401 只处理 401 的错误,其他方法也都只在自己该处理的时候执行。每个人各司其职,就是责任链。

接下来开始实现。

增加判断

根据责任链的定义,每个方法都必须要知道当前这件事是不是自己应该处理的,所以要把原本在 httpErrorHandler 实现的 if 判断分散到每个方法中,变成由内部控制自己的责任:


const response400 = (error) => {
  if (error.response.status !== 400) return;
  console.log('你是不是提交了什么奇怪的东西?');
};

const response401 = (error) => {
  if (error.response.status !== 401) return;
  console.log('需要先登陆!');
};

const response403 = (error) => {
  if (error.response.status !== 403) return;
  console.log('是不是想偷摸干坏事?');
};

const response404 = (error) => {
  if (error.response.status !== 404) return;
  console.log('这里什么也没有...');
};

const httpErrorHandler = (error) => {
  response400(error);
  response401(error);
  response403(error);
  response404(error);
};

把判断的逻辑放到各自的方法中之后,httpErrorHandler 的代码就精简了很多,也去除了所有在 httpErrorHandler 中的逻辑,现在httpErrorHandler 只需要按照顺序执行 response400 到 response404 就行了,反正该执行就执行,不该执行的也只是直接 return 而已。

实现真正的责任链

虽然只要重构到上一步,所有被分拆的错误处理方法都会自行判断当前是不是自己该做的,但是如果你的代码就这样了,那么将来看到 httpErrorHandler 的其他人只会说:

这是什么神仙代码?API 一遇到错误就执行所有错误处理?

因为他们不知道在每个处理方法里面还有判断,也许过一段时间之后你自己也会忘了这事,因为现在的 httpErrorHandler 看起来就只是从 response400 到 response404,即使我们知道功能正确,但完全看不出是用了责任链。

那到底怎样才能看起来像是个链呢?其实你可以直接用一个数字记录所有要被执行的错误处理方法,并通过命名告诉将来看到这段代码的人这里是责任链:

const httpErrorHandler = (error) => {
  const errorHandlerChain = [
    response400,
    response401,
    response403,
    response404
  ];
  errorHandlerChain.forEach((errorHandler) => {
    errorHandler(error);
  });
};

优化执行

这样一来责任链的目的就有达到了,如果像上面代码中用 forEach 处理的话,那当遇到 400 错误时,实际上是不需要执行后面的 response401 到 response404 的。

所以还要在每个错误处理的方法中加上一些逻辑,让每个方法可以判断,如果是遇到自己处理不了的事情,就丢出一个指定的字符串或布尔值,接收到之后就再接着执行下一个方法,但如果该方法可以处理,则在处理完毕之后直接结束,不需要再继续把整个链跑完。

const response400 = (error) => {
  if (error.response.status !== 400return 'next';
  console.log('你是不是提交了什么奇怪的东西?');
};

const response401 = (error) => {
  if (error.response.status !== 401return 'next';
  console.log('需要先登陆!');
};

const response403 = (error) => {
  if (error.response.status !== 403return 'next';;
  console.log('是不是想偷摸干坏事?');
};

const response404 = (error) => {
  if (error.response.status !== 404return 'next';;
  console.log('这里什么都没有...');
};

如果链中某个节点执行结果为 next,则让下后面的方法继续处理:

const httpErrorHandler = (error) => {
  const errorHandlerChain = [
    response400,
    response401,
    response403,
    response404
  ];
  
  for(errorHandler of errorHandlerChain) {
    const result = errorHandler(error);
    if (result !== 'next') break;
  };
};

封装责任链的实现

现在责任链已经实现完成了,但是判断要不要给下一个方法的逻辑(判断 result !== 'next') ,却暴露在外面,这也许会导致项目中每个链的实现方法都会不一样,其他的链有可能是判断 nextSuccessor 或是 boolean,所以最后还需要封装一下责任链的实现,让团队中的每个人都可以使用并且遵守项目中的规范。

责任链需要:

  1. 当前的执行者。
  2. 下一个的接收者。
  3. 判断当前执行者执行后是否需要交由下一个执行者。

所以封装成类以后应该是这样:

class Chain {
  constructor(handler) {
    this.handler = handler;
    this.successor = null;
  }

  setSuccessor(successor) {
    this.successor = successor;
    return this;
  }

  passRequest(...args) {
    const result = this.handler(...args);
    if (result === 'next') {
      return this.successor && this.successor.passRequest(...args);
    }
    return result;
  }
}

用 Chain 创建对象时需要将当前的职责方法传入并设置给 handler,并且可以在新对象上用 setSuccessor 把链中的下一个对象指定给 successor,在 setSuccessor 里返回代表整条链的 this,这样在操作的时候可以直接在 setSuccessor 后面用 setSuccessor 设置下一个接收者。

最后,每个通过 Chain 产生的对象都会有 passRequest 来执行当前的职责方法,…arg 会把传入的所有参数变成一个数组,然后一起交给 handler 也就是当前的职责方法执行,如果返回的结果 result 是 next 的话,就去判断有没有指定 sucessor 如果有的话就继续执行,如果 result 不是 next,则直接返回 result

有了 Chain 后代码就会变成:

const httpErrorHandler = (error) => {
  const chainRequest400 = new Chain(response400);
  const chainRequest401 = new Chain(response401);
  const chainRequest403 = new Chain(response403);
  const chainRequest404 = new Chain(response404);

  chainRequest400.setSuccessor(chainRequest401);
  chainRequest401.setSuccessor(chainRequest403);
  chainRequest403.setSuccessor(chainRequest404);

  chainRequest400.passRequest(error);
};

这时就很有链的感觉了,大家还可以再继续根据自己的需求做调整,或是也不一定要使用类,因为设计模式的使用并不需要局限于如何实现,只要有表达出该模式的意图就够了。

责任链的优缺点

优点:

  1. 符合单一职责,使每个方法中都只有一个职责。
  2. 符合开放封闭原则,在需求增加时可以很方便的扩充新的责任。
  3. 使用时候不需要知道谁才是真正处理方法,减少大量的 if 或 switch 语法。

缺点:

  1. 团队成员需要对责任链存在共识,否则当看到一个方法莫名其妙的返回一个 next 时一定会很奇怪。
  2. 出错时不好排查问题,因为不知道到底在哪个责任中出的错,需要从链头开始往后找。
  3. 就算是不需要做任何处理的方法也会执行到,因为它在同一个链中,文中的例子都是同步执行的,如果有异步请求的话,执行时间也许就会比较长。

与策略模式的不同

在前面我还提到过策略模式,先说说两个模式之间的相似处,那就是都可以替多个同一个行为(response400response401 等)定义一个接口(httpErrorHandler),而且在使用时不需要知道最后是谁执行的。在实现上策略模式比较简单。

由于策略模式直接用 if 或 switch 来控制谁该做这件事情,比较适合一个萝卜一个坑的状况。而策略模式虽然在例子中也是针对错误的状态码做各自的事,都在不归自己管的时候直接把事交给下一位处理,但是在责任链中的每个节点仍然可以在不归自己管的时候先做些什么,然后再交给下个节点:

const response400 = (error) => {
  if (error.response.status !== 400) {
    // 先做点什么...
    return 'next';
  }
  console.log('你是不是提交了什么奇怪的东西?');
};