@FunC
2017-12-10T21:14:39.000000Z
字数 5717
阅读 2121
Node.js
设计模式指的对同一类问题可复用的解决方案。
设计模式中最广为人知的,是面向对象的GoF(Gang of Four)。由于 JavaScript 拥有基于原型链的面向对象、动态类型、函数作为一等公民、可函数式编程等特性,其设计模式和传统的设计模式有所不同,下面进行介绍。
创建对象时,调用工厂函数而不是使用new
操作符或Object.create()
。
compose
灵活创建对象constructor
私有,防止被修改或扩展实质是通过判断process.env.NODE_ENV
的值来切换初始化过程。而环境的设定则在运行时设定,如export NODE_ENV=development; node profilerTest
这里有一点函数式编程的味道在。
由于 JavaScript 是基于原型链的,所以因此可通过 compose
方法直接将对象的特性”拼装“起来:
const runner = stampit.compose(character, mover);
const samurai = stampit.compose(character, mover, slasher);
const gunslinger = stampit.compose(character, mover, shooter);
const westernSamurai = stampit.compose(gunslinger, samurai);
而其中的 compose
方法可通过Object.assign()
实现
npm package:stampit
将一个 executor
函数作为 constructor 的参数传入,允许其使用内部的一部分函数或属性。
安全地提高灵活性。(暴露的内部方法只能在 executor
中使用)
// 我们熟知的 Promise constructor 就暴露了内部的 resolve, reject方法
new Promise((resolve, reject) => {
// ...
});
Proxy 模式指的是只能通过代理来调用真正对象(Subject)的方法。而在代理中对 subject 的行为作了一定的修改。
通常借助继承实现,但由于 JavaScript 是动态类型,可直接用对象字面量+工厂函数:
function createProxy(subject) {
return {
// proxied method
hello: () => (subject.hello() + ' world!');
// delegated method
goodbye: () => (subject.goodbye.apply(subject, arguments))
};
}
在一些场景下只能使用该方法,如懒加载。
如需委托大部分方法时,可以借助一些库的帮助。如 delegates
直接在原对象的方法上修改(既是优点也是缺点)
function createProxy(subject) {
const helloOrig = subject.hello;
subject.hello = () => (helloOrig.call(this) + ' world!');
return subject;
}
ES2015 实现的一个 Proxy constructor,形如:
const proxy = new Proxy(target, handler);
其中 target 即先前的 subject,handler 则包含一系列 “陷阱方法”,能改变一些 JS 默认方法/操作符的表现,例如:
const evenNumbers = new Proxy([], {
get: (target, index) => index * 2,
has: (target, number) => number % 2 === 0
});
2 in evenNumbers; // true
5 in evenNumbers; // false
evenNumbers[7]; // 14
这是一个很棒的例子。我们创建了一个虚拟数组(因为没有存储数据),但被调用时又能返回正确结果。其中 Proxy 中的 get 方法修改了成员访问符的行为,has 方法修改了 in 操作符的行为。
对被装饰的实例添加新的方法。(而不是对整个类添加)
便于实现最小化内核以及增加扩展性(便于社区添加新方法)。
与代理模式类似,有对象组合和对象增强两种(参见上文)
通过适配器模式,实现使用不同的接口访问对象的方法。
策略模式允许一个对象(Context)支持多种逻辑(策略,strategy)。其中 context 是公共逻辑,strate 则提供灵活的差异化实现:
更形象的比喻是:context 是一把枪,而不同的 strategy 则是不同的子弹(霰弹、火焰弹、毒弹)。
if…else
语句,同时能实现虚拟的无限数量的策略(只需装填对应的“子弹”)Passport.js 是一个授权登陆框架,支持不同的授权协议。例如支持 Facebook 和 Twitter 的授权策略。同时,因为使用了策略模式,社区可以自己实现需要的服务授权,例如能找到微博和微信的 strategy。
状态模式其实就是包含了一系列的策略模式,在内部可以通过改变状态来使用不同的策略:
一般需要不同的状态(state)有着同名方法,并且需要状态切换方法和状态初始化方法。
其中状态的初始化既可以由 context 实现,也可以让 state 对象自己实现。
模板模式定义了一个抽象的伪类,实现了一个算法的骨架,并空出了部分步骤。空出部分由子类实现:
实际上,Stream 中就已经用到了这种模式。例如我们要自定义一个 writable stream,就要继承writable stream 的同时,自己实现它的._write()
方法。
这样看来,和策略模式的主要差异在于空出部分的复用性。像自定义的 Stream,定制程度高,复用度低,适合模板模式。而登陆授权复用度高,为了便于拓展策略,使用了策略模式。
实质类似于管道处理(pipeline)。核心在于实现一个中间件管理器(Middleware Manager),来组织并执行中间件:
中间件管理器的本质,其实是一个遍历器。我们来实现一个中间件管理器做说明:
module.exports = class ZmqMiddlewareManager {
constructor(socket) {
this.socket = socket;
// 处理请求的中间件
this.inboundMiddleware = [];
// 处理响应的中间件
this.outboundMiddleware = [];
// 运行中间件管理器
socket.on('message', message => {
// 先对请求进行处理
this.executeMiddleware(this.inboundMiddleware, {
data: message
});
});
}
send(data) {
const message = {
data: data
};
// send 方法为返回响应的方法,所以执行 outboundMiddleware
// 所以分别使用了 push 和 unshift
this.executeMiddleware(this.outboundMiddleware, message,
() => {
this.socket.send(message.data);
}
);
}
// 注意,对请求的处理和对响应的处理通常是反向的
// 例如 请求->解压->解密->处理->加密->压缩->响应
use(middleware) {
if (middleware.inbound) {
this.inboundMiddleware.push(middleware.inbound);
}
if (middleware.outbound) {
this.outboundMiddleware.unshift(middleware.outbound);
}
}
executeMiddleware(middleware, arg, finish) {
// 本质为遍历器
function iterator(index) {
if (index === middleware.length) {
return finish && finish();
}
middleware[index].call(this, arg, err => {
if (err) {
return console.log('There was an error: ' + err.message);
}
iterator.call(this, ++index);
});
}
// 启动遍历器
iterator.call(this, 0);
}
};
中间值得注意的是区分了 inboundMiddleware
和outboundMiddleware
。在这种实现中,中间件需要分别提供inbound
方法和outbound
方法。注意跟下文 Koa 的方式作对比。
不同于 Express,Koa 直接使用了ES2015的 generator 作为中间件(遍历器本质),Koa 2 更是使用了 async/await 来实现中间件。
Koa 使用的是“洋葱模型”,如图所示:
即同一个中间件(Koa中为generator)会被调用两次,并且调用顺序相反。
要做到这点,其实就是使用了 generator 可以通过 yield 暂停并切出 generator 的特点。来看示例:
const lastCall = new Map();
module.exports = function *(next) {
// inbound
const now = new Date();
if (lastCall.has(this.ip) && now.getTime() - lastCall.get(this.ip).getTime() < 1000) {
return this.status = 429; // Too Many Requests
}
// yield 之后交给下一个中间件执行
yield next;
// outbound
lastCall.set(this.ip, now);
this.set('X-RateLimit-Reset', now.getTime() + 1000);
};
这样看来,generator 跟中间件真的是天作之合。
命令(command):是指一个包含了之后需要执行的动作的所有信息的对象。
客户端(client):负责创建命令并传递给触发器(Invoker)
触发器(invoker)复制对目标执行命令
目标(target)命令的执行对象
组合起来就是命令模式:
见注释
//The Command
function createSendStatusCmd(service, status) {
let postId = null;
// command 本体,一个待执行的闭包
const command = () => {
postId = service.sendUpdate(status);
};
// command 的 undo 方法,提供撤回
command.undo = () => {
if(postId) {
service.destroyUpdate(postId);
postId = null;
}
};
// 序列化方法,用于远程调用
command.serialize = () => {
return {type: 'status', action: 'post', status: status};
};
return command;
}
//The Invoker
class Invoker {
constructor() {
// 记录命令调用历史
this.history = [];
}
run (cmd) {
// 记录本次命令
this.history.push(cmd);
cmd();
console.log('Command executed', cmd.serialize());
}
delay (cmd, delay) {
setTimeout( () => {
this.run(cmd);
}, delay)
}
undo () {
// 调用 undo() 前记得从历史中弹出
const cmd = this.history.pop();
cmd.undo();
console.log('Command undone', cmd.serialize());
}
// 借助序列化实现远程调用
runRemotely (cmd) {
request.post('http://localhost:3000/cmd',
{json: cmd.serialize()},
err => {
console.log('Command executed remotely', cmd.serialize());
}
);
}
}
现在流行的 Redux 就使用了类似的思想。