本文搬运自《Head First 设计模式》第二章

工作合约

工作合约 恭喜贵公司获选为蔽公司建立下一代 Internet 气象观测站!该气象观测站必须建立在我们专利申请中的 WeatherData 对象上,由 WeatherData 对象负责追踪目前的天气状况(温度,适度,气压)。我们希望贵公司建立一个应用,有三种布告板,分别显示目前的状况,气象统计以及简单的预报。当 WeatherObject 对象获得最新的测量数据时,三种布告板必须时时更新。 而且,这是一个可以扩展的气象站,Weather-O-Rama 气象站希望公布一组 API,好让其他开发人员可以写出自己的气象布告板,并插入此应用中。我们希望贵公司能够提供这样的 API。 Weather-O-Rama 气象站有很好的商业运营模式:一旦客户上钩,他们使用每个布告板都要付钱。最好的部分就是,为了感觉贵公司建立此系统,我们将以公司的认股权支付你。 我们期待看到你的设计和应用的 alpha 版本。 真挚的 Johnny Hurricane —- Weather-O-Rama气象站执行长 附注:我们正在通宵整理 WeatherData 源文件给你们

气象监测应用的概况

此系统中的三个部分是气象站(获取实际气象数据的物理装置),WeatherData 对象(追踪来自气象站的数据,并更新布告板)和布告板(显示目前天气状况给用户看)。
分享二 观察者模式 - 图1

瞧一瞧刚送到的 WeatherData 类

如同他们所承诺的,隔天早上收到了 WeatherData 源文件,看了一下代码,一切都很直接:
分享二 观察者模式 - 图2

