Skip to main content

Chain of Responsibility

一种将请求在一串对象中传递的方式, 寻找可以处理这个请求的对象:

  • 请求发送者只需知道链中的第一个节点, 从而降低发送者和一组接收者之间的强耦合.
  • 请求发送者可以任意选择第一个节点, 从而减少请求在链中的传递次数.
  • 职责链中的节点数量和顺序可以自由变化.
  • 符合开放封闭原则.
CoR Use Case
  • AOP: Aspect Oriented Programming.
  • Middlewares:
    • Redux.
    • Express/Koa.
    • NestJS.
  • Request handling.
  • Logging and error handling.
  • Event handling: DOM event capture and bubble chian.
  • Authorization and authentication.
  • JavaScript Prototype chain.
  • JavaScript Scope chain.
class Creature {
constructor(name, attack, defense) {
this.name = name
this.attack = attack
this.defense = defense
}

toString() {
return `${this.name} (${this.attack}/${this.defense})`
}
}

// Link Node.
class CreatureModifier {
constructor(creature) {
this.creature = creature
this.next = null
}

// Build chains.
add(modifier) {
if (this.next)
this.next.add(modifier)
else this.next = modifier
}

// Pass objects along to chains.
handle() {
if (this.next)
this.next.handle()
}
}

class NoBonusesModifier extends CreatureModifier {
constructor(creature) {
super(creature)
console.log('New')
}

handle() {
console.log('No bonuses for you!')
}
}

class DoubleAttackModifier extends CreatureModifier {
constructor(creature) {
super(creature)
console.log('New')
}

handle() {
console.log(`Doubling ${this.creature.name}'s attack`)
this.creature.attack *= 2
super.handle() // Call next();
}
}

class IncreaseDefenseModifier extends CreatureModifier {
constructor(creature) {
super(creature)
console.log('New')
}

handle() {
if (this.creature.attack <= 2) {
console.log(`Increasing ${this.creature.name}'s defense`)
this.creature.defense++
}
super.handle() // Call next();
}
}

const peekachu = new Creature('Peekachu', 1, 1)
console.log(peekachu.toString())

const root = new CreatureModifier(peekachu)
root.add(new DoubleAttackModifier(peekachu))
root.add(new IncreaseDefenseModifier(peekachu))
// Chain: creatureModifier -> doubleAttackModifier -> increaseDefenseModifier.
root.handle()

console.log(peekachu.toString())
import { Buffer } from 'node:buffer'

class Koa extends EventEmitter {
constructor() {
super()
this.middlewares = []
}

use(fn) {
this.middlewares.push(fn)
}

compose(middlewares, ctx) {
const dispatch = (index) => {
// End of chain.
if (index === middlewares.length)
return Promise.resolve()

// `next` function: call next middleware recursively.
const next = () => dispatch(index + 1)

// Call current middleware.
const middleware = middlewares[index]
return Promise.resolve(middleware(ctx, next))
}

return dispatch(0)
}

handleRequest(req, res) {
// When ctx.body doesn't change, statusCode contains '404'.
res.statusCode = 404

// Create context proxy for `req` and `res` operations.
const ctx = this.createContext(req, res)

// Middleware (open api for Koa users).
const fn = this.compose(this.middlewares, ctx)

fn.then(() => {
if (typeof ctx.body === 'object' && ctx.body !== null) {
res.setHeader('Content-Type', 'application/json;charset=utf8')
res.end(JSON.stringify(ctx.body))
} else if (ctx.body instanceof Stream) {
ctx.body.pipe(res)
} else if (typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)) {
res.setHeader('Content-Type', 'text/htmlCharset=utf8')
res.end(ctx.body)
} else {
res.end('Not Found')
}
}).catch((err) => {
this.emit('error', err)
res.statusCode = 500
res.end('Internal Server Error')
})
}

listen(...args) {
const server = http.createServer(this.handleRequest.bind(this))
server.listen(...args)
}
}

const app = new Koa()

app.use(async (ctx, next) => {
console.log(1)
await next()
console.log(2)
})

app.use(async (ctx, next) => {
console.log(3)

const p = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('3.5')
resolve()
}, 1000)
})

await p.then()
await next()
console.log(4)
ctx.body = 'Hello Koa'
})

app.listen(2323, () => {
console.log('Koa server are listening to http://localhost:2323 ...')
})