toBeTheLight.github.io 荒原

阅读:《图解设计模式》8 管理状态


书中将 Observer 观察者模式、Memento 备忘录模式、State 状态模式归纳为状态相关的设计模式。 观察者模式:观察对象管理监听他的所有观察者,并在发生变化时通知所有观察者。 备忘录模式:捕获一个对象的内部状态,并在该对象之外保存这个状态,以便需要时将该对象恢复到原先保存的状态。 状态模式:将不同状态下的行为封装为不同的类,允许在状态改变时通过切换状态类改变行为。

Observer 观察者模式

定义

观察对象管理听他的所有观察者,并在发生变化时通知所有观察者。

角色

  • 抽象观察目标:定义或实现了注册观察者、删除观察者、通知观察者等方法。
  • 具体观察目标:实现抽象观察目标接口。
  • 抽象观察者:定义了接收观察目标状态变化的接口。
  • 具体观察者:实现抽象观察者接口。

JavaScript 实现

// 抽象观察目标
class Subject {
  constructor ({ value }) {
    this.value = value || 0
    this.observers = []
  }
  add (observer) {
    this.observers.push(observer)
  }
  notify (...args) {
    this.observers.forEach((observer) => observer.update(this, ...args))
  }
  getValue () {
    return this.value
  }
}
// 具体观察目标
class Data extends Subject {
  constructor ({ value }) {
    super({ value })
  }
  run () {
    while(this.value < 10) {
      this.value += 1
      this.notify(Date.now())
    }
  }
}
// 抽象观察者
class Observer {
  constructor () {
  }
  update () {
    throw new Error('需实现 Observer 类的 update 方法')
  }
}
// 具体观察者
class TimeObserver extends Observer {
  update (subject, time) {
    console.log(time, subject.getValue())
  }
}
class StepObserver extends Observer {
  constructor () {
    super()
    this.lastTime = 0
  }
  update (subject, time) {
    const step = this.lastTime === 0 ? 0 : time - this.lastTime
    this.lastTime = time
    console.log(step, subject.getValue())
  }
}
// 使用
const data = new Data(2)
data.add(new TimeObserver())
data.add(new StepObserver())
data.run()

思考

  • 模式对比:
    • 中介者模式类似的是,都通过消息推送的方式通知动作。不同的是,中介者模式是多个子对象向中介者对象推送消息,由中介者协调调用目标子对象;而在订阅者模式中是一个被观察对象向它的多个观察者推送消息。
    • 在经典的观察者模式(如同实现代码)之上还延伸出了发布订阅模式,除观察者和被观察者之外,还存在一个接收和消费消息的消息中心,由它来连接观察者和订阅者,可以保证没有观察者和被观察对象时,系统也能正确存在。在结构上来说,可以当做是经典观察者模式和中介者模式的结合。

Memento 备忘录模式

定义

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。

角色

  • 发起人:管理自身的内部状态,并提供将当前状态创建为备忘录和从备忘录中恢复数据的功能。
  • 备忘录:负责存储发起人的内部状态,在需要的时候提供状态给发起人,同时提供两种类型的接口。一套供发起人使用,权限较高,一套供管理者使用,权限较低。
  • 管理者:对备忘录进行管理,提供保存和获取备忘录的功能,可以由使用者直接扮演。

JavaScript 实现

由于备忘录要提供两种权限等级的接口,我们选择使用文件和 Symbol 来实现。同时直接有系统使用者扮演管理者角色。

// random 文件
const MEMENTO_DATA = Symbol('MEMENTO_DATA')
const DATA = Symbol('data')
// 发起人
class Random {
  constructor (data = {}) {
    this[DATA] = data
  }
  createMemento () {
    return new Memento(this[DATA])
  }
  restoreMemento (memento) {
    this[DATA] = memento
  }
  get (key) {
    return this[DATA][key]
  }
  run (key) {
    this[DATA][key] = Math.random()
  }
}
// 备忘录
class Memento {
  constructor (data = {}) {
    this[MEMENTO_DATA] = JSON.parse(JSON.stringify(data))
  }
  get (key) {
    return this[MEMENTO_DATA][key] || -Infinity
  }
}
export {
  Random
}
// 使用方文件
import Random from './Random'
const key = 'num'
const random = new Random()
let memento = random.createMemento()
for (let i = 0; i < 100; i++) {
  random.run(key)
  if (random.get(key) > memento.get(key)) {
    memento = random.createMemento()
    console.log('值变大,更新数据', random.get(key))
  } else {
    random.restoreMemento(memento)
    console.log('值变小,恢复数据')
  }
}

State 状态模式

定义

将不同状态下的行为封装为不同的类,允许在状态改变时通过切换状态类改变行为。

角色

  • 环境:即暴露给用户的类,定义了客户感兴趣的接口,维护一个当前状态,并将与状态相关的操作委托给状态。
  • 抽象状态:定义了与状态相关的行为。
  • 具体状态:实现状态相关行为。

JavaScript 实现

// context
class Context {
  constructor () {
    this.state = new DayState()
  }
  setState (state) {
    this.state = state
  }
  run () {
    let time = 0
    setInterval(() => {
      if (time >= 24) time = 0
      this.state.setTime(this, time)
      this.state.health()
      time++
    }, 1000)
  }
}
// state
class State {
  setTime () {
    throw new Error('需实现 State 类的 setTime 方法')
  }
  health () {
    throw new Error('需实现 State 类的 health 方法')
  }
}
class DayState {
  setTime (context, time) {
    if (time < 8 || time >20) {
      context.setState(new NightState())
    }
  }
  health () {
    console.log('当前服务状态为,cpu:sth, 延迟:sth,内存:sth')
  }
}
class NightState {
  setTime (context, time) {
    if (time >= 8 && time <= 20) {
      context.setState(new DayState())
    }
  }
  health () {
    console.log('当前服务状态为,延迟:sth')
  }
}
// 使用
new Context().run()

思考

  • 策略模式对比:
    • 相同:两种设计模式的组成是基本相同的,都是以将核心功能封装到一个类中,通过委托调用的方式包装成供用户调用的接口。
    • 不同:
      • 在策略模式中,策略的使用是由用户决定的,需要用户对策略有所了解。而在状态模式中,状态的切换是由 Context 角色发起的调用在状态的方法中进行的,对用户来说无感知,但是却要求状态之间有所了解。
      • 策略模式在运行当中没有策略的切换,一个策略便能组成一个完整的功能。而状态模式中则是存在状态的切换。
  • 模式本身:
    • 由于状态的切换是发生在状态的方法内部的,所以要求状态对别的状态需要知晓。
    • 当增加了新的状态时,需要修改其他状态类,才能切换到新的状态上,是违背开闭原则的。当然,我们也可以把状态切换的能力从状态的职责中拿走,放到 Context 角色中,这样就要求 Context 对所有的状态知晓,特征贴近中介者模式。

Content