@FunC
2017-12-10T13:14:39.000000Z
字数 5717
阅读 2521
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 methodhello: () => (subject.hello() + ' world!');// delegated methodgoodbye: () => (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; // true5 in evenNumbers; // falseevenNumbers[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 和 unshiftthis.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) {// inboundconst 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;// outboundlastCall.set(this.ip, now);this.set('X-RateLimit-Reset', now.getTime() + 1000);};
这样看来,generator 跟中间件真的是天作之合。
命令(command):是指一个包含了之后需要执行的动作的所有信息的对象。
客户端(client):负责创建命令并传递给触发器(Invoker)
触发器(invoker)复制对目标执行命令
目标(target)命令的执行对象
组合起来就是命令模式:

见注释
//The Commandfunction 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 Invokerclass 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 就使用了类似的思想。