一、前言
在日常开发中,我们经常遇到逻辑复杂的业务,导致代码写得又长又乱。有些逻辑像一个流程,在不同的节点需要做不同的操作。
比如,我们经常会遇到上传文件的业务。该业务要求先验证文件正确性,然后上传,最后跳转到成功的页面。
if checkFile {
uploadFile { result
if result {
showSuccessView { result
if result {
//handle success
} else {
//handle error
}
}
} else {
//TODO handle error
}
}
} else {
//TODO handle error
}
我们可以看到伪代码中呈现着回调地狱,上传前、上传、上传后的操作逻辑也零散地分布,很容易造成阅读和维护困难,而中间件的出现,让我们处理这类业务变得简单很多。
在讲中间件之前我们先来回顾下AOP。
二、AOP
AOP意为面向切面编程。 以页面统计为例,先来看下传统的流程。
我们可以把方框里的流程合为一个,另外系统还会有其他页面统计流程,我们先把这些流程放到一起:
不难发现每个页面都有一个相同的页面统计流程。这样的处理有如下几个问题:
- 重复代码:每个界面都需要加入页面统计的代码
- 影响主流程的清晰度:页面统计跟主流程无关,但又需要加入到各个界面中
- 扩展性差:新增一个界面,就得为其加入页面统计的代码
有没有想过把这个页面统计的代码是提取出来,不放到主流程里去呢?这就是AOP的思想了,传统的流程讲究从上而下的处理流程 ,而AOP讲究“面”,从横向切面将相同的流程提取出去,所以也叫“横切面”,如下图所示。
AOP提倡从横向切面思路向管道某个位置插入一段代码逻辑,这样就实现在任何业务逻辑前后都有相同代码逻辑段,开发者只需专注写业务逻辑,既不影响主流程,而且隔离了业务逻辑,达到高内聚低耦合。
附上iOS使用AOP实现页面统计的代码,帮助大家理解AOP。
@implementation UIViewController (Analytics)
+ (void)load{
[self swizzleInstanceMethod:@selector(viewWillAppear:) with:@selector(swizzled_viewWillAppear:)];
[self swizzleInstanceMethod:@selector(viewWillDisappear:) with:@selector(swizzled_viewWillDisappear:)];
}
- (void)swizzled_viewWillAppear:(BOOL)animated{
[self swizzled_viewWillAppear:animated];
[Analytics beginTimingEvent: self.className];
}
- (void)swizzled_viewWillDisappear:(BOOL)animated{
[self swizzled_viewWillDisappear:animated];
[Analytics endTimingEvent: self.className];
}
思考
- AOP和OOP是什么关系?有什么区别?
写多了OOP的代码,会发现AOP跟OOP的思路不同。OOP是将做同一件事情的业务逻辑封装成一个对象。但是,在做一件事情过程(主流程)中又想做别的事情(比如页面统计)对OOP来说难以解决。而AOP的出现让OOP代码能专注于主流程,更好地遵循单一职责原则,提高内聚性。所以,我认为AOP对OOP做了一个补充。
怎么判断代码是否达到了高内聚?
这个问题吊一下大家胃口,这里先不解答,大家可以在留言处评论,后续会贴出个人的理解。AOP思想如何解决上传文件的业务的问题?
由于JavaScript的动态性较好,下面以JavaScript代码为例,看下如何利用AOP优化上传文件业务。
利用AOP优化上传文件业务
AOP从横向切面思路向管道某个位置插入一段代码逻辑。而这个位置通常就是调用前(before)和调用后(after)。
Function.prototype.before = function(fn){
var self = this;
return function(){
var res = fn.call(this);
if(res) {
self.apply(this, arguments);
}
}
}
Function.prototype.after = function(fn){
var self = this;
return function(){
self.apply(this, arguments);
fn.call(this);
}
}
有了这两个扩展,可以按下面的方式实现上传业务。
function checkFile(){
console.log('checking file');
if fileIsVaild() {
return true
} else {
console.log('file is invalid');
return false
}
}
function uploadFile() {
console.log('uploading file');
if fileUploadSuccess() {
return true
} else {
console.log('file upload failed');
return false
}
}
function showSuccessView() {
console.log('show success view');
}
uploadFile.before(checkFile).after(showSuccessView)();
思考
使用AOP后解决了什么问题?还有什么问题?
从上面的例子,可以看出AOP已经实现了业务隔离。但却带来了一串长长的链式调用,如果处理不当很容易掉链子。另外,这种结构实现异步操作较为麻烦。有什么办法,既能隔离业务,又能清爽地使用?
这时候,我们的主角就该上场了。
三、中间件
为了理解中间件,我们来看下Koa的中间件使用。
const logger = (ctx, next) => {
console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`);
next();
}
app.use(logger);
像上面代码中的logger函数就叫做"中间件"(middleware),因为它处在 HTTP Request 和 HTTP Response 中间,用来实现某种中间功能。app.use()用来加载中间件。
中间件栈
多个中间件会形成一个栈结构(middle stack),以"先进后出"(first-in-last-out)的顺序执行,被称为洋葱结构。
举个例子
const one = (ctx, next) => {
console.log('>> one');
next();
console.log('<< one');
}
const two = (ctx, next) => {
console.log('>> two');
next();
console.log('<< two');
}
const three = (ctx, next) => {
console.log('>> three');
next();
console.log('<< three');
}
app.use(one);
app.use(two);
app.use(three);
/*result
>> one
>> two
>> three
<< three
<< two
<< one
*/
中间件一个奇妙的点在于next函数。如果中间件内部没有调用next函数,那么执行权就不会传递下去。
中间件的实现
export default class MiddlewareCenter {
constructor() {
this._middlewares = []
this._context = null
}
use(middleware) {
if (typeof middleware != 'function') {
console.warn('middleware must be a function.')
return null
}
this._middlewares.push(middleware)
return this
}
handleRequest(context) {
const fn = compose(this._middlewares)
this._context = context
fn(this._context)
}
}
function compose(middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}
详细的代码可以看我的开源项目middleware-center。
利用中间件优化上传业务
const middlewareCenter = new UploadFileCenter()
middlewareCenter.handle('uploader')
/* result
beforeUpload
startUplaod
finishUpload
after finishUpload
after startUplaod
after beforeUpload
*/
class UploadFileCenter extends MiddlewareCenter {
constructor() {
super()
this._middlewareMap = { 'uploader': [this.beforeUpload, this.startUplaod, this.finishUpload] }
this.content = ""
}
handle(name) {
let middlewares = this._middlewareMap[name]
for (let middleware of middlewares) {
this.use(middleware)
}
this.handleRequest(this)
}
// Middlewares
async beforeUpload (ctx, next) {
console.log('beforeUpload')
ctx.content = await ctx.genContent()
await next()
console.log('after beforeUpload')
}
async startUplaod (ctx, next) {
console.log('startUplaod')
let result = await ctx.upload(ctx.content)
await next()
console.log('after startUplaod')
}
finishUpload (ctx, next) {
console.log('finishUpload')
//do something like notify listeners
console.log('after finishUpload')
}
// Helpers
genContent() {
return new Promise((resolve, reject) => {
setTimeout(function () {
resolve('upload content')
}, 3)
})
}
upload(content) {
return new Promise((resolve, reject) => {
setTimeout(function () {
resolve(true)
}, 5)
})
}
}
大家可以看到,利用中间件不但可以实现业务隔离,调用也很清晰,只要根据业务调整use的顺序即可。各个中间件还可以随意组合,各组件间也没有依赖关系,自身内聚性高。细心的朋友可以发现,middleware-center 还支持异步方法。得益于中间件的洋葱结构,使得使用者可以处理业务的任何“位置”(如:beforeUpload、startUplaod、finishUpload、after finishUpload、after startUplaod、after beforeUpload)
遍地开花
好思想应该遍地开花,业余时间我用swift简单实现了中间件模型MiddlewareCenter,但还没优化,和处理引用问题,感兴趣的朋友可以一起维护。
class ViewController: UIViewController {
let center = MiddlewareCenter()
override func viewDidLoad() {
super.viewDidLoad()
center.use(BeforeUpload())
center.use(StartUpload())
center.use(EndUpload())
center.handle(ctx: nil)
}
}
public class StartUpload: Middleware {
public override func execute() {
print("before StartUpload.")
next?.execute()
print("after StartUpload.")
}
}
public class BeforeUpload: Middleware {
public override func execute() {
print("before upload.")
next?.execute()
print("after upload.")
}
}
public class EndUpload: Middleware {
public override func execute() {
print("before EndUpload.")
next?.execute()
print("after EndUpload.")
}
}
四、总结
中间件实现了业务隔离,满足每个业务所需的数据,又能很好控制业务下发执行的权利,所以“中间件”模式算是一种不错的设计。
理解中间件,主要理解三个概念,包括:context、next、洋葱结构。context为业务所需的上下文,比如koa是处理网络请求的,所以它的context包含request、response;next是业务流的下发控制,使用好next,可以灵活地处理各种业务,包括错误处理、中间件重用等;洋葱结构可以让使用者轻松地处理各个流程的“位置”。