设计模式在前端开发中的实践(八)——职责链模式

职责链模式

职责链模式可以说是前端中几乎最常见的设计模式了,但是可能大家并没有注意到这就是职责链模式,像日常开发中的Promise链式调用(异步请求神器axios的请求或响应拦截器就是基于Promise链实现的。),像Rx.js这类库。

1、基本概念

职责链模式:使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系。将这个对象连成一条链,直到有一个对象处理它为止。

当客户提交一个请求时,请求是沿着一个业务处理链进行传递。

另外,如果业务代码足够复杂,并且每个if-else分支都有大量的逻辑需要处理,此时使用职责链模式对代码进行重构就比较划算了。因为它可以抹平if-else分支,从而可以使得代码的业务逻辑看起来比较直观(每个处理节点只关心对应的逻辑,无法处理则向后传递,代码单一化职责),但是如果业务逻辑简单的if-else经过这么一个大动干戈的重构,有点儿得不偿失。

职责链模式的UML图如下所示:

其含义是:具体的业务逻辑处理节点实现接口,每个业务逻辑节点有一个后继节点,在当前节点处理完成之后(或不能处理),委托给它的后继节点处理。

如果职责链构建的太长,那处理效率必然会下降,在实际开发中,我们需要注意取舍。

2、代码范式

ts 复制代码
abstract class Component {
  successor?: Component;

  setSuccessor(next: Component) {
    this.successor = next;
  }

  abstract exec(target: Map<string, string>): void;
}

class ComputerComponent extends Component {
  exec(target: Map<string, string>): void {
    target.set("computer", "Windows 98");
    console.log("计算机行业处理");
    this.successor?.exec(target);
  }
}

class FinanceComponent extends Component {
  exec(target: Map<string, string>): void {
    target.set("finance", "招商银行600036");
    console.log("金融行业处理");
    this.successor?.exec(target);
  }
}

class MedicalComponent extends Component {
  exec(target: Map<string, string>): void {
    target.set("medical", "APTX 4869");
    console.log("医药行业处理");
    this.successor?.exec(target);
  }
}

class AgricultureComponent extends Component {
  exec(target: Map<string, string>): void {
    target.set("agriculture", "杂交水稻");
    console.log("农业行业处理");
    this.successor?.exec(target);
  }
}

class Invoker {
  chian: Component;

  constructor(component: Component) {
    this.chian = component;
  }

  doWork() {
    const map = new Map();
    this.chian.exec(map);
    console.log(map);
  }
}

const computer = new ComputerComponent();
const finance = new FinanceComponent();
const medical = new MedicalComponent();
const agriculture = new AgricultureComponent();

computer.setSuccessor(finance);
finance.setSuccessor(medical);
medical.setSuccessor(agriculture);

const invoker = new Invoker(computer);

invoker.doWork();

每个处理节点可以根据自己的业务需求选择处理当前任务再将其委托给下一个处理节点,也可以根据业务直接忽略当前处理委托给一下处理节点(示例是需要处理的方式)

3、在前端开发中的实践

3.1 axios的拦截器设计

在前端中,我们可能每天都在使用Promise,如果你研究过Promise,你会发现,Promise就是基于职责链模式设计的,而我们给then方法中部署回调函数则就是每个职责节点的处理器,因为then方法返回一个新的Promise可以为其委托新的业务处理节点。

axios的拦截器管道就是基于Promise进行设计的,具体源码节选如下:

js 复制代码
// 文件位置: axios>lib>core>Axios.js 版本------>0.21.1
// 以省略无关代码
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  // 注册两个拦截器的管理器
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager(),
  };
}

