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

更新日期: 2021-07-06阅读: 1.4k标签: 代码

目的

在开始学习责任链之前,先看一下在开发中常见的问题。下面是前端用来处理 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 !== 400) return 'next';
  console.log('你是不是提交了什么奇怪的东西?');
};

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

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

const response404 = (error) => {
  if (error.response.status !== 404) return '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. 就算是不需要做任何处理的方法也会执行到,因为它在同一个链中,文中的例子都是同步执行的,如果有异步请求的话,执行时间也许就会比较长。

与策略模式的不同

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

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

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

那在什么场景下使用呢?比如有一天你觉得世界那么大,应该去看看,在离职时需要走一个签字流程:你自己、你的 Leader 还有人资都需要做签字这件事,所以责任链就可以把这三个角色的签字过程串成一个流程,每个人签过后都会交给下面一位,一直到人资签完后才完成整个流程。而且如果通过责任链处理这个流程,不论之后流程怎样变动或增加,都有办法进行弹性处理。

上面的例子的需求是策略模式所无法胜任的。

本文首发微信公众号:前端先锋


链接: https://www.fly63.com/article/detial/10507

不要浪费时间写完美代码

一个系统可以维持5年,10年,甚至20年以上,但是代码和设计模式的生命周期非常短,当对一个解决方案使用不同的方法进行迭代的时候,通常只能维持数月,数日,甚至几分钟的时间

Google内部在代码质量上的实践

良好的编程习惯涉及到很多方面,但在软件行业内,大多数的公司或组织都不会把良好的编程习惯列为主要关注点。 例如,具有可读性和可维护性的代码比编写好的测试代码或使用正确的工具更有意义,前者的意义在于可以让代码更易于理解和修改。

减少嵌套,降低代码复杂度

减少嵌套会让代码可读性更好,同时也能更容易的找出bug,开发人员可以更快的迭代,程序也会越来越稳定。简化代码,让编程更轻松!

关于 Google 发布的 JS 代码规范

Google为了那些还不熟悉代码规范的人发布了一个JS代码规范。其中列出了编写简洁易懂的代码所应该做的最佳实践。代码规范并不是一种编写正确JavaScript代码的规则,而是为了保持源代码编写模式一致的一种选择。

你解决的问题比你编写的代码更重要!

程序员似乎忘记了软件的真正目的,那就是解决现实问题。您编写的代码的目的是为了创造价值并使现有世界变得更美好,而不是满足您对自我世界应该是什么的以自我为中心的观点。有人说:如果你拥有的只是一把锤子,那么一切看起来都像钉子一样

tinymce与prism代码高亮实现及汉化的配置

TinyMCE是一个轻量级的基于浏览器的所见即所得编辑器,由JavaScript写成。它对IE6+和Firefox1.5+都有着非常良好的支持。功能方强大,并且功能配置灵活简单。另一特点是加载速度非常快的。

js函数式编程与代码执行效率

函数式编程对应的是命令式编程, 函数式编程的核心当然是对函数的运用. 而高阶函数(Higher-order)是实现函数式编程的基本要素。高阶函数可以将其他函数作为参数或者返回结果。所以JS天生就支持函数式编程

接手代码太烂,要不要辞职?

朋友发表了一条说说:入职新公司,从重构代码到放弃”,我就问他怎么了?他说,刚进一家新公司,接手代码太烂,领导让我先熟悉业务逻辑,然后去修复之前项目中遗留的bug,实在不行就重构

js高亮显示关键词_页面、搜索关键词高亮显示

页面实现关键词高亮显示:在项目期间遇到一个需求,就是搜索关键词时需要高亮显示,主要通过正则匹配来实现页面关键词高亮显示。在搜索结果中高亮显示关键词:有一组关键词数组,在数组中筛选出符合关键字的内容并将关键字高亮

写优雅的代码,做优雅的程序员

软件工程学什么? 学计算机,写程序,做软件,当程序员。听说学计算机很辛苦? 是的,IT行业加班现象严重。在计算机世界里,技术日新月异,自学能力是程序员最重要的能力之一。选了这个专业,就要时刻保持好奇心和技术嗅觉,不能只满足于完成课内作业。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!