关于怎样把对象组合起来,并规定出这些对象之间的交流方式,让组合而成的这种结构,变得易于扩展、易于形成模块、易于复用且易于适配。
- 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 = options
this.queue = []
this.currentState = null
this.socket = null
this.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 = failsafeSocket
this.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 = false
for (const data of this.failsafeSocket.queue) {
this._safeWrite(data)
}
// 发生这样的事件说明服务器下线了
this.failsafeSocket.socket.once('error', () => {
this.hasDisconnected = true
this.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 usage
failsafeSocket.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` 符号来表示。
```typescript
class 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 = 0
let nextCol = 0
return {
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 null
yield '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 world
console.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 = 0
let nextCol = 0
while (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 结构来迭代,和下面这段代码实现的功能相同:
```typescript
const 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.value
try {
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 = socket
this.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 = initialMessage
for 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 的中间件系统
服务器端
```typescript
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.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}
)
}
}
```typescript
export function createPostStatusCmd (service, status) {
let postId = null
// The Command
return {
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 Invoker
export 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 code
const invoker = new Invoker()
const command = createPostStatusCmd(statusUpdateService, 'HI!')
invoker.run(command)
invoker.undo()
invoker.delay(command, 1000 * 3)
invoker.runRemotely(command)