Axios.prototype.request = function request(config) {
  // Hook up interceptors middleware
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  // 处理请求拦截器
  this.interceptors.request.forEach(function unshiftRequestInterceptors(
    interceptor
  ) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  // 处理响应拦截器
  this.interceptors.response.forEach(function pushResponseInterceptors(
    interceptor
  ) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  // 部署一个Promise链,类似链表的头插法操作,像多米罗骨牌那样的链式反应,是反向部署的
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
  // 返回最开头的那个Promise给外界,到时候就可以起到导火索的作用
  return promise;
};

但是Promise构建的职责链有一个缺点,就是一旦部署好了,就不再从中删除或增加节点,并且Promise链就像鞭炮一样,如果你将其点燃了(因为Promise的状态只会从pendingresolvedrejected),它就会从头到尾噼里啪啦的炸到结束,关键就是炸到结束这串鞭炮就没了,我想再点一串,就得从新再买一串鞭炮。

比如:

js 复制代码
let trigger = null;

function demo() {
  return new Promise((resolve) => {
    trigger = resolve;
  });
}

const d = demo();

d.then(() => {
  console.log(1);
}).then(() => {
  console.log(2);
});

trigger(); // 只会触发一次输出1, 2
trigger();
trigger();

在有些时候,如果业务处理链可能有变动的时候,用Promise可能就不太符合我们的需求了,这个时候,真的就要自己写一下职责链模式的范式了。

实现方式和装饰模式AOP范式几乎差不多,根据返回结果决定是否将业务逻辑抛给下一个处理节点。

js 复制代码
Function.prototype.after = function (fn) {
  let self = this;
  return function () {
    let ret = self.apply(this, arguments);
    // 根据上一个职责处理的返回决定是否将其向后抛
    if (ret === "nextSuccessor") {
      return fn.apply(this, arguments);
    }
    return ret;
  };
};

基于AOP实现模拟的一个业务场景:

js 复制代码
function Computer() {
  console.log("计算机行业处理");
  return "nextSuccessor";
}

function Finance() {
  console.log("金融行业处理");
  return "nextSuccessor";
}

function Medical() {
  console.log("医药行业处理");
  return "nextSuccessor";
}

function Agriculture() {
  console.log("农业行业处理");
}
// 构建职责链
let chain = Computer.after(Finance).after(Medical).after(Agriculture);
chain();

上述这个职责链可以触发多次,不存在像Promise链那样的问题,而且可以随时应对业务的修改。

所以实际开发中可以根据自己的需求选择是直接使用Promise链还是自己实现职责链模式。

以上代码改编自曾探老师所著的《JavaScript设计模式与开发实践》,但是在实际开发中,有些团队要求不能修改原型对象,因此还可以考虑别的实现方式,接下来继续看别的实现方式。

3.2 表单提交的处理

在实际前端的开发中,我们会有遇到表单的内容会比较复杂的场景。

比如,在提交之前,前端需要做提交的验证;

比如在提交之前,需要做一些数据的转换,比如,有些时候,我们在开发表单的时候,为了我们编程的方便,我们有些时候可能是做的是一个对象,后端要求我们传的是一个JSON.stringify之后的结果(或者相反);

有些时候,还需要在提交之前,最后做一些XSS的验证,比如用户输入的内容需要被回显,我们需要修改用户的不合法输入。

上面的处理,如果写在一起的话,会使得我们的代码杂,变的难以维护。而且,也不是所有的表单都需要应用数据转换(另外,甚至数据转换的规则有可能也是可以进行抽象的),因此,从代码的可维护性来看的话,这个场景使用职责链模式来改写较为合适。

js 复制代码
class FormHandler {
    constructor() {
        this.nextHandler = null;
    }

    setNext(handler) {
        this.nextHandler = handler;
        return handler;
    }

    handle(request) {
        if (this.nextHandler) {
            return this.nextHandler.handle(request);
        }
    }
}

class ValidationHandler extends FormHandler {
    handle(request) {
        console.log("Validating form...");
        // 进行验证,假设验证总是成功的
        if(xxx) {
            throw new Error('validation error');
        }
        return super.handle(request);
    }
}

class DataTransformHandler extends FormHandler {
    handle(request) {
        console.log("Transforming data...");
        // 进行数据转换,这里仅作演示
        request.transformed = true;
        return super.handle(request);
    }
}

class XSSProtectionHandler extends FormHandler {
    handle(request) {
        console.log("Applying XSS Protection...");
        // 进行XSS转换,这里简化处理
        if(request.data) {
            request.data = request.data.replace(/<script.*?>.*?<\/script>/gi, "");
        }
        return super.handle(request);
    }
}

应用职责链处理表单数据:

js 复制代码
const form = { data: 'Some user input <script>alert("xss")</script>' };
// 创建职责链
const validator = new ValidationHandler();
const transformer = new DataTransformHandler();
const xssProtector = new XSSProtectionHandler();
// 设置职责链顺序
validator.setNext(transformer).setNext(xssProtector);
// 处理表单
let processedForm = null;
try {
    processedForm = validator.handle(form);
} catch(exp) {
    console.log(exp)
}
// 这个form就可以放心的发送给后端了
console.log(processedForm);

上述代码每个处理器都关注于单一任务,使得代码易于理解和维护。此外,这种模式使得未来在处理链中添加或移除步骤变得简单,提高了代码的可扩展性和灵活性。例如,如果需要添加一个新的表单处理步骤(如日志记录),只需创建一个新的处理器类并将其添加到链中。

3.3 NestJS中的过滤器

在NestJS中,过滤器的设计就完美的使用了职责链模式的处理流程。

过滤器有几种绑定的形式,如果还不太了解的同学,可以先查看我之前的这篇文章记录我的NestJS探究历程(六)------过滤器

我们添加的过滤器就像是一个一个的职责节点,过滤器的先后顺序是方法过滤器到控制器过滤器,最后是全局过滤器。如果请求过程发生错误,被一个过滤器节点被捕获了的话,则后面的过滤器则不会处理了,若没有(过滤器有自己关注的过滤器类型),则会接着被后面的过滤器捕获。

比如,下面是一个只捕获HttpException的过滤器

ts 复制代码
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}

在这个过滤器中,我们可以处理一些业务,然后返回给用户错误的信息。若不想把这个错误吃掉,还可以继续throw这个错误给后面的过滤器处理。

一般我们会设置一个全局的捕获任意错误类型的过滤器,这就像上文提到的,我们设置了一个兜底的过滤器,避免遗漏对捕获的错误。

不过,一般NestJS框架的设计者不会轻易的相信他的用户会那么自觉的处理异常,所以它会再给我们的逻辑添加一层异常的捕获,避免因报错导致程序的异常退出。

3.4 Express的中间件

Express的中间件管理也是职责链模式的体现,只不过有一点儿区别的就是,在use方法里面,如果我们不调用next方法,请求会被挂起。

回到之前的思路,假设命中一个检验的中间件,在执行的过程中,不能通过简要,我们就可以直接调用Response对象设置要向前端返回的内容,然后调用end方法终止请求。如果一切正常,前面的中间件完成了自己的职责之后,将任务向后继的中间传递,从而完成一些定制的逻辑。

最后,再看一下Express中间件的一个模拟实现。

ts 复制代码
type Request = any; // 模拟 Express 的 Request 类型
type Response = any; // 模拟 Express 的 Response 类型
type NextFunction = () => void;

class MyExpress {
  private middlewares: Array<
    (req: Request, res: Response, next: NextFunction) => void
  > = [];

  use(
    middleware: (req: Request, res: Response, next: NextFunction) => void,
  ): void {
    this.middlewares.push(middleware);
  }

  // 模拟 Express 的处理请求方法
  handleRequest(req: Request, res: Response): void {
    let index = 0;

    // 定义如何执行中间件
    const next: NextFunction = () => {
      if (index < this.middlewares.length) {
        const middleware = this.middlewares[index];
        index++;
        middleware(req, res, next);
      }
    };

    next();
  }
}

// 使用示例
const app = new MyExpress();
app.use((req, res, next) => {
    console.log('Middleware 1');
    next(); // 显式调用 next
});
app.use((req, res, next) => {
    console.log('Middleware 2');
    // 这里不调用 next,请求处理到此结束
});
app.handleRequest({}, {}); // 模拟处理请求

总结

职责链模式的优点包括:

  1. 分离关注点:每个处理器只关注于单一的任务,这使得代码更加清晰和易于维护。
  2. 灵活性:可以轻松地在链中添加或移除处理器,而不影响其他处理器。
  3. 可扩展性:随着应用的发展,可以方便地扩展或修改处理链。
  4. 简化复杂过程:将复杂的表单处理流程拆分为更小、更易管理的部分。

通过这种方式,职责链模式在处理具有多个步骤的复杂流程时提供了一个清晰且有效的解决方案。

使用职责链模式需要注意的一个问题就是职责链不能设置的太长,否则会降低处理的效率;另外就是一定要注意有一个兜底的处理逻辑。

相关推荐
与衫2 分钟前
掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系
android·javascript·sql
金灰3 分钟前
HTML5--裸体回顾
java·开发语言·前端·javascript·html·html5
茶卡盐佑星_6 分钟前
说说你对es6中promise的理解?
前端·ecmascript·es6
Манго нектар34 分钟前
JavaScript for循环语句
开发语言·前端·javascript
蒲公英100141 分钟前
vue3学习:axios输入城市名称查询该城市天气
前端·vue.js·学习
天涯学馆1 小时前
Deno与Secure TypeScript:安全的后端开发
前端·typescript·deno
以对_1 小时前
uview表单校验不生效问题
前端·uni-app
Zheng1132 小时前
【可视化大屏】将柱状图引入到html页面中
javascript·ajax·html
程序猿小D2 小时前
第二百六十七节 JPA教程 - JPA查询AND条件示例
java·开发语言·前端·数据库·windows·python·jpa
john_hjy2 小时前
【无标题】
javascript