[关闭]
@FunC 2017-12-10T21:14:39.000000Z 字数 5717 阅读 2121

Node.js Design Patterns | CH06

Node.js


设计模式

设计模式指的对同一类问题可复用的解决方案。

设计模式中最广为人知的,是面向对象的GoF(Gang of Four)。由于 JavaScript 拥有基于原型链的面向对象、动态类型、函数作为一等公民、可函数式编程等特性,其设计模式和传统的设计模式有所不同,下面进行介绍。

工厂模式(Factory)

描述

创建对象时,调用工厂函数而不是使用new操作符或Object.create()

优点

根据环境改变初始化过程

实质是通过判断process.env.NODE_ENV的值来切换初始化过程。而环境的设定则在运行时设定,如export NODE_ENV=development; node profilerTest

使用 compose 灵活创建对象

这里有一点函数式编程的味道在。
由于 JavaScript 是基于原型链的,所以因此可通过 compose 方法直接将对象的特性”拼装“起来:

  1. const runner = stampit.compose(character, mover);
  2. const samurai = stampit.compose(character, mover, slasher);
  3. const gunslinger = stampit.compose(character, mover, shooter);
  4. const westernSamurai = stampit.compose(gunslinger, samurai);

而其中的 compose 方法可通过Object.assign()实现
npm package:stampit

Revealing constructor

描述

将一个 executor 函数作为 constructor 的参数传入,允许其使用内部的一部分函数或属性。

优点

安全地提高灵活性。(暴露的内部方法只能在 executor 中使用)

例子

  1. // 我们熟知的 Promise constructor 就暴露了内部的 resolve, reject方法
  2. new Promise((resolve, reject) => {
  3. // ...
  4. });

代理模式(Proxy)

描述

Proxy 模式指的是只能通过代理来调用真正对象(Subject)的方法。而在代理中对 subject 的行为作了一定的修改。

使用场景

实现方法

对象组合

通常借助继承实现,但由于 JavaScript 是动态类型,可直接用对象字面量+工厂函数:

  1. function createProxy(subject) {
  2. return {
  3. // proxied method
  4. hello: () => (subject.hello() + ' world!');
  5. // delegated method
  6. goodbye: () => (subject.goodbye.apply(subject, arguments))
  7. };
  8. }

在一些场景下只能使用该方法,如懒加载。
如需委托大部分方法时,可以借助一些库的帮助。如 delegates

对象增强(monkey patching)

直接在原对象的方法上修改(既是优点也是缺点)

  1. function createProxy(subject) {
  2. const helloOrig = subject.hello;
  3. subject.hello = () => (helloOrig.call(this) + ' world!');
  4. return subject;
  5. }

ES2015 Proxy

ES2015 实现的一个 Proxy constructor,形如:

  1. const proxy = new Proxy(target, handler);

其中 target 即先前的 subject,handler 则包含一系列 “陷阱方法”,能改变一些 JS 默认方法/操作符的表现,例如:

  1. const evenNumbers = new Proxy([], {
  2. get: (target, index) => index * 2,
  3. has: (target, number) => number % 2 === 0
  4. });
  5. 2 in evenNumbers; // true
  6. 5 in evenNumbers; // false
  7. evenNumbers[7]; // 14

这是一个很棒的例子。我们创建了一个虚拟数组(因为没有存储数据),但被调用时又能返回正确结果。其中 Proxy 中的 get 方法修改了成员访问符的行为,has 方法修改了 in 操作符的行为。

装饰者模式(Decorator)

描述

对被装饰的实例添加新的方法。(而不是对整个类添加)

优点

便于实现最小化内核以及增加扩展性(便于社区添加新方法)。

实现方式

与代理模式类似,有对象组合和对象增强两种(参见上文)

适配器模式(Adapter)

描述

通过适配器模式,实现使用不同的接口访问对象的方法。

优点

例子

策略模式(Strategy)

描述

策略模式允许一个对象(Context)支持多种逻辑(策略,strategy)。其中 context 是公共逻辑,strate 则提供灵活的差异化实现:

更形象的比喻是:context 是一把枪,而不同的 strategy 则是不同的子弹(霰弹、火焰弹、毒弹)。

优点

缺点

应用

Passport.js 是一个授权登陆框架,支持不同的授权协议。例如支持 Facebook 和 Twitter 的授权策略。同时,因为使用了策略模式,社区可以自己实现需要的服务授权,例如能找到微博和微信的 strategy。

状态模式(State)

描述

状态模式其实就是包含了一系列的策略模式,在内部可以通过改变状态来使用不同的策略:

优点

实现

一般需要不同的状态(state)有着同名方法,并且需要状态切换方法状态初始化方法
其中状态的初始化既可以由 context 实现,也可以让 state 对象自己实现。

模板模式(Template)

描述

模板模式定义了一个抽象的伪类,实现了一个算法的骨架,并空出了部分步骤。空出部分由子类实现:

优点

应用

实际上,Stream 中就已经用到了这种模式。例如我们要自定义一个 writable stream,就要继承writable stream 的同时,自己实现它的._write()方法。
这样看来,和策略模式的主要差异在于空出部分的复用性。像自定义的 Stream,定制程度高,复用度低,适合模板模式。而登陆授权复用度高,为了便于拓展策略,使用了策略模式。