我们目前知道些什么

  • WeatherData 类提供了三个 getter 方法,可以取得三个测量值:温度,湿度和气压。
  • 当新的测量数据备妥时,measurementsChanged() 方法就会被调用(我们不在乎这个方法是怎么被调用的,我们只在乎它被调用了)。
  • 我们需要实现三个使用天气数据的布告板:“目前状态”布告,“气象统计”布告,“天气预报”布告。一旦有新的测量,这个布告需要马上更新。
  • 此系统必须可扩展,让其他开发人员建立定制的布告板,用户可以随心所欲地添加或删除任何布告板。目前初始的布告板有三类:“目前状况”布告,“气象统计”布告,“天气预报”布告。

    先看一个错误示范

    这是第一个可能的实现,我们依照 Weather-O- Rama 气象站开发人员的暗示,在measurementsChanged() 方法中添加我们的代码:

    1. export class WeatherData {
    2. public measurementsChanged() {
    3. // 调用三个 get 方法,以取得最近的测量值。
    4. const temp = this.getTemperature();
    5. const humidity = this.getHumidity();
    6. const pressure = this.getPressure();
    7. // 现在,更新三个布告栏。
    8. currentConditionsDisplay.update(temp, humidity, pressure);
    9. statisticsDisplay.update(temp, humidity, pressure);
    10. forecastDisplay.update(temp, humidity, pressure);
    11. }
    12. // 这里是其他 WeatherData 的方法
    13. }

    这样实现有什么问题:

  • 我们这是在针对实现编程,而非针对接口。

  • 对于每个新的布告板,我们都要修改代码。
  • 我们没有封装改变的部分。
  • 我们无法在运行时动态增加或删除布告栏。

    认识观察者模式

    我们看看报纸和杂志的订阅是怎么回事
  1. 报社的业务就是出版报纸
  2. 向某家报社订阅报纸,只要他们有新报纸出版,就会给你送来。只要你是他们的客户,你就会一直收到新报纸
  3. 当你不想再看报纸时,取消订阅,他们就不会再送新报纸来
  4. 只要报社还在运营,就会一直有人(或单位)向他们订阅报纸或取消订阅报纸

    出版者+订阅着=观察者模式

    如果你了解报纸订阅是怎么回事,其实就知道观察者模式是怎么回事,只是名称不太一样:出版者改称为“主题”(Subject),订阅者改称为“观察者”(Observer)。
    让我们来看得更仔细一点:
    分享二 观察者模式 - 图3

    定义观察者模式

    当你试图勾勒观察者模式时,可以利用报纸订阅服务,以你出版社和订阅者来比拟这一切。
    在真实世界中,你通常会看到观察者模式被定义成: :::info 观察者模式
    定义了对象之间一对多依赖,这样一来,当一个对象改变时,
    它的所有依赖者都会收到通知并自动更新。 ::: 分享二 观察者模式 - 图4

    松耦合的威力

    当两个对象之间松耦合,它们依然可以交互,但是不太清楚彼此的细节。
    观察者模式提供了一种对象设计,让主题和观察者之间松耦合。
    为什么呢?
    关于观察者的一切,主题只知道观察者实现了某个接口(Observer接口)。主题不需要知道观察者的具体类是谁,做了什么或其他细节。
    任何时候我们都可以增加新的观察者。因为主题唯一依赖的东西是一个实现 Observer 接口的对象列表,所以我们可以随时增加观察者。事实上,在运行时我们可以用新的观察者取代现有观察者,主题不受任何影响。同样的,也可以在任何时候删除某个观察者。
    有新类型的观察者出现时,主题的代码不需要修改。假如我们有个新的具体类需要当观察者,我们不需要为了兼容新类型而修改主题的代码,所有要做的就是在新的类里实现此观察者接口,然后注册为观察者即可。主题不在乎别的,它只会发送通知给所有实现了观察者接口的对象。
    我们可以独立的复用主题或观察者。如果我们在其他地方需要使用主题和观察者,可以轻易的复用,因为二者并非紧耦合。
    改变主题或观察者其中一方,并不会影响另一方。因为两者是松耦合的,所以只要他们之间接口仍被遵守,我们就可以自由得改变他们而不会有任何问题。 :::info 设计原则
    为了交互对象之间的松耦合设计而努力 ::: 松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的互相依赖降到了最低。

    完成气象站

    设计气象站

    分享二 观察者模式 - 图5

    实现气象站

    根据上面的 UML 图,我们要开始实现这个系统了。如果我们使用 Java 去实现,实际上在 java.util 包中已经内置了最基本的 Observer 接口与Observable 类,这和我们的 Subject 接口 与 Observer 接口很相似。既然我们时前端开发那么我们就用 TS 去实现。
    所以,让我们从建立接口开始: ```typescript export interface Subject { registerObserver(o: Observer); removeObserver(o: Observer); notifyObservers(); }

export interface Observer { update(temp: number, humidity: number, pressure: number); }

export interface DisplayElement { display(); }

  1. > 把观测值直接传入观察者中是更新状态最直接的方法。你认为这样做法明智吗?
  2. > 暗示:这些观测值的种类和个数在未来有可能改变吗?如果以后会改变,这些变化是否被很好得封装?
  3. 别担心,在我们完成第一次实现后,我们会再回来探讨这个设计决策。
  4. <a name="s7B35"></a>
  5. ### 在 WeatherData 中实现主题接口
  6. 还记得我们在一开始试图实现的 WeatherData 类吗?我们现在要用观察者模式实现。
  7. ```typescript
  8. export class WeatherData implements Subject {
  9. private observers: Set<Observer> = new Set();
  10. private temperature: number;
  11. private humidity: number;
  12. private pressure: number;
  13. public registerObserver(o: Observer) {
  14. this.observers.add(o);
  15. }
  16. public removeObserver(o: Observer) {
  17. if (this.observers.has(o)) {
  18. this.observers.delete(o);
  19. }
  20. }
  21. public notifyObservers() {
  22. this.observers.forEach((observer) => {
  23. observer.update(this.temperature, this.humidity, this.pressure);
  24. });
  25. }
  26. public measurementsChanged() {
  27. this.notifyObservers();
  28. }
  29. /**
  30. * 为了测试,模拟读取气象数据
  31. */
  32. public setMeasurements(temp: number, humidity: number, pressure: number) {
  33. this.temperature = temp;
  34. this.humidity = humidity;
  35. this.pressure = pressure;
  36. this.measurementsChanged();
  37. }
  38. }

现在我们来建立布告板吧

我们已经把 WeatherData 类写出来了,现在轮到布告板了。Weather-O-Rama 气象站订购了三个布告板:目前状况布告板,统计布告板和预测布告板。我们先看看目前布告板。

  1. /**
  2. * 目前状况布告板
  3. */
  4. export class CurrentConditionsDiaplay implements Observer, DisplayElement {
  5. private temperature: number;
  6. private humidity: number;
  7. public constructor(private weatherData: Subject) {
  8. weatherData.registerObserver(this);
  9. }
  10. public update(temp: number, humidity: number) {
  11. this.temperature = temp;
  12. this.humidity = humidity;
  13. this.display();
  14. }
  15. public display() {
  16. console.log(
  17. `Current conditions: ${this.temperature}F degress and ${this.humidity}%humidity`
  18. );
  19. }
  20. }

