likes
comments
collection
share

JavaScript设计模式之责任链模式

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

概念

在《JavaScript设计模式与开发实践》中是这样介绍责任链模式的:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链处理该请求,知道有一个对象处理它为止

责任链模式案例

接下来,我们通过一个常见的案例来说明到底什么是责任链模式,以及为什么要用责任链模式

案例描述

我们需要做一个售卖手机的电商网站,经过缴纳500元或200元定金后,开始进入购买阶段。而电商网站有如下的优惠模式:

  • 支付过500元定金的用户: 收到100元优惠券
  • 支付过200元定金的用户: 收到50元优惠券
  • 没有支付定金的用户: 普通购买,没有优惠券,且库存有限的情况下不一定能买得到

在页面加载时,我们会得到以下几个字段:

  • orderType: 订单类型,1: 500元定金用户2: 200元定金用户3: 普通用户;
  • pay: 是否已经支付了定金,ture表示已经支付定金,虽然下单是500元用户,但是如果没有支付定金,也只能按照普通用户购买;
  • stock: 普通购买的手机数量库存,已经支付定金的将不会收到此限制;

原始代码

看到上诉需求后,相信大家肯定是可以完成该功能,无非就是if - else没有什么逻辑是解决不了的,接下来可能会写出以下的代码:

var order = function(orderType, pay, stock) {
  if (orderType === 3 || pay === false) {
    // 普通购买用户
    if (stock > 0) {
      console.log('普通购买,无优惠券')
    } else {
      console.log('手机库存不足')
    }
  } else if (orderType === 1) {
      console.log('500元定金用户,100元优惠券')
  } else if (orderType === 2) {
    console.log('200元定金用户,50元优惠券')
  }
}

我对书中的原始代码做了优化,因为感觉书中的原始代码是写的过于复杂了,当然也可能是为了更好的与业务逻辑吻合,先判断orderType,再判断是否支付定金的方式更符合业务逻辑,下面还是贴出来书中的原始代码:

var order = function (orderType, pay, stock) {
  if (orderType === 1) { // 500 元定金购买模式
    if (pay === true) { // 已支付定金
      console.log('500 元定金预购, 得到 100 优惠券');
    } else { // 未支付定金,降级到普通购买模式
      if (stock > 0) { // 用于普通购买的手机还有库存
        console.log('普通购买, 无优惠券');
      } else {
        console.log('手机库存不足');
      }
    }
  }
  else if (orderType === 2) { // 200 元定金购买模式
    if (pay === true) {
      console.log('200 元定金预购, 得到 50 优惠券');
    } else {
      if (stock > 0) {
        console.log('普通购买, 无优惠券');
      } else {
        console.log('手机库存不足');
      }
    }
  }
  else if (orderType === 3) {
    if (stock > 0) {
      console.log('普通购买, 无优惠券');
    } else {
      console.log('手机库存不足');
    }
  }
};

虽然我们实现了需求,但是这样的代码显得更难以阅读,当功能不断迭代,优惠的逻辑越来越复杂的时候,我们的代码最终会变成难以维护的“屎山”,尽管我自己写的代码更少一点,随着优惠逻辑越来越复杂最终也会难以维护。

责任链模式优化

接下来,我们将用责任链模式对这段代码进行优化:

  • 将功能分为三个单独的函数:500元定金函数order500、200元定金函数order200、无定金函数orderNormal
  • orderTypepaystock当作参数分别传给这三个函数
  • 依次通过各个函数,直到可以处理为止,即:order500不满足时,传递给order200, order200不满足时传递给orderNormal

接下来写一下代码来实现这个功能:

var order500 = function(orderType, pay, stock) {
  if (orderType === 1 && pay === true) {
    console.log('500元用户,得到100元优惠券')
  } else {
    order200(rderType, pay, stock)
  }
}

var order200 = function(orderType, pay, stock) {
  if (orderType === 2 && pay) {
    console.log('200元用户,得到50元优惠券')
  } else {
    orderNormal(orderType, pay, stock)
  }
}

var orderNormal = function(orderType, pay, stock) {
  if (stock > 0) {
    console.log('普通用户购买,无优惠券')
  } else {
    console.log('手机库存不足')
  }
}

