(本文是基于《Head First 设计模式》观察者模式的学习笔记)
引例:设计天气站
我司最近接到一笔订单,要求我们在 WeatherData
对象上建立一个应用。要求如下:
该应用需要有三种布告板,分别显示当前的状况(currentConditions
),气象统计(statistics
)以及简单的预报(forecast
),而且必须是实时更新的。
此外,这得是一个可扩张的气象站,甲方希望公布一组 API,好让其他开发人员可以写出自己的气象布告板,并插入到此应用中。希望我司能够提供这样的 API。
模板源文件
甲方在第二天提供了提供了模板源文件:
class WeatherData {
// 获取温度、湿度、压力。我们不需要知道具体的获取步骤
// 只需要直接调用即可。
getTemperature() {}
getHumidity() {}
getPressure() {}
// 一旦气象测量发生变化,调用此方法。
measurementChanged() {}
// ...
}
复制代码
我们的主要任务就是实现 measurementChanged()
,好让它更新当前状况,气象统计,天气预报的显示布告板。
任务分析
WeatherData
类具有各种getter
方法,可以得到三个测量值:温度、湿度和压力。- 当数据发生变化是,
measurementChanged()
方法就会被调用。(这与vue
很像)而且,我们不在乎该方法是如何被调用的,我们只在乎它被调用了。 - 我们需要实现三个使用天气数据的布告板:“当前状况”布告板、“气象统计”布告板、“天气预报”布告板。一旦
WeatherData
数据发生变化,这些布告板立刻更新。 - 该系统必须可以扩张。其他开发人员可以建立定制的布告板,用户可以随心所欲地添加或删除任何布告板。
错误示例
在开始动手之前,我们来看一个错误的示范:
class WeatherData {
// ...
measurementChanged() {
const temp = this.getTemperature()
const humidity = this.getHumidity()
const pressure = this.getPressure()
// 更新布告板,后面会实现
currentConditionsDisplay.update(temp, humidity, pressure)
statisticsDisplay.update(temp, humidity, pressure)
forecastDisplay.update(temp, humidity, pressure)
}
}
复制代码
潜在问题:
- 高度耦合。
显然,我们在measurementChanged()
中使用了xxxDisplay
。针对具体的实现编程,会导致我们以后在增加或删除布告板是,必须修改程序。 - 缺乏封装性。
对于布告板,我们调用了其update
方法,很明显,我们可以将其统一成一个接口,并且参数为temp, humidity, pressure
。
为了解决该问题,我们可以使用观察者模式。
观察者模式
在讲观察者模式之前,来看生活中的一个例子——报纸和杂志的订阅。
- 我们向某家报社订阅报纸。只要他们有新报纸出版,就会给我们送过来。只要我们还是他们的用户,他们就会不断地给我们送报纸。
- 当我们不想再收到报纸的时候,我们可以取消订阅,报社将不会再送新的报纸过来。
- 只要报社还在运行,就会有新的用户/单位向他们订阅报纸/取消订阅。
从上图中我们可以知道,当用户订阅了出版社的报纸时,每当出版社有新的报纸,就会通知(notifyObservers)订阅的用户。没有订阅的则不会收到出版社的通知。
事实上,观察者就是这样一个原理。如果我们把出版社改为“主题”(Subject),订阅者/用户改为“观察者”(Observer)。这就是我们的观察者模式。
从上面的图中我们可以知道,一个主题对应多个对象。也就是说观察者模式定义了一对多的关系。
定义
观察者模式:定了了对象之间的一对多依赖。当一个对象的状态发生改变时,它的所有依赖者都会收到通知并且更新。
模型
在观察者模式中,主题(Subject)可以添加观察者(addObserver),删除观察者(deleteObserver),通知(发布消息)所有的观察者(notifyObservers)。对于我们的观察者(Observer)对象,应该提供一个更新(update)功能,当主题发生变化时,会根据我们提供的更新功能进行更新。
类比到生活中的例子。报社就是主题(Subject),报社的用户就是观察者(Observer)。报社可以添加用户、删除用户和通知用户,对于没用订阅的用户,报社不会给予理睬。对于用户一端,用户可以向报社订阅报纸,也可以取消订阅。
下面来看观察者模式的抽象代码:
interface Observer {
update(/*some states*/): any
}
interface Subject {
observers: Observer[]
addObserver(observer: Observer): void
deleteObserver(observer: Observer): void
notifyObservers(): void
}
复制代码
具体实现
class ConcreteSubject implements Subject {
private readonly observers: Observer[] = []
private state: number = 1
addObserver(observer: Observer): void {
this.observers.push(observer)
}
deleteObserver(observer: Observer): void {
const index = this.observers.indexOf(observer)
if (index >= 0) {
for (let i = index, len = this.observers.length - 1; i < len; i++) {
this.observers[i] = this.observers[i + 1]
}
this.observers.pop()
}
}
notifyObservers(): void {
this.observers.forEach((observer) => observer.update())
}
getState(): number {
return this.state
}
setState(state: number) {
if (this.state !== state) {
this.state = state
this.notifyObservers()
}
}
}
class ConcreteObserver implements Observer {
constructor(private subject: Subject, private name?: string) {
subject.addObserver(this)
}
update() {
console.log(`${this.name} update data/state.`)
}
deregister() {
this.subject.deleteObserver(this)
}
}
复制代码
- 主题实现具体的接口。
- 主题在更新状态(
setState
)时,通知观察者(notifyObservers
)。 - 观察者在创建的时候,订阅主题(调用
subject.addObserver
)。 - 观察者实现
update
函数。一旦主题状态发生变化,观察者就可以接收到变化。
简单地测试:
const subject = new ConcreteSubject()
const observer1 = new ConcreteObserver(subject, 'observer1')
const observer2 = new ConcreteObserver(subject, 'observer2')
const observer3 = new ConcreteObserver(subject, 'observer3')
subject.setState(100)
console.log('----------')
observer2.deregister()
subject.setState(10)
// observer1 update data/state.
// observer2 update data/state.
// observer3 update data/state.
// ----------
// observer1 update data/state.
// observer3 update data/state.
复制代码
上述例子中,观察者(Observer
)并没有获取主题(Subject
)的状态。事实上,我们可以在观察者中获取到主题的状态。
interface Observer {
// ...
// 获取主题状态。只需要主题在调用观察者 update 方法时,
// 传入主题的状态即可。
update(state: any): void
}
复制代码
松耦合
观察者模式提供了一种对象设计,让主题和观察者之间松耦合。
主题(Subject)只知道观察者(Observer)实现了某个接口(如上面的 Observer
接口)。主题不需要知道观察者的具体类是什么,做了什么事。
由于主题的唯一依赖是一个实现了 Observer
的接口对象列表,所以我们可以随时添加观察者( addObserver
)。
此外,我们可以在运行时用新的观察者代替现有的观察者,主题不会受到影响。同样的,我们也可以在任何地方删除某些观察者(deleteObserver)。
更重要的是,我们可以独立地复用主题或者观察者。如果在别的地方需要用到主题/观察者,我们可以轻易复用,因为它们两者之间的耦合性很低。
气象站的实现
我们现在就把之前的气象站任务实现了。这里需要实现一个主题(WeatherData
),三个观察者(CurrentConditionsDisplay, StatisticsDisplay, ForecastDisplay)
主题的实现
class WeatherData implements Subject {
private _temperature: number = 0
private _humidity: number = 0
private _pressure: number = 0
private _observers: Observer[] = []
getTemperature() {
return this._temperature
}
getHumidity() {
return this._humidity
}
getPressure() {
return this._pressure
}
// 为了实现接口适配
// ⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇
measurementChanged() {
this.notifyObservers()
}
setMeasurements(temperature: number, humidity: number, pressure: number) {
this._temperature = temperature
this._humidity = humidity
this._pressure = pressure
this.measurementChanged()
}
addObserver(observer: Observer): void {
this._observers.push(observer)
}
deleteObserver(observer: Observer): void {
const index = this._observers.indexOf(observer)
if (index >= 0) {
for (let i = index, len = this._observers.length - 1; i < len; i++) {
this._observers[i] = this._observers[i + 1]
}
this._observers.pop()
}
}
notifyObservers(): void {
this._observers.forEach((observer) =>
observer.update(this._temperature, this._humidity, this._pressure)
)
}
}
复制代码
这里需要注意的点是,我们实现 measurementChanged
是通过 notifyObservers
进行委托实现的。这样做主要是为了与甲方的要求一致。
观察者的实现
interface DisplayElement {
display(): void
}
class CurrentConditionsDisplay implements Observer, DisplayElement {
private _temperature!: number
private _humidity!: number
// private subject: Subject
constructor(private subject: Subject) {
subject.addObserver(this)
}
display(): void {
if (
typeof this._temperature !== 'undefined' &&
typeof this._humidity !== 'undefined'
) {
console.log(
`Current conditions: ${this._temperature}F degrees and ${this._humidity}% humidity`
)
}
}
update(temperature: number, humidity: number, pressure: number) {
this._temperature = temperature
this._humidity = humidity
this.display()
}
}
class StatisticsDisplay implements Observer, DisplayElement {
private _temperatures: number[] = []
// private subject: Subject
constructor(private subject: Subject) {
subject.addObserver(this)
}
display(): void {
let min: string | number = 0
let max: string | number = 0
let avg: string | number = 0
if (this._temperatures.length === 0) {
min = 'unknown'
max = 'unknown'
avg = 'unknown'
}
this._temperatures.sort((a, b) => a - b)
min = this._temperatures[0]
max = this._temperatures[this._temperatures.length - 1]
avg =
this._temperatures.reduce((prev, cur) => prev + cur, 0) /
this._temperatures.length
console.log(
`Avg/Max/Min temperature = ${avg.toFixed(2)}/${min.toFixed(
2
)}/${max.toFixed(2)}`
)
}
update(temperature: number, humidity: number, pressure: number) {
this._temperatures.push(temperature)
this.display()
}
}
class ForecastDisplay implements Observer, DisplayElement {
// private _temperatures!: number
// private _humidity!: number
// private _pressure!: number
// private subject: Subject
constructor(private subject: Subject) {
subject.addObserver(this)
}
display(): void {
console.log('Forecast: Improving weather on the way!')
}
update(temperature: number, humidity: number, pressure: number) {
this.display()
}
}
复制代码
除了实现 Observer
接口外,我们还实现了 DisplayElement
接口,方便展示。
简单地测试
function weatherStation() {
const weatherData = new WeatherData()
const currentConditionsDisplay = new CurrentConditionsDisplay(weatherData)
const statisticsDisplay = new StatisticsDisplay(weatherData)
const forecastDisplay = new ForecastDisplay(weatherData)
console.log('-------------------------------------------')
weatherData.setMeasurements(80, 65, 30.4)
console.log('-------------------------------------------')
weatherData.setMeasurements(82, 70, 29.2)
console.log('-------------------------------------------')
weatherData.setMeasurements(78, 90, 29.2)
}
weatherStation()
// -------------------------------------------
// Current conditions: 80F degrees and 65% humidity
// Avg/Max/Min temperature = 80.00/80.00/80.00
// Forecast: Improving weather on the way!
// -------------------------------------------
// Current conditions: 82F degrees and 70% humidity
// Avg/Max/Min temperature = 81.00/80.00/82.00
// Forecast: Improving weather on the way!
// -------------------------------------------
// Current conditions: 78F degrees and 90% humidity
// Avg/Max/Min temperature = 80.00/78.00/82.00
// Forecast: Improving weather on the way!
复制代码
从输出的结果上看:当我们执行 weatherData.setMeasurements
时,我们的布告板就会执行 update
操作,这就是我们想要的结果。
vue 中的观察者模式
vue 通过主题去完成依赖收集,通过观察者去实现页面更新。
由于主题需要去收集依赖,vue 采用了一个巧妙的设计:利用 js 单线程将依赖(也就是我们的观察者)添加到主题的一个静态属性上。所以我们可以在主题的代码上看到。
class Dep {
static target
// others...
}
复制代码
其次就是收集依赖的时机。对于每一份数据,vue 需要进行依赖收集。例如:
const data: {
msg: 'hello vue',
person: {
name: ’foo’,
age: 18
},
},
Object.keys(data).forEach((key) => {
defineReactive(data, key, data[key])
})
function defineReactive(data, key, value) {
// ⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇
// 设置主题 / 收集依赖
const dep = new Dep()
// value 可能是一个对象
observe(value)
Object.defineProperty(data, key, {
get() {
// ⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇
dep.addSubscriber(Dep.target)
return value
},
set(newValue) {
// console.log('observer: key = ', key)
if (value === newValue) {
return
}
value = newValue
// newValue 可能是一个对象
observe(newValue)
// ⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇
dep.notifySubscribers()
}
})
}
复制代码
遍历 data 中的数据时,每一份就是一个主题(或者说依赖)。添加观察者的时机在于使用到该份数据。所以我们可以在 get()
中看到,dep.addSubscriber()
。
再者,通知观察者的时机在于数据更新时。所以我们可以在 set()
中看到 dep.notifySubscribers()
。
以 data.msg
为例。当我们将 data.msg
设置为响应式的时候,为其创建一份主题(const dep = new Dep()
)。
其次,我们要为 data.msg
主题添加观察者,添加观察者的时机在于我们使用了 data.msg
。而使用 data.msg
会触发其 get
方法。所以我们在 get
方法内部进行依赖收集(dep.addSubscriber(Dep.target)
)。
但是,这里存在的问题是我们如何确定我们需要添加的观察者(依赖)呢?
事实上,我们需要在使用 data.msg
的 get
方法之前就确定好观察者。
vue 以用 js 单线程的特点,在观察者中使用到依赖时,将自己挂到 Dep.target
上。
然后,当执行 data.msg
的 get
方法时,Dep.target
就是我们要的观察者(依赖)。于是在观察者中才有如下代码:
class Watcher {
// ...
get() {
// 在使用依赖之前,将自己挂到 Dep.target 上。
Dep.target = this
// ⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇
// 使用到依赖
const newValue = Watcher.compute(this.expr, this.scope)
// 收集依赖完成,卸载。
Dep.target = null
return newValue
}
}
复制代码
具体的代码如下:
defineReactive
/**
* 将对象设置为响应式。
* @param {Object} data
* @param {*} key
* @param {*} value
*/
export function defineReactive(data, key, value) {
const dep = new Dep()
// value 可能是一个对象
observe(value)
Object.defineProperty(data, key, {
configurable: false,
enumerable: true,
get() {
dep.addSubscriber(Dep.target)
return value
},
set(newValue) {
// console.log('observer: key = ', key)
if (value === newValue) {
return
}
value = newValue
// newValue 可能是一个对象
observe(newValue)
dep.notifySubscribers()
}
})
}
复制代码
观察者
let uuid = 0
export class Watcher {
/**
* 观察者
* @param {string} expr expression
* @param {Object} scope
* @param {Function | undefined} cb callback
*/
constructor(expr, scope, cb) {
this.expr = expr
this.scope = scope
this.cb = cb
this.uuid = uuid++
this.update()
}
/**
* 根据表达式获取相应的数据。
*/
get() {
Dep.target = this
const newValue = Watcher.compute(this.expr, this.scope)
Dep.target = null
return newValue
}
update() {
let newValue = this.get()
if (isFunction(this.cb)) {
this.cb(newValue)
}
// console.log(newValue)
}
/**
* 计算表达式。
* @param {string} expr expression
* @param { Object } scope 作用域。
* @returns {*}
*/
static compute(expr, scope) {
try {
const func = new Function('scope', 'with(scope){return ' + expr + '}')
return func(scope)
} catch (e) {
console.error('watcher: ', e)
}
}
}
复制代码
主题
export class Dep {
/** @type{ Watcher } */
static target
constructor() {
/** @type{Map<number, Watcher>} */
// 使用 Map 和 使用 [] 类似。
this.subscribers = new Map()
}
/**
* 添加观察者。
* @param { Watcher } subscriber
*/
addSubscriber(subscriber) {
if (!subscriber) {
return
}
this.subscribers.set(subscriber.uuid, subscriber)
}
/**
* 通知观察者。
*/
notifySubscribers() {
this.subscribers.forEach((watcher) => {
watcher.update()
})
}
}
复制代码
小结
- 观察者模式定义了对象之间一对多的关系。
- 主题用共同的接口来更新观察者(
Observer.update
)。 - 观察者和倍观察者之间使用松耦合(loosecoupling) 方式结合。被观察者不知道观察者的细节,只知道观察者实现了相应的接口。
观察者模式中使用的设计原则;
- 变与不变。在观察者模式中,会变化的是主题(
Subject
)的状态(state
),以及观察者的数量(observers
)。在这个模式下,我们可以改变依赖于主题状态的对象,而不用修改主题。 - 面向接口,而非面向实现。主题于观察者都使用接口实现,观察者利用主题的接口向主题注册(
addObserver
),而主题利用观察者接口通知观察者(Observer.update
),这样可以让两者正常运转,同时又具有松耦合的特性。 - 多用组合,少用继承。观察者模式利用组合讲许多的观察者组合到主题中(
observers
),对象之间的关系不是通过继承产生的,而是在运行时利用组合的方式而产生的。