中间件(Middleware)

描述

实质类似于管道处理(pipeline)。核心在于实现一个中间件管理器(Middleware Manager),来组织并执行中间件:

优点

实现

中间件管理器的本质,其实是一个遍历器。我们来实现一个中间件管理器做说明:

  1. module.exports = class ZmqMiddlewareManager {
  2. constructor(socket) {
  3. this.socket = socket;
  4. // 处理请求的中间件
  5. this.inboundMiddleware = [];
  6. // 处理响应的中间件
  7. this.outboundMiddleware = [];
  8. // 运行中间件管理器
  9. socket.on('message', message => {
  10. // 先对请求进行处理
  11. this.executeMiddleware(this.inboundMiddleware, {
  12. data: message
  13. });
  14. });
  15. }
  16. send(data) {
  17. const message = {
  18. data: data
  19. };
  20. // send 方法为返回响应的方法,所以执行 outboundMiddleware
  21. // 所以分别使用了 push 和 unshift
  22. this.executeMiddleware(this.outboundMiddleware, message,
  23. () => {
  24. this.socket.send(message.data);
  25. }
  26. );
  27. }
  28. // 注意,对请求的处理和对响应的处理通常是反向的
  29. // 例如 请求->解压->解密->处理->加密->压缩->响应
  30. use(middleware) {
  31. if (middleware.inbound) {
  32. this.inboundMiddleware.push(middleware.inbound);
  33. }
  34. if (middleware.outbound) {
  35. this.outboundMiddleware.unshift(middleware.outbound);
  36. }
  37. }
  38. executeMiddleware(middleware, arg, finish) {
  39. // 本质为遍历器
  40. function iterator(index) {
  41. if (index === middleware.length) {
  42. return finish && finish();
  43. }
  44. middleware[index].call(this, arg, err => {
  45. if (err) {
  46. return console.log('There was an error: ' + err.message);
  47. }
  48. iterator.call(this, ++index);
  49. });
  50. }
  51. // 启动遍历器
  52. iterator.call(this, 0);
  53. }
  54. };

中间值得注意的是区分了 inboundMiddlewareoutboundMiddleware。在这种实现中,中间件需要分别提供inbound方法和outbound方法。注意跟下文 Koa 的方式作对比。

在 Koa 中使用 generator 作中间件

不同于 Express,Koa 直接使用了ES2015的 generator 作为中间件(遍历器本质),Koa 2 更是使用了 async/await 来实现中间件。
Koa 使用的是“洋葱模型”,如图所示:

即同一个中间件(Koa中为generator)会被调用两次,并且调用顺序相反。
要做到这点,其实就是使用了 generator 可以通过 yield 暂停并切出 generator 的特点。来看示例:

  1. const lastCall = new Map();
  2. module.exports = function *(next) {
  3. // inbound
  4. const now = new Date();
  5. if (lastCall.has(this.ip) && now.getTime() - lastCall.get(this.ip).getTime() < 1000) {
  6. return this.status = 429; // Too Many Requests
  7. }
  8. // yield 之后交给下一个中间件执行
  9. yield next;
  10. // outbound
  11. lastCall.set(this.ip, now);
  12. this.set('X-RateLimit-Reset', now.getTime() + 1000);
  13. };

这样看来,generator 跟中间件真的是天作之合。

命令模式(Command)

描述

命令(command):是指一个包含了之后需要执行的动作的所有信息的对象。
客户端(client):负责创建命令并传递给触发器(Invoker)
触发器(invoker)复制对目标执行命令
目标(target)命令的执行对象
组合起来就是命令模式:

优点

缺点

实现

见注释

  1. //The Command
  2. function createSendStatusCmd(service, status) {
  3. let postId = null;
  4. // command 本体,一个待执行的闭包
  5. const command = () => {
  6. postId = service.sendUpdate(status);
  7. };
  8. // command 的 undo 方法,提供撤回
  9. command.undo = () => {
  10. if(postId) {
  11. service.destroyUpdate(postId);
  12. postId = null;
  13. }
  14. };
  15. // 序列化方法,用于远程调用
  16. command.serialize = () => {
  17. return {type: 'status', action: 'post', status: status};
  18. };
  19. return command;
  20. }
  21. //The Invoker
  22. class Invoker {
  23. constructor() {
  24. // 记录命令调用历史
  25. this.history = [];
  26. }
  27. run (cmd) {
  28. // 记录本次命令
  29. this.history.push(cmd);
  30. cmd();
  31. console.log('Command executed', cmd.serialize());
  32. }
  33. delay (cmd, delay) {
  34. setTimeout( () => {
  35. this.run(cmd);
  36. }, delay)
  37. }
  38. undo () {
  39. // 调用 undo() 前记得从历史中弹出
  40. const cmd = this.history.pop();
  41. cmd.undo();
  42. console.log('Command undone', cmd.serialize());
  43. }
  44. // 借助序列化实现远程调用
  45. runRemotely (cmd) {
  46. request.post('http://localhost:3000/cmd',
  47. {json: cmd.serialize()},
  48. err => {
  49. console.log('Command executed remotely', cmd.serialize());
  50. }
  51. );
  52. }
  53. }

现在流行的 Redux 就使用了类似的思想。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注