order500(1, true, 500

优化责任链传递

根据上面的代码,我们将不同的逻辑拆分成了3个互不关联的函数,但是大家也不难发现,处理链条传递的过程非常的僵硬,如果某一天有先后顺序的调整,这样的代码也是很难维护的,这也违反了开放-封闭原则。所以我们需要对个代码进行优化,让他变得更加灵活。

  • 首先,我们对于函数的传递进行一个约定:当不满足条件时,返回固定的字符串nextSuccessor表示继续向后传递
  • 接下来把函数包装进责任链节点
  • 最后指定节点在责任链中的顺序

代码如下:

将三个函数中需要向后传递的部分调整为返回nextSuccessor

var order500 = function (orderType, pay, stock) {
  if (orderType === 1 && pay === true) {
    console.log('500元用户,得到100元优惠券')
  } else {
    return 'nextSuccessor'
  }
}

var order200 = function (orderType, pay, stock) {
  if (orderType === 2 && pay) {
    console.log('200元用户,得到50元优惠券')
  } else {
    return 'nextSuccessor'
  }
}

var orderNormal = function (orderType, pay, stock) {
  if (stock > 0) {
    console.log('普通用户购买,无优惠券')
  } else {
    console.log('手机库存不足')
  }
}

封装了一个Chain方法,用来将之前的三个函数进行包装,setNextSuccessor函数用来指定责任链中的下一个节点,而passRequest函数则用来一次执行各个责任链中的节点,直到找到可以处理的函数。

var Chain = function (fn) {
  this.fn = fn;
  this.successor = null;
}

Chain.prototype.setNextSuccessor = function (successor) {
  return this.successor = successor;
}

Chain.prototype.passRequest = function () {
  var ret = this.fn.apply(this, arguments);
  if (ret === 'nextSuccessor') {
    return this.successor && this.successor.passRequest.apply(this.successor, arguments);
  }
  return ret;
}

下面,封装三个节点,并指定它们的顺序:

var chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var chainOrderNormal = new Chain(orderNormal);

// 指定顺序
chainOrder500.setNextSuccessor(chainOrder200);
chainOrder200.setNextSuccessor(chainOrderNormal);

// 执行
chainOrder500.passRequest(1, false, 50)

使用责任链模式优化后的代码,虽然看起来更多,但更利于后续的维护,我们只需要关注各个小函数的逻辑,如果需要新增一个300元优惠券的逻辑,我们也只需要创建一个order300的函数,然后封装成节点chainOrder300,设置其顺序chainOrder500.setNextSuccessor(chainOrder300)

异步责任链

根据上面的代码,我们可以实现通过函数中返回固定字符串nextSuccessor来表示是否需要传递给下一个节点,而在实际的业务中,我们可能经常会遇到异步问题。比如在节点中发起一个ajax请求,根据请求结果才能判断是否继续在责任链中passRequest传递给下一个节点。

所以,我们需要再新增一个next方法,手动传递给责任链下一个节点:

Chain.prototype.next = function() {
  return this.successor && this.successor.passRequest.apply( this.successor, arguments );
}

假设,order500需要延迟一秒后再传递给下一个节点

var order500 = function (orderType, pay, stock) {
  if (orderType === 1 && pay === true) {
    console.log('500元用户,得到100元优惠券')
  } else {
    setTimeout(() => {
      this.next(orderType, pay, stock);
    }, 1000);
  }
}

chainOrder500.passRequest(1, false, 50)

效果如下:

JavaScript设计模式之责任链模式

可以看到异步功能也实现了。

源码中的责任链模式

express中间件

回顾中间件

Express官网中是这样介绍中间件的:

中间件函数能够访问请求对象(req)、响应对象(res)以及应用程序的请求/响应循环中的下一个中间件函数。下一个中间件函数通常由名为 next 的变量来表示。如果当前中间件函数没有结束请求/响应循环,那么它必须调用 next(),以将控制权传递给下一个中间件函数。否则,请求将保持挂起状态

写一个文档中的例子来回顾一下中间件的作用:

var app = express();

app.use(function (req, res, next) {
  console.log('Time:', Date.now());
  next();
});

app.use('/user/:id', function (req, res, next) {
  console.log('Request Type:', req.method);
  next();
});

app.get('/user/:id', function (req, res, next) {
  res.send('USER');
});

上面的代码中,第一个中间件会在每次收到请求时执行,第二个中间件会在 /user/:id 路径中为任何类型的 HTTP 请求执行,第三个则会针对处理 /user/:idGET请求。对于前两个中间件,需要通过next函数依次向下执行,这样的执行逻辑与我们这次介绍的责任链模式的非常相似。那么,我们接下来就看下Express中是如何实现这样的逻辑的。

中间件原理

为了便于了解expressnext的实现原理,我们写一个简单的demo进行调试,代码如下:

var express = require('express');
var app = express();

app.use(function (req, res, next) {
  next();
})

app.get('/search', function (req, res) {
  res.send({
    query: req.query,
    params: req.params,
  });
});


app.listen(3000);

执行express, express初始化

当我们执行express函数时,首先express会进行一个初始化,源码如下:

// /lib/express.js

function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

  mixin(app, EventEmitter.prototype, false);
  mixin(app, proto, false);

  // expose the prototype that will get set on requests
  app.request = Object.create(req, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  // expose the prototype that will get set on responses
  app.response = Object.create(res, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  app.init();
  return app;
}

在研究中间件实现的过程中,初始化部分的源码并不重要,只需要知道当服务接收到一个请求的时候,实际会去执行handle函数即可。

执行app.use,初始化Router

当我们执行app.use时,会执行lazyrouter函数创建一个Router实例,接下来会遍历use中的函数,并调用router.useRouter添加中间件,

lazyrouter源码如下:

// lib/application.js

app.lazyrouter = function lazyrouter() {
  if (!this._router) {
    this._router = new Router({
      caseSensitive: this.enabled('case sensitive routing'),
      strict: this.enabled('strict routing')
    });

    this._router.use(query(this.get('query parser fn')));
    this._router.use(middleware.init(this));
  }
};

router.use源码如下:

// lib/router/index.js

proto.use = function use(fn) {
  var offset = 0;
  var path = '/';

  var callbacks = flatten(slice.call(arguments, offset));

  for (var i = 0; i < callbacks.length; i++) {
    var fn = callbacks[i];

    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);

    layer.route = undefined;
    this.stack.push(layer);
  }

  return this;
};

在源码中可以了解到,当我们执行app.use时,会做以下几件事情:

  • 当我们使用app.use创建中间件时,express源码中会将函数构建成一个Layer实例添加stack

  • 初始化Router实例后,express默认创建了两个的Layer实例存入stack

执行app.get,注册中间件

当我们执行app.get函数时,在express内部实际执行了router.route函数,最终会创建一个Layer实例存入stack中, 源码如下:

express中对各种类型的请求添加了函数:

// lib/application.js

methods.forEach(function(method){
  app[method] = function(path){
    this.lazyrouter();

    var route = this._router.route(path);
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});

通过源码可以看到,内部会调用router.route函数,接下来再了解一下这部分的源码:

// lib/router/index.js

proto.route = function route(path) {
  var route = new Route(path);

  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.dispatch.bind(route));

  layer.route = route;

  this.stack.push(layer);
  return route;
};

此时,可以看到,在route函数中,express会将函数构造化成一个Layer实例,并存入stack中。

到这里,demo服务算是启动完成了,此时我们可以通过调试看到,stack中存在4个Layer,分别是内部的两个和demo代码中app.useapp.get函数执行后的两个,如图所示:

JavaScript设计模式之责任链模式

处理请求,中间件实现原理

接下来,我们再看一下当express接收到一个请求时,内部会如何处理。这部分逻辑也是了解express中间件原理的核心,当我们收到一个请求时,此时会执行app.handle(req, res, next)函数,app.handle源码如下:

// lib/application.js

app.handle = function handle(req, res, callback) {
  var router = this._router;

  router.handle(req, res, done);
};

在这段代码中,我们可以看到done函数实际就是我们demo代码中的next函数,接下来会调用到router.handle函数,我们再看下router.handle函数的源码,由于这部分源码较多,我会将一部分用不到的逻辑省略到,接下来是删减后的源码部分:

// lib/router/index.js

proto.handle = function handle(req, res, out) {
  var self = this;
  var idx = 0;
 
  var stack = self.stack;
  req.next = next;
  
  next();
  
  function next(err) {
    var layerError = err === 'route'
      ? null
      : err;

    // find next matching layer
    var layer;
    var match;
    var route;

    while (match !== true && idx < stack.length) {
      layer = stack[idx++];
      match = matchLayer(layer, path);
      route = layer.route;

      if (match !== true) {
        continue;
      }

      if (!route) {
        // process non-route handlers normally
        continue;
      }
    }

    // no match
    if (match !== true) {
      return done(layerError);
    }

    // store route for dispatch on change
    if (route) {
      req.route = route;
    }

    // Capture one-time layer values
    req.params = self.mergeParams
      ? mergeParams(layer.params, parentParams)
      : layer.params;
    var layerPath = layer.path;

    // this should be done for the layer
    self.process_params(layer, paramcalled, req, res, function (err) {
      if (err) {
        next(layerError || err)
      } else if (route) {
        layer.handle_request(req, res, next)
      } else {
        trim_prefix(layer, layerError, layerPath, path)
      }
    });
  }
}

在源码中可以看到最重要的部分就是需要执行next函数,下面我们要将next函数分开几个部分分别来看:

  • 首先,在next函数中有个while循环:
while (match !== true && idx < stack.length) {
  layer = stack[idx++];
  match = matchLayer(layer, path);
  route = layer.route;

  if (match !== true) {
    continue;
  }

  if (!route) {
    // process non-route handlers normally
    continue;
  }
}

在这段精简后的逻辑中,我们可以看到,这个循环就是在stack中找到第一个匹配的layer,当matchtrue也就是匹配到的时候,则会跳出循环执行后面的逻辑。带入到我们的demo案例中,当我们发起了一个/search的请求时,实际在刚才stack中的四个layer都满足条件,由于前面两个是express内部的中间件,我们就从第三个layer的逻辑来思考。

  • 当跳出循环后,我们会进入到后面的逻辑,源码如下:
    // Capture one-time layer values
    req.params = self.mergeParams
      ? mergeParams(layer.params, parentParams)
      : layer.params;
    var layerPath = layer.path;

    // this should be done for the layer
    self.process_params(layer, paramcalled, req, res, function (err) {
      if (err) {
        next(layerError || err)
      } else if (route) {
        layer.handle_request(req, res, next)
      } else {
        trim_prefix(layer, layerError, layerPath, path)
      }
    });

这部分逻辑中,我们可以看到就是对params进行了赋值后,执行process_params函数。而在我们的demo代码中,process_params直接执行了done,也就是执行process_params的最后传入的回调函数,根据前面stack中的四个Layer截图,我们发现只有最后一个Layer是有route的,也就是说前面几个中间件执行时route实际为undefined,所以将会执行trim_prefix函数。

最终函数会执行到layer.handle_request(req, res, next), layer.handle_request的源码如下:

  // lib/router/layer.js

  Layer.prototype.handle_request = function handle(req, res, next) {
    var fn = this.handle;

    try {
      fn(req, res, next);
    } catch (err) {
      next(err);
    }
  };

我们可以看到,接下来我们将会执行fn,也就是demo中传入的中间件函数,当我们在代码中调用next函数时,此时执行的是handle中创建的next函数,并从下一个Layer开始(stack[id++])继续遍历找到满足条件的Layer并执行。这样在各个中间件中调用next时,实际就是不断从stack中取出下一个Layer,满足执行条件就会跳出循环执行这个中间件

原理总结

根据刚才的源码分析,我们总结出为什么当我们调用next函数时会自动执行到下一个中间件函数:

  • 首先,我们将所有的函数都添加到stack中,每一个函数为一个Layer实例
  • 在调用时执行handle函数,handle内部创建一个next函数,通过next函数获取stack中的下一个满足执行条件的Layer
  • 当中间件函数主动调用next函数时,会找到下一个满足条件的Layer并执行(layer.handle_request(req, res, next)),依次类推直到全部执行完成

以上就是对express中间件实现方式的源码分析,可以发现通过调用next控制执行下一个中间件的逻辑是比较简单。

总结

合理的使用责任链模式可以很好的帮助我们管理代码,降低发起请求的对象和处理请求对象之间的耦合性。责任链中的节点数量和顺序是可以自由变化的,我们可以在运行时决定链中包含哪些节点。 在前端领域中,也有很多责任链模式的影子,例如:作用域链、原型链、DOM节点中的事件冒泡。 希望大家可以通过前面的案例和express中间件的实现原理对责任链模式有一个更深刻的认识,能够在遇到类似场景时想到使用责任链模式。不论是使用案例中setNextSuccessor初始化执行顺序还是像express源码中那样通过一个stack规定执行顺序,只要适合自己的场景那就是最好的。

感谢阅读🙏

转载自:https://juejin.cn/post/7268622342728335412
评论
请登录