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

阿轩的BUG
文章191
评论56
标签101
站点日志
作者总数:1位
置顶文章:0篇
标签总数:101条
文章总数:196篇
评论总数:56条
微语总数:149条
运行天数:2314天
最近更新:2024-09-30
文章标签
最新评论
Sherlock
2025-08-21
不是应该打包成apk文件吗,怎么突然就运行编译了,下一步呢
老金er
2025-05-25
人生设计非常必要,在信息化发达的今天,周围大部分人依然只能过着人生工程模式生活,长时间生活洗礼后,尽管“人生工程”已经不再适合很多人,他们也无法做出任何改变,人生设计给我们提供了新的思路,值得尝试
赏帮赚
2025-05-25
职场太多的尔虞我诈
三笑
2024-08-16
emlog6.0.1可以升级支持吗?
pony
2024-07-08
这篇文章提供了一种新的人生规划方法——"人生设计",它借鉴了产品设计的理念,鼓励人们以创造性和探索性的方式思考和规划自己的生活。以下是对这篇文章的几个评价维度: 创新性:将产品设计的理念应用于人生规划是一个新颖的视角,为人们提供了一种跳出传统生活轨迹,探索个性化生活路径的方法。 实用性:文章不仅提出了理念,还详细介绍了具体的实施步骤,包括自我评估、制定计划、原型设计和实践选择等,具有很强的操作性。 启发性:通过强调好奇心、不断尝试、重新定义问题等心态,文章鼓励人们打破常规,勇于探索未知,这对于激发个人潜能和创造力具有积极作用。 适应性:"人生设计"理念适用于不同人生阶段和不同背景的人,无论是希望从头开始的人,还是已经取得一定成就但希望探索新方向的人,都可以从中获得启发。 情感共鸣:文章对许多人对传统生活轨迹的不满和对改变的渴望进行了深刻的洞察,能够引起读者的共鸣。 可持续性:人生设计强调的是一个持续迭代和试错的过程,这与现实生活的发展规律相吻合,有助于人们在不断变化的环境中做出适应性调整。 教育意义:作为一种人生规划方法,"人生设计"可以作为教育的一部分,帮助年轻人更早地学会自我探索和规划,为未来的生活和职业发展打下基础。 局限性:尽管"人生设计"提供了一种新的思考框架,但它可能需要个人具备一定的自我认知和反思能力,对于一些缺乏自我探索经验的人来说,可能需要额外的指导和支持。 总体来说,这篇文章提供了一种有价值的人生规划方法,对于那些渴望改变、寻求个性化生活的人来说,是一种有益的参考和指导。
首页实用干货 🌋

前言

在开始学习责任链之前,先看一下在开发中常见的问题。下面是前端用来处理 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 只需要按照顺序执行 response400response404 就行了,反正该执行就执行,不该执行的也只是直接 return 而已。

实现真正的责任链

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

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

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

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

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

优化执行

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

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

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. 使用时候不需要知道谁才是真正处理方法,减少大量的 ifswitch 语法。

缺点:

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

与策略模式的不同

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

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

const response400 = (error) => {
  if (error.response.status !== 400) {
    // 先做点什么...
    return 'next';
  }
  console.log('你是不是提交了什么奇怪的东西?');
};
上一篇
💫 前端监控的搭建步骤,别再一头雾水了!
下一篇
🎉 360Kan接口封装