关于怎样把对象组合起来,并规定出这些对象之间的交流方式,让组合而成的这种结构,变得易于扩展、易于形成模块、易于复用且易于适配。
- Strategy(策略)模式:修改组件的某一部分,让该组件能够适应特定的需求
- State(状态)模式:根据组件的行为修改它的状态
- Template(模板)模式:复用某组件的结构,以定义新的组件
- Iterator(迭代器)模式:定义一套通用的接口,使得开发者可以通过这套接口迭代某个集合
- Middleware(中间件)模式:将一系列处理步骤定义到一个模块里面
- Command(命令)模式:把执行某例程所需的信息表示出来,让我们能够轻松地传输、存储并处理这种信息
Strategy(策略)模式
Strategy 模式把某对象(context)的逻辑里面可能发生变化的(variable)部分单独抽出来,让这一部分可以用各种策略(strategy)来实现,从而令该对象表现出不同的形式。 context 会实现这一系列算法所共用的逻辑,并把这些算法有可能出现分歧的地方预留出来,根据某个可变的因素(例如某个输入值、某个系统配置或用户选项)来运用相应的 strategy(策略),以调整自己的行为。

这个模式中的这些 strategy 指的通常是一系列解决方案里面的某几种方案,这些方案都实现同一套接口,也就是 context 所要求的那套接口。
处理各种格式的配置信息
import { promises as fs } from 'fs'import objectPath from 'object-path'export class Config {constructor (formatStrategy) {this.data = {}this.formatStrategy = formatStrategy}get (configPath) {return objectPath.get(this.data, configPath)}set (configPath, value) {return objectPath.set(this.data, configPath, value)}async load (filePath) {console.log(`Deserializing from ${filePath}`)this.data = this.formatStrategy.deserialize(await fs.readFile(filePath, 'utf-8'))}async save (filePath) {console.log(`Serializing to ${filePath}`)await fs.writeFile(filePath,this.formatStrategy.serialize(this.data))}}
import ini from 'ini'export const iniStrategy = {deserialize: data => ini.parse(data),serialize: data => ini.stringify(data)}export const jsonStrategy = {deserialize: data => JSON.parse(data),serialize: data => JSON.stringify(data, null, ' ')}
import { Config } from './config.js'import { jsonStrategy, iniStrategy } from './strategies.js'async function main () {const iniConfig = new Config(iniStrategy)await iniConfig.load('samples/conf.ini')iniConfig.set('book.nodejs', 'design patterns')await iniConfig.save('samples/conf_mod.ini')const jsonConfig = new Config(jsonStrategy)await jsonConfig.load('samples/conf.json')jsonConfig.set('book.nodejs', 'design patterns')await jsonConfig.save('samples/conf_mod.json')}main()
以上程序是让用户在构造 Config 实例时自行选择相应的策略对象。
还可以使用以下手段来实现策略对象的切换功能:
- 创建两套策略体系,以分别表示整个流程之中可以变化的那两个地方:其中一套针对反序列化操作,另一套针对序列化操作。
- 动态地选择策略:让 Config 对象根据文件的扩展名来选择合适的策略。可以构建一张映射表(map 结构),把每一种扩展名(extension)所对应的策略(strategy)保存进去,让 Config 对象从这张表中查出自己应该使用哪种策略
State(状态)模式
State 模式是 Strategy 模式的特例或特化(specialization),它会根据 Context 的状态(state)来改变自己的策略(strategy)状态迁移(state transition)可以由 context 对象发起并控制,也可以由用户所写的代码或者由 state 对象自己来做。后一种方法最灵活,而且耦合度低,因为 context 不需要知道总共有多少种状态,也不用担心什么时候应该从其中一种状态切换到另外一种。
实现一种能够处理常见错误的 socket
import { OfflineState } from './offlineState.js'import { OnlineState } from './onlineState.js'export class FailsafeSocket {constructor (options) {this.options = optionsthis.queue = []this.currentState = nullthis.socket = nullthis.states = {offline: new OfflineState(this), // socket 在服务器下线时的行为online: new OnlineState(this) // socket 在服务器上线时的行为}this.changeState('offline')}// 负责让 socket 从一种状态迁移到另一种状态changeState (state) {console.log(`Activating state: ${state}`)this.currentState = this.states[state]this.currentState.activate()}send (data) {this.currentState.send(data)}}
import jsonOverTcp from 'json-over-tcp-2' // 解析流经 socket 的所有数据并调整为 json 格式export class OfflineState {constructor (failsafeSocket) {this.failsafeSocket = failsafeSocket}send (data) {this.failsafeSocket.queue.push(data)}activate () {const retry = () => {setTimeout(() => this.activate(), 1000)}console.log('Trying to connect...')this.failsafeSocket.socket = jsonOverTcp.connect(this.failsafeSocket.options,() => {console.log('Connection established')this.failsafeSocket.socket.removeListener('error', retry)// 与服务器建立连接后状态迁移到 online 状态this.failsafeSocket.changeState('online')})this.failsafeSocket.socket.once('error', retry)}}
export class OnlineState {constructor (failsafeSocket) {this.failsafeSocket = failsafeSocketthis.hasDisconnected = false}send (data) {this.failsafeSocket.queue.push(data)this._safeWrite(data)}_safeWrite (data) {this.failsafeSocket.socket.write(data, (err) => {if (!this.hasDisconnected && !err) {this.failsafeSocket.queue.shift()}})}activate () {this.hasDisconnected = falsefor (const data of this.failsafeSocket.queue) {this._safeWrite(data)}// 发生这样的事件说明服务器下线了this.failsafeSocket.socket.once('error', () => {this.hasDisconnected = truethis.failsafeSocket.changeState('offline')})}}
import jsonOverTcp from 'json-over-tcp-2'const server = jsonOverTcp.createServer({ port: 5000 })server.on('connection', socket => {socket.on('data', data => {console.log('Client data', data)})})server.listen(5000, () => console.log('Server started'))
import { FailsafeSocket } from './failsafeSocket.js'const failsafeSocket = new FailsafeSocket({ port: 5000 })setInterval(() => {// send current memory usagefailsafeSocket.send(process.memoryUsage())}, 1000)
Template(模板)模式
Template 模式定义一个抽象的类,用来实现某个组件的骨架(skeleton,即通用的部分),并在其中留下一些还没有实现的步骤,让子类去实现。这样的步骤叫做 template method(模板方法),子类通过实现模板方法,来填补抽象类在逻辑上的缺失,让整个组件的功能完整。这个模式想帮助我们针对某组件定义一系列的类,以展示该组件的各种形式。
用 template 模式重新实现配置管理器
import { promises as fsPromises } from 'fs'import objectPath from 'object-path'export class ConfigTemplate {async load (file) {console.log(`Deserializing from ${file}`)this.data = this._deserialize(await fsPromises.readFile(file, 'utf-8'))}async save (file) {console.log(`Serializing to ${file}`)await fsPromises.writeFile(file, this._serialize(this.data))}get (path) {return objectPath.get(this.data, path)}set (path, value) {return objectPath.set(this.data, path, value)}// 通过抛出错误提醒子类必须覆写这个方法_serialize () {throw new Error('_serialize() must be implemented')}_deserialize () {throw new Error('_deserialize() must be implemented')}}
import { ConfigTemplate } from './configTemplate.js'import ini from 'ini'export class IniConfig extends ConfigTemplate {_deserialize (data) {return ini.parse(data)}_serialize (data) {return ini.stringify(data)}}
import { ConfigTemplate } from './configTemplate.js'export class JsonConfig extends ConfigTemplate {_deserialize (data) {return JSON.parse(data)}_serialize (data) {return JSON.stringify(data, null, ' ')}}
Iterator(迭代器)模式
只要某结构能按顺序产生元素,或者其中的元素能按顺序获取,那么我们就可以利用 Iterator 模式定义一套接口,让用户通过该接口访问此结构之中的元素。
iterator 协议
在 JS 语言里面实现 Iterator 模式,需要从 iterator protocol(迭代器协议)入手,这个协议约定了一套接口,用来产生一系列的值。
迭代器协议定义了一种叫做 iterator 的对象,这种对象必须实现 next() 方法,且 next() 方法每次受到调用时,都通过 iterator result 对象,把下一个元素返回给调用者,这个 iterator result 对象具备 done 与 value 这两项属性:
- 如果迭代结束,done 属性应该是 true,否则应该是 undefined 或 false ```typescript const A_CHAR_CODE = 65 const Z_CHAR_CODE = 90
function createAlphabetIterator () { let currCode = A_CHAR_CODE
return { next () {
// String.fromCodePoint() 静态方法返回使用指定的代码点序列创建的字符串const currChar = String.fromCodePoint(currCode)if (currCode > Z_CHAR_CODE) {return { done: true }}currCode++return { value: currChar, done: false }}
} }
const iterator = createAlphabetIterator() let iterationResult = iterator.next() while (!iterationResult.done) { console.log(iterationResult.value) iterationResult = iterator.next() }
<a name="p9uQh"></a>## iterable 协议iterable protocol(可迭代协议)规定了一种标准的方式,让某对象能够依照这种方式返回迭代器,以表示该对象里面的元素能够通过迭代器来迭代。这样的对象称为 iterable。<br />在 JS 中,可以让某种对象实现 `@@iterator `方法来表明这是个可迭代的(iterable)对象,这个方法可以通过内置的 `Symbol.iterator` 符号来表示。```typescriptclass MyIterable {// 其他方法[Symbol.iterator]() {// 返回迭代器}}
export class Matrix {constructor (inMatrix) {this.data = inMatrix}get (row, column) {if (row >= this.data.length || column >= this.data[row].length) {throw new RangeError('Out of bounds')}return this.data[row][column]}set (row, column, value) {if (row >= this.data.length || column >= this.data[row].length) {throw new RangeError('Out of bounds')}this.data[row][column] = value}[Symbol.iterator] () {let nextRow = 0let nextCol = 0return {next: () => {if (nextRow === this.data.length) {return { done: true }}const currVal = this.data[nextRow][nextCol]if (nextCol === this.data[nextRow].length - 1) {nextRow++nextCol = 0} else {nextCol++}return { value: currVal }}}}}
import { Matrix } from './matrix.js'const matrix2x2 = new Matrix([['11', '12'],['21', '22']])const iterator = matrix2x2[Symbol.iterator]()let iterationResult = iterator.next()while (!iterationResult.done) {console.log(iterationResult.value)iterationResult = iterator.next()}
Generator(生成器)
Generator 也叫做半协程(semicoroutine),是一种比普通函数更宽广的函数结构,可以有多个入口点(entry point)
生成器函数(generator function)所返回的生成器对象(generator object),本身即是 iterator,也是 iterable
function * myGenerator() {// 生成器的主体部分}
简单的生成器函数
function * fruitGenerator () {yield 'peach'yield 'watermelon'return 'summer'}const fruitGeneratorObj = fruitGenerator()console.log(fruitGeneratorObj.next()) // {value: 'peach', done: false}console.log(fruitGeneratorObj.next()) // {value: 'watermelon', done: false}console.log(fruitGeneratorObj.next()) // {value: 'summer', done: true}console.log('Using for...of:')for (const fruit of fruitGenerator()) {console.log(fruit)}// peach// watermelon
for … of 结构不会打印出 summer ,因为这个值不是由生成器函数的代码通过 yield 命名给出的,所以不会把他当成生成器对象所生成的元素
控制生成器对象的执行逻辑
function * twoWayGenerator () {try {const what = yield nullyield 'Hello ' + what} catch (err) {yield 'Hello error: ' + err.message}}console.log('Passing a value back to the generator:')const twoWay = twoWayGenerator()twoWay.next()// 传入的参数会被赋值给上一条 yield 语句左侧的变量console.log(twoWay.next('world')) // Hello worldconsole.log('Using throw():')const twoWayException = twoWayGenerator()twoWayException.next()// {value: "Hello error: Boom!", done: false}console.log(twoWayException.throw(new Error('Boom!')))console.log('Using return():')const twoWayReturn = twoWayGenerator()// {value: "myReturnValue", done: true}console.log(twoWayReturn.return('myReturnValue'))
- throw 方法返回的是一个表示迭代结果的对象,包含 done 与 value 属性
return 方法会迫使生成器终止,并返回 { done: true, value: returnArgument }形式的对象
怎么用生成器来替换普通的迭代器
export class Matrix {// ...* [Symbol.iterator] () {let nextRow = 0let nextCol = 0while (nextRow !== this.data.length) {yield this.data[nextRow][nextCol]if (nextCol === this.data[nextRow].length - 1) {nextRow++nextCol = 0} else {nextCol++}}}}
```typescript import { Matrix } from ‘./matrix.js’
const matrix2x2 = new Matrix([ [‘11’, ‘12’], [‘21’, ‘22’] ])
const iterator = matrix2x2Symbol.iterator let iterationResult = iterator.next() while (!iterationResult.done) { console.log(iterationResult.value) iterationResult = iterator.next() }
这次把维护迭代状态所用的两个变量,设计成 *[Symbol.iterator]() 函数本省的局部变量,而不是将其放在闭包中,因为这个函数所返回的生成器对象从暂停状态恢复执行的时候,是可以访问到生成器函数里面那些局部变量的,而且那些变量会延续它们暂停时的取值。<a name="imtfc"></a>## async iterator(异步迭代器)async iterator 必须返回 Promise,其他的和普通的 iterator 一样<br />可以把这种 iterator 的 next() 方法实现为异步函数,或通过 [Symbol.asyncIterator] 标识的方法同步地返回一个 async iterator<br />async iterator 可以用 for await...of 结构来迭代,和下面这段代码实现的功能相同:```typescriptconst asyncIterator = iterator[Symbol.asyncIterator]();let iterationResult = await asyncIterator.next();while (!iterationResult.done) {console.log(iterationResult.value);iterationResult = await asyncIterator.next();}
import superagent from 'superagent'export class CheckUrls {constructor (urls) {this.urls = urls}[Symbol.asyncIterator] () {// 可迭代对象通过 [Symbol.iterator]() 来触发它的 @@iterator 方法即可获得迭代器const urlsIterator = this.urls[Symbol.iterator]()// 返回 Promise 对象return {async next () {const iteratorResult = urlsIterator.next()if (iteratorResult.done) {return { done: true }}const url = iteratorResult.valuetry {const checkResult = await superagent.head(url).redirects(2)return {done: false,value: `${url} is up, status: ${checkResult.status}`}} catch (err) {return {done: false,value: `${url} is down, error: ${err.message}`}}}}}}
import { CheckUrls } from './checkUrls.js'async function main () {const checkUrls = new CheckUrls(['https://nodejsdesignpatterns.com','https://example.com','https://mustbedownforsurehopefully.com'])for await (const status of checkUrls) {console.log(status)}}main()
在使用 for await…of 结构来迭代时,如果这个结构因为遇到 break 语句、return 语句或异常而提前跳出,那么系统会在迭代器上面调用可选的 return() 方法,所以如果你的迭代器在那几种情况下,需要像正常执行完毕时那样做一些清理,那么可以把这些清理操作写在 return() 里
async generator(异步生成器)
async function * generatorFunction() {// 生成器函数的主体部分}
函数的主体部分会出现 await 指令,它所返回的 async generator 对象,在执行 next() 方法的时候,会返回一个 Promise 对象,这个 Promise 对象得到解析后,会形成一个表示结果的对象,这个对象跟普通的 generator 在执行 next() 方法时所返回的对象一样,都具备 done 与 value 属性。
import superagent from 'superagent'export class CheckUrls {constructor (urls) {this.urls = urls}async * [Symbol.asyncIterator] () {for (const url of this.urls) {try {const checkResult = await superagent.head(url).redirects(2)yield `${url} is up, status: ${checkResult.status}`} catch (err) {yield `${url} is down, error: ${err.message}`}}}}
async iterator 与 Node.js 平台的 stream
stream.Readable 的设计者想让它成为一种 async iterable,它实现了 @@asyncIterator 方法,所以也能够通过 for await…of 结构读取
import split from 'split2'async function main () {const stream = process.stdin.pipe(split())for await (const line of stream) {console.log(`You wrote: ${line}`)}}main()
Middleware(中间件)模式
在 Node.js 平台中,凡是能够在底层服务与应用程序间起到粘合作用的软件层,都可以叫做中间件
从模式的角度谈中间件
中间件模式其实是一种管道,只要有一组以函数形式来表示的处理单元、过滤器或处理程序,它们之间需要连接成一条异步的序列,用来给某种类型的数据做前置处理(preprocessiong)或后置处理(postprocessing),那么我们就可以用中间件模式来实现该管道。
这个模型最关键的部分是 Middleware Manager(中间件管理器),它负责安排并执行中间件函数(middleware function)。
- 用户可以通过 use() 函数注册新的中间件
- 管理中的每个单元(unit)都把上一个单元的执行结果,当作自己的输入信息
- 管道中的每个中间件,都能够决定这份数据是否不再需要继续处理。当它认为这份数据不需要继续处理的时候,可以调用某个特殊的函数(如果下一个中间件是通过回调触发的),也可以不去触发回调。另外,还可以抛出错误。当某个中间件出错时,这个模式通常会触发另一系列的中间件,那个系列是专门用来处理错误的
下面两种方法都可以把修改后的数据沿着管道传递下去:
- 让每个中间件把自己想要添加的属性或函数,补充到它所接收的这份数据上面,并把补充后的数据继续往下传
将数据设计成不可变的对象,让每个中间件都把处理结果另外表示成一个对象,然后继续往下传递
创建针对 ZeroMQ 的中间件框架
编写 Middleware Manager(中间件管理器)
export class ZmqMiddlewareManager {constructor (socket) {this.socket = socketthis.inboundMiddleware = [] // 处理入站信息this.outboundMiddleware = [] // 出站信息this.handleIncomingMessages().catch(err => console.error(err))}async handleIncomingMessages () {for await (const [message] of this.socket) {await this.executeMiddleware(this.inboundMiddleware, message).catch(err => {console.error('Error while processing the message', err)})}}async send (message) {const finalMessage = await this.executeMiddleware(this.outboundMiddleware, message)return this.socket.send(finalMessage)}use (middleware) {if (middleware.inbound) {this.inboundMiddleware.push(middleware.inbound)}if (middleware.outbound) {// 和入站的函数相反顺序this.outboundMiddleware.unshift(middleware.outbound)}}// 执行某条管道中的那些中间件函数async executeMiddleware (middlewares, initialMessage) {let message = initialMessagefor await (const middlewareFunc of middlewares) {// 把每个函数的执行结果传给管道中的下一个函数message = await middlewareFunc.call(this, message)}return message}}
实现处理消息所用的中间件函数
export const jsonMiddleware = function () {return {inbound (message) {return JSON.parse(message.toString())},outbound (message) {return Buffer.from(JSON.stringify(message))}}}
```typescript import { inflateRaw, deflateRaw } from ‘zlib’ import { promisify } from ‘util’
const inflateRawAsync = promisify(inflateRaw) const deflateRawAsync = promisify(deflateRaw)
export const zlibMiddleware = function () { return { inbound (message) { return inflateRawAsync(Buffer.from(message)) }, outbound (message) { return deflateRawAsync(message) } } }
<a name="OiI76"></a>### 使用这个针对 ZeroMQ 的中间件系统服务器端```typescriptimport zeromq from 'zeromq'import { ZmqMiddlewareManager } from './zmqMiddlewareManager.js'import { jsonMiddleware } from './jsonMiddleware.js'import { zlibMiddleware } from './zlibMiddleware.js'async function main () {const socket = new zeromq.Reply()await socket.bind('tcp://127.0.0.1:5000')const zmqm = new ZmqMiddlewareManager(socket)zmqm.use(zlibMiddleware())zmqm.use(jsonMiddleware())zmqm.use({async inbound (message) {console.log('Received', message)if (message.action === 'ping') {await this.send({ action: 'pong', echo: message.echo })}return message}})console.log('Server started')}main()
客户端
import zeromq from 'zeromq'import { ZmqMiddlewareManager } from './zmqMiddlewareManager.js'import { jsonMiddleware } from './jsonMiddleware.js'import { zlibMiddleware } from './zlibMiddleware.js'async function main () {const socket = new zeromq.Request()await socket.connect('tcp://127.0.0.1:5000')const zmqm = new ZmqMiddlewareManager(socket)zmqm.use(zlibMiddleware())zmqm.use(jsonMiddleware())zmqm.use({inbound (message) {console.log('Echoed back', message)return message}})setInterval(() => {zmqm.send({ action: 'ping', echo: Date.now() }).catch(err => console.error(err))}, 1000)console.log('Client connected')}main()
Command(命令)模式
凡是把执行某动作所需的信息封装起来以便稍后加以执行的对象,都可以叫做 command 对象
设计这种对象是因为我们不打算直接调用某个方法或函数,而是打算创建这么一个 command 对象,用来表达我们需要执行调用操作的这一想法。这个想法稍后会由另一个组件负责实现,那个组件会把它落实成真正的操作。
- Command:这个组件表示一种对象,用来封装调用某方法或某函数时所必备的信息
- Client(用户/客户):这个组件用来创建 command,并把它提供给 Invoker
- Invoker(调用者/调用器):负责在 Target 上面执行 command
- Target(目标,也叫做 receiver 接收者):表示需要执行的东西,可能是某个单独的函数,或对象中的某个方法
Task 模式
在 JS 中创建一个表示调用操作的对象,可以通过创建一个函数,并在其中定义一个闭包,把需要调用的那项操作(target)裹在闭包中,然后返回该闭包,这种闭包叫做 target 的绑定函数(bound function)function createTask(target, ...args) {return () => {target(...args)}}
这样就能通过一个单独的组件(task)来控制并安排某项有待执行的任务(target),这个组件就是 Command 模式的 Invokerconst task = target.bind(null, ...args)
用复杂些的方法实现 Command
```typescript const statusUpdates = new Map()
// The Target
export const statusUpdateService = {
postUpdate (status) {
const id = Math.floor(Math.random() * 1000000)
statusUpdates.set(id, status)
console.log(Status posted: ${status})
return id
},
destroyUpdate (id) {
statusUpdates.delete(id)
console.log(Status removed: ${id})
}
}
```typescriptexport function createPostStatusCmd (service, status) {let postId = null// The Commandreturn {run () {postId = service.postUpdate(status)},undo () {if (postId) {service.destroyUpdate(postId)postId = null}},serialize () {return { type: 'status', action: 'post', status: status }}}}
import superagent from 'superagent'// The Invokerexport class Invoker {constructor () {this.history = []}run (cmd) {this.history.push(cmd)cmd.run()console.log('Command executed', cmd.serialize())}delay (cmd, delay) {setTimeout(() => {console.log('Executing delayed command', cmd.serialize())this.run(cmd)}, delay)}undo () {const cmd = this.history.pop()cmd.undo()console.log('Command undone', cmd.serialize())}async runRemotely (cmd) {await superagent.post('http://localhost:3000/cmd').send({ json: cmd.serialize() })console.log('Command executed remotely', cmd.serialize())}}
import { createPostStatusCmd } from './createPostStatusCmd.js'import { statusUpdateService } from './statusUpdateService.js'import { Invoker } from './invoker.js'// The Client codeconst invoker = new Invoker()const command = createPostStatusCmd(statusUpdateService, 'HI!')invoker.run(command)invoker.undo()invoker.delay(command, 1000 * 3)invoker.runRemotely(command)