很方便得,我们实现了另外两块布告板

  1. /**
  2. * 统计布告板
  3. */
  4. export class StatisticsDisplay implements Observer, DisplayElement {
  5. private tempSum = 0;
  6. private maxTemp = 0;
  7. private minTemp = 200;
  8. private numReadings = 0;
  9. public constructor(private weatherData: Subject) {
  10. weatherData.registerObserver(this);
  11. }
  12. public update(temp: number, humidity: number, pressure: number) {
  13. this.tempSum += temp;
  14. this.numReadings++;
  15. if (temp > this.maxTemp) {
  16. this.maxTemp = temp;
  17. }
  18. if (temp < this.minTemp) {
  19. this.minTemp = temp;
  20. }
  21. this.display();
  22. }
  23. public display() {
  24. console.log(
  25. 'Avg/Max/Min temperature = ' +
  26. this.tempSum / this.numReadings +
  27. '/' +
  28. this.maxTemp +
  29. '/' +
  30. this.minTemp
  31. );
  32. }
  33. }
  1. /**
  2. * 预测布告板
  3. */
  4. export class ForecastDisplay implements Observer, DisplayElement {
  5. private currentPressure = 29.92;
  6. private lastPressure = 0;
  7. public constructor(private weatherData: Subject) {
  8. weatherData.registerObserver(this);
  9. }
  10. public update(temp: number, humidity: number, pressure: number) {
  11. this.lastPressure = this.currentPressure;
  12. this.currentPressure = pressure;
  13. this.display();
  14. }
  15. public display() {
  16. console.log('Forecast: ');
  17. if (this.currentPressure > this.lastPressure) {
  18. console.log('Improving weather on the way!');
  19. } else if (this.currentPressure == this.lastPressure) {
  20. console.log('More of the same');
  21. } else if (this.currentPressure < this.lastPressure) {
  22. console.log('Watch out for cooler, rainy weather');
  23. }
  24. }
  25. }

启动气象站

先建立一个测试程序

气象站已经完成得差不多了,我们还需要一些代码将一切连接在一起:

  1. export class WeatherStation {
  2. public main() {
  3. const weatherData = new WeatherData();
  4. new CurrentConditionsDiaplay(weatherData);
  5. new StatisticsDisplay(weatherData);
  6. new ForecastDisplay(weatherData);
  7. weatherData.setMeasurements(80, 65, 30.4);
  8. weatherData.setMeasurements(82, 70, 29.2);
  9. weatherData.setMeasurements(78, 90, 29.2);
  10. }
  11. }

运行程序

image.png

新需求:加入酷热指数布告板

刚刚接到气象站来电通知,他们还需要酷热指数布告板,而我们实现起来也很容易,之前涉及的代码完全不需要改动什么,只需要新建一个类就可以了:

  1. /**
  2. * 酷热指数布告板
  3. */
  4. export class HeatIndexDisplay implements Observer, DisplayElement {
  5. private heatIndex = 0;
  6. public constructor(private weatherData: Subject) {
  7. weatherData.registerObserver(this);
  8. }
  9. public update(temp: number, humidity: number, pressure: number) {
  10. this.heatIndex = this.computeHeatIndex(temp, humidity);
  11. this.display();
  12. }
  13. public display() {
  14. console.log('Heat index is ' + this.heatIndex);
  15. }
  16. private computeHeatIndex(t: number, rh: number) {
  17. return (
  18. 16.923 +
  19. 0.185212 * t +
  20. 5.37941 * rh -
  21. 0.100254 * t * rh +
  22. 0.00941695 * (t * t) +
  23. 0.00728898 * (rh * rh) +
  24. 0.000345372 * (t * t * rh) -
  25. 0.000814971 * (t * rh * rh) +
  26. 0.0000102102 * (t * t * rh * rh) -
  27. 0.000038646 * (t * t * t) +
  28. 0.0000291583 * (rh * rh * rh) +
  29. 0.00000142721 * (t * t * t * rh) +
  30. 0.000000197483 * (t * rh * rh * rh) -
  31. 0.0000000218429 * (t * t * t * rh * rh) +
  32. 0.000000000843296 * (t * t * rh * rh * rh) -
  33. 0.0000000000481975 * (t * t * t * rh * rh * rh)
  34. );
  35. }
  36. }

再看刚才的那个设计决策

我们可以看一下 java.uitl.Observer 是怎么做的:
它也有一个 update 方法,但是方法签名不太一样

  1. update(Observable o, Object arg)

这样做就比较灵活了,
主题本身当作第一个个变量,好让观察者知道是哪个主题通知它的。
第二个参数是具体的数据对象,这样就能做到通用。
感兴趣的同学可以用最新的设计方式重构上面的实现。

本章要点

  • 观察者模式定义了对象之间一对多的关系
  • 主题(也就是可观察者)用一个共同的接口来更新观察者
  • 观察者和可观察者之间用松耦合的方式结合(loosecoupling),可观察者不知道观察者的细节,只知道观察者实现了观察者接口。

    本章源码