编写可维护代码之“中间件模式”

引言

此次我们谈论的中间件,针对前端和 Node 的 Express 和 Koa 开发而言。对于严格意义上的中间件(平台与应用之间的通用服务),例如用于缓解后台高访问量的消息中间件,本篇不会去叙述,因为不是本篇的论述意图。

言归正传,当我们在编写业务代码时候,我们无法避免有些业务逻辑复杂而导致业务代码写得又长又乱,如果再加上时间紧凑情况下写出来的代码估计会更让人抓狂。以至于我们一直在寻求更好的架构设计和更好的代码设计,这是一个没有终点的求知之路,但是在这条路上会越走越好。

1. AOP

AOP 意为面向切面编程,是在 Java 的 Spring 框架的重点内容,其作用如下图所示:

根据上图,整个响应 http 过程可以看做是一条串联的管道,对于每个 http 请求我们都想插入相同的逻辑例如数据过滤、日志统计的目的,为了不和业务逻辑混淆一块,提高代码复用率,AOP 提倡从横向切面思路向管道某个位置插入一段代码逻辑,这样就实现在任何业务逻辑前后都有相同代码逻辑段,开发者只需专注写业务逻辑,既不影响整个响应 http 过程,而且隔离了业务逻辑,实现高内聚低耦合原则。

可以说 AOP 对 OOP 进行了一个补充,OOP 是对做同一件事情的业务逻辑封装成一个对象,但是做一件事情过程中又想做别的事情对 OOP 来说难以解决。就像上图所示,当系统在响应用户修改信息的请求时,系统在业务处理之前对用户提交的数据做了安全过滤,业务处理之后还要做日志统计。相反如果把所有逻辑都柔合在一起,每次写业务都需重复编写数据过滤和日志统计的代码,违反了单一职责,高内聚低耦合的原则,并且降低代码复用率。

在前端,我们可以借用这种思想通过 before 和 after 函数来实现,我们看下代码实现:

Function.prototype.before = function(fn){//函数处理前执行fn
  var self = this;
   return function(){
     fn.call(this);
     self.apply(this, arguments);
   }
}
Function.prototype.after = function(fn){//函数处理后执行fn
  var self = this;
   return function(){
     self.apply(this, arguments);
     fn.call(this);
   }
}

实现思路是对被处理的函数通过闭包封装在新的函数里,在新的函数内部按照顺序执行传入的参数fn和被处理的函数。

举个栗子:

用户提交表单数据之前需要用户行为统计,代码应该是这样写:

function report(){
   console.log('上报数据');
}
function submit(){
   console.log('提交数据');
}
submit.before(report)(); //提交之前执行report
//结果: 上报数据
//      提交数据

从代码可以看出已经把统计和数据提交业务隔离起来,互不影响。

但是如果提交数据之前,需要数据验证并且依据验证结果判断是否能提交,怎么做?这里要改动before函数,看下代码:

Function.prototype.before = function(fn){//函数处理后执行fn
  var self = this;
   return function(){
     var res = fn.call(this);
     if(res)//返回成功则执行函数
       self.apply(this, arguments);
   }
}
function report(){
   console.log('上报数据');
   return true;
}
function validate(){
   console.log('验证不通过');
   return false;
}
function submit(){
   console.log('提交数据');
}

submit.before(report).before(validate)();
//结果: 
// 验证不通过 
function report(){
   console.log('上报数据');
   return true;
}
function validate(){
   console.log('验证通过');
   return true;
}
function submit(){
   console.log('提交数据');
}

submit.before(report).before(validate)();
//结果: 
// 验证通过
// 上报数据
// 提交数据

AOP思想在前端分解隔离业务已经做到位了,但是却有了一串长长的链式出来,如果处理不当很容易让维护者看晕,例如下面这样:

//提交数据前,验证数据,然后上报,在提交之后做返回首页的跳转
function report(){
   console.log('上报数据');
   return true;
}
function validate(){
   console.log('验证通过');
   return true;
}
function submit(){
   console.log('提交数据');
}
function goBack(){
   console.log('返回首页')
}
submit.before(report).before(validate).after(goBack)();
//结果: 
// 验证通过
// 上报数据
// 提交数据

栗子可能并没有那么晕,但是也得仔细看才能看懂整个流程,实际开发中估计会有更麻烦情况出现,另外,如果 before 或after 的参数 fn 是一个异步操作的话,又需要做些 patch,显然还是有些不足的,那么还有没有其他解决办法呢,既能隔离业务,又能方便清爽地使用~我们可以先看看其他框架的中间件解决方案。

2. express 与 koa的中间件

express 和 koa 本身都是非常轻量的框架,express 是集合路由和其他几个中间件合成的 web 开发框架,koa 是 express 原班人马重新打造一个更轻量的框架,所以 koa 已经被剥离所有中间件,甚至连 router 中间件也被抽离出来,任由用户自行添加第三方中间件。express 和 koa 中间件原理一样,我们就抽 express 来讲。

我们先看下express中间件写法:

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

app.use(function(req, res, next) {
  console.log('数据统计');
  next();//执行权利传递给
});

app.use(function(req, res, next) {
  console.log('日志统计');
  next();
});

app.get('/', function(req, res, next) {
  res.send('Hello World!');
});

app.listen(3000);
//整个请求处理过程就是先数据统计、日志统计,最后返回一个Hello World!

上图运作流程图如下:

从上图来看,每一个“管道”都是一个中间件,每个中间件通过next方法传递执行权给下一个中间件,express就是一个收集并调用各种中间件的容器。

中间件就是一个函数,通过 express 的 use 方法接收中间件,每个中间件有 express 传入的 req , res 和 next 参数。如果要把请求传递给下一个中间件必须使用next()方法。当调用res.send方法则此次请求结束,node 直接返回请求给客户,但是若在res.send方法之后调用 next 方法,整个中间件链式调用还会往下执行,因为当前 hello world 所处的函数也是一块中间件,而 res.send 只是一个方法用于返回请求。

3. 借用中间件

我们可以借用中间件思想来分解我们的前端业务逻辑,通过 next 方法层层传递给下一个业务。做到这几点首先必须有个管理中间件的对象,我们先创建一个名为 Middleware 的对象:

function Middleware(){
   this.cache = [];
}

Middleware通过数组缓存中间件。下面是next和use方法:


Middleware.prototype.use = function(fn){
  if(typeof fn !== 'function'){
    throw 'middleware must be a function';
  }
  this.cache.push(fn);
  return this;
}

Middleware.prototype.next = function(fn){
  if(this.middlewares && this.middlewares.length > 0 ){
    var ware = this.middlewares.shift();
    ware.call(this, this.next.bind(this));
  }
}
Middleware.prototype.handleRequest = function(){//执行请求
  this.middlewares = this.cache.map(function(fn){//复制
    return fn;
  });
  this.next();
}

我们用Middleware简单使用一下:

var middleware = new Middleware();
middleware.use(function(next){console.log(1);next();})
middleware.use(function(next){console.log(2);next();})
middleware.use(function(next){console.log(3);})
middleware.use(function(next){console.log(4);next();})
middleware.handleRequest();
//输出结果: 
//1
//2
//3
//

4没有出来是因为上一层中间件没有调用next方法,我们升级一下Middleware高级使用

var middleware = new Middleware();
middleware.use(function(next){
  console.log(1);next();console.log('1结束');
});
middleware.use(function(next){
   console.log(2);next();console.log('2结束');
});
middleware.use(function(next){
   console.log(3);console.log('3结束');
});
middleware.use(function(next){
   console.log(4);next();console.log('4结束');
});
middleware.handleRequest();
//输出结果: 
//1
//2
//3
//3结束
//2结束
//1结束

上面代码的流程图:

可以看出:每一个中间件执行权利传递给下一个中间件并等待其结束以后又回到当前并做别的事情,方法非常巧妙,有这特性读者可以玩转中间件。

4. 实际应用

/**
* @param data 验证的数据
* @param next 
*/
function validate(data, next){
  console.log('validate', data);//验证
  next();//通过验证
}

/**
* @param data 发送的数据
* @param next 
*/
function send(data, next){
   setTimeout(function(){//模拟异步
     console.log('send', data);//已发送数据
     next();
    }, 100);
}
function goTo(url, next){
   console.log('goTo', url);//跳转
}

validate 和 send 函数都需要数据参数,目前 Middleware 只传 next,需要传递 data 数据才能顺利执行下去,然而每个中间件需要的数据不一定都一致(就像 goTo 与validate、send)。

我们需要引入一个 options 对象来包裹这一串逻辑需要的数据,每个中间件在 options 内提取自己所需的数据,这样就能满足所有中间件,Middleware 函数做相应调整:

function Middleware(){
  this.cache = [];
  this.options = null;//缓存options
}

Middleware.prototype.use = function(fn){
  if(typeof fn !== 'function'){
    throw 'middleware must be a function';
  }
  this.cache.push(fn);
  return this;
}

Middleware.prototype.next = function(fn){

  if(this.middlewares && this.middlewares.length > 0 ){
    var ware = this.middlewares.shift();
    ware.call(this, this.options, this.next.bind(this));//传入options与next
  }
}
/**
* @param options 数据的入口
* @param next 
*/
Middleware.prototype.handleRequest = function(options){
  this.middlewares = this.cache.map(function(fn){//复制
    return fn;
  });
  this.options = options;//缓存数据
  this.next();
}

业务逻辑做相应修改:

function validate(options, next){
  console.log('validate', options.data);
  next();//通过验证
}
function send(options, next){
   setTimeout(function(){//模拟异步
     console.log('send', options.data);
     options.url = 'www.baidu.com';//设置跳转的url
     next();
    }, 100);
}
function goTo(options){
##    console.log('goTo', options.url);
}

var submitForm = new Middleware();
submitForm.use(validate).use(send).use(goBack);
submitForm.handleRequest({data:{name:'xiaoxiong', age: 20}});
//结果:
// validate Object {name: "xiaoxiong", age: 20}
//
// send Object {name: "xiaoxiong", age: 20}
// goTo www.baidu.com


submitForm.handleRequest({data:{name:'xiaohong', age: 21}});//触发第二次,改变数据内容

//结果:
// validate Object {name: "xiaohong", age: 21}
//
// send Object {name: "xiaohong", age: 21}
// goTo www.baidu.com

以上代码大功告成。

5. 总结

通过以上代码,实现了业务隔离,满足每个业务所需的数据,又能很好控制业务下发执行的权利,所以“中间件”模式算是一种不错的设计。从代码阅读和代码编写的角度来说难度并不大,只要维护人员拥有该方面的知识,问题就不大了。

我的知乎原文:https://zhuanlan.zhihu.com/p/26063036

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

胡杰雄的专栏

1 篇文章1 人订阅

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏陈树义

策略模式与SPI机制,到底有什么不同?

这里说的策略模式是一种设计模式,经常用于有多种分支情况的程序设计中。例如我们去掉水果皮,一般来说对于不同的水果,会有不同的拨皮方式。此时用程序语言来表示是这样的...

743
来自专栏华仔的技术笔记

iOS应用架构谈 本地持久化方案及动态部署

嗯,你们要的大招。跟着这篇文章一起也发布了CTPersistance和CTJSBridge这两个库,希望大家在实际使用的时候如果遇到问题,就给我提issue或者...

3387
来自专栏IMWeb前端团队

谈一谈CDN的JS,CSS文件加载出错主域名重试的问题

背景知识 【卡爷文章】CSS文件动态加载(续)—— 残酷的真相 浏览器 CSS/JS 加载能力测试表 css、js的相互阻塞 了解这些基础知识之后,我们再来谈谈...

2075
来自专栏技术墨客

Jolokia架构介绍 原

    虽然jolokia是为了满足JSR-160的要求,但是他和JSR-160连接器有巨大的差异。其中最引人注目的区别是jolokia传递数据是无类型的数据(...

903
来自专栏java一日一条

给 Java 开发者的 10 个大数据工具和框架

当今IT开发人员面对的最大挑战就是复杂性,硬件越来越复杂,OS越来越复杂,编程语言和API越来越复杂,我们构建的应用也越来越复杂。根据外媒的一项调查报告,中软卓...

814
来自专栏Kirito的技术分享

深入理解 RPC 之集群篇

上一篇文章分析了服务的注册与发现,这一篇文章着重分析下 RPC 框架都会用到的集群的相关知识。 集群(Cluster)本身并不具备太多知识点,在分布式系统中,...

3269
来自专栏程序猿DD

都在说微服务,那么微服务的反模式和陷阱是什么(三)

前文导读: 《都在说微服务,那么微服务的反模式和陷阱是什么(一)》 《都在说微服务,那么微服务的反模式和陷阱是什么(二)》 九、通信协议使用的陷阱 在微服务架构...

1765
来自专栏王磊的博客

React Native顶|底部导航使用小技巧

导航一直是App开发中比较重要的一个组件,ReactNative提供了两种导航组件供我们使用,分别是:NavigatorIOS和Navigator,但是前者只能...

2836
来自专栏河湾欢儿的专栏

(第一版)知识点

782
来自专栏腾讯Bugly的专栏

【Dev Club分享】基于RxJava的一种MVP实现

Dev Club 是一个交流移动开发技术,结交朋友,扩展人脉的社群,成员都是经过审核的移动开发工程师。每周都会举行嘉宾分享,话题讨论等活动。 本期,我们邀请了腾...

3547

扫码关注云+社区