观察者模式,是一种行为性模型,又叫发布-订阅模式,定义了对象之间的一对多关系,当一个对象改变状态时,它所有的依赖者(观察者对象)都会收到通知。

对象之间是松耦合的,可以交互,但都不太清楚对方的细节。

假设我们现在有一个需求,实现一个气象站,当天气发生变化时需要在气象站的三个布告板上显示相关信息。

需求分析:

  • 天气是被观察的对象,即被观察者 Observable
  • 三个布告板是依赖于天气对象,他们需要观察天气发生变化时,即观察者 Observer

天气对象我们定义为 Weather,它有三个变量:

  • temperature:温度
  • humidity:湿度
  • pressure:气压

三个布告板我们也需要进行定义,比如:

  • ConditionBoard:用于显示当前天气情况,温度、湿度和气压的具体值。
  • StatisticsBoard:用于显示温度统计情况,最高温度、最低温度、平均温度等。
  • ForecastBoard:用于显示气压预测天气情况,气压正在升高或降低。

    代码设计

    由前面可知,观察者模式定义了「一个被观察者」对应「多个观察者」的关系。在编写具体实现代码前,我们先抽象出「被观察者」和「观察者」的一些方法。

作为被观察者,理论上它至少有以下三个方法:

  • registerObserver(...):注册观察者
  • removeObserver(...):移除观察者
  • notifyObserver(): 通知观察者

当被观察者发出通知时,观察者需要有一个方法来接收通知:

  • receiveNotify(...):接收被观察者的通知

另外,在气象站这个需求中,每个布告板都是用来显示天气相关信息的,所以我们也可抽象出一个 display() 方法来显示信息内容。

具体实现

首先,我们来实现被观察者 Weather

  1. // 被观察者抽象接口
  2. public interface Observable {
  3. void registerObserver(Observer o);
  4. void removeObserver(Observer o);
  5. void notifyObserver();
  6. }
  7. // 天气对象
  8. public class Weather implements Observable {
  9. private float mTemperature;
  10. private float mHumidity;
  11. private float mPressure;
  12. private List<Observer> mObservers;
  13. public Weather() {
  14. mObservers = new ArrayList<>();
  15. }
  16. @Override
  17. public void registerObserver(Observer o) {
  18. mObservers.add(o);
  19. }
  20. @Override
  21. public void removeObserver(Observer o) {
  22. mObservers.remove(o);
  23. }
  24. @Override
  25. public void notifyObserver() {
  26. for(Observer o : mObservers) {
  27. o.receiveNotify(mTemperature, mHumidity, mPressure);
  28. }
  29. }
  30. // 设置天气测量结果后,携带数据通知各个被观察者
  31. public void setMeasurements(float temperature, float humidity, float pressure) {
  32. mTemperature = temperature;
  33. mHumidity = humidity;
  34. mPressure = pressure;
  35. notifyObserver();
  36. }
  37. }

事实上,被观察者可以设计得更加丰富一些。比如说,提供 “清空观察者列表”、“获取观察者数量” 等方法。

接着,我们来实现作为观察者的三个布告板。

  1. // 布告板行为抽象接口
  2. public interface Board {
  3. void display();
  4. }
  5. // 观察者接受通知抽象接口
  6. public interface Observer {
  7. void receiveNotify(float temperature, float humidity, float pressure);
  8. }
  9. // 当前天气情况布告板
  10. public class ConditionBoard implements Board, Observer {
  11. private float mTemperature;
  12. private float mHumidity;
  13. private float mPressure;
  14. @Override
  15. public void display() {
  16. System.out.println("温度:" + mTemperature + ",湿度" + mHumidity + ",气压:" + mPressure);
  17. }
  18. @Override
  19. public void receiveNotify(float temperature, float humidity, float pressure) {
  20. mTemperature = temperature;
  21. mHumidity = humidity;
  22. mPressure = pressure;
  23. display();
  24. }
  25. }
  26. // 温度统计布告板
  27. public class StatisticsBoard implements Board, Observer {
  28. private float mMaxTemperature = -Float.MAX_VALUE;
  29. private float mMinTemperature = Float.MAX_VALUE;
  30. private float mSumOfTemperature = 0f;
  31. private int mCount = 0;
  32. @Override
  33. public void display() {
  34. String text = "最高/最低/平均(温度):";
  35. text += (mMaxTemperature + "/" + mMinTemperature + "/" + mSumOfTemperature / mCount);
  36. System.out.println(text);
  37. }
  38. @Override
  39. public void receiveNotify(float temperature, float humidity, float pressure) {
  40. if (temperature > mMaxTemperature) {
  41. mMaxTemperature = temperature;
  42. }
  43. if (temperature < mMinTemperature) {
  44. mMinTemperature = temperature;
  45. }
  46. mSumOfTemperature += temperature;
  47. mCount++;
  48. display();
  49. }
  50. }
  51. // 气压预测布告板
  52. public class ForecastBoard implements Board, Observer {
  53. private float mCurrentPressure = Float.MAX_VALUE;
  54. private float mLastPressure = Float.MAX_VALUE;
  55. @Override
  56. public void display() {
  57. String message;
  58. if (mCurrentPressure > mLastPressure) {
  59. message = "气压正在升高";
  60. } else if (mCurrentPressure == mLastPressure) {
  61. message = "气压平稳";
  62. } else {
  63. message = "气压正在降低";
  64. }
  65. System.out.println(message);
  66. }
  67. @Override
  68. public void receiveNotify(float temperature, float humidity, float pressure) {
  69. mLastPressure = mCurrentPressure;
  70. mCurrentPressure = pressure;
  71. display();
  72. }
  73. }

最后,我们来实现气象站 WeatherStation

气象站相当一个管理者,管理「被观察者」和「观察者」对象的创建,以及他们之间订阅关系的建立和取消等。

  1. public class WeatherStation {
  2. private Weather mWeather;
  3. private ConditionBoard mConditionBoard;
  4. private StatisticsBoard mStatisticsBoard;
  5. private ForecastBoard mForecastBoard;
  6. public WeatherStation() {
  7. mWeather = new Weather();
  8. mConditionBoard = new ConditionBoard();
  9. mStatisticsBoard = new StatisticsBoard();
  10. mForecastBoard = new ForecastBoard();
  11. }
  12. public void onCreate() {
  13. mWeather.registerObserver(mConditionBoard);
  14. mWeather.registerObserver(mStatisticsBoard);
  15. mWeather.registerObserver(mForecastBoard);
  16. }
  17. public void onDestroy() {
  18. mWeather.removeObserver(mConditionBoard);
  19. mWeather.removeObserver(mStatisticsBoard);
  20. mWeather.removeObserver(mForecastBoard);
  21. }
  22. public void addBoard(Observer o) {
  23. mWeather.registerObserver(o);
  24. }
  25. public void removeBoard(Observer o) {
  26. mWeather.removeObserver(o);
  27. }
  28. public void start() {
  29. System.out.println("===第1次测量===");
  30. mWeather.setMeasurements(80, 65, 30.4f);
  31. System.out.println("===第2次测量===");
  32. mWeather.setMeasurements(82, 70, 29.2f);
  33. System.out.println("===第3次测量===");
  34. mWeather.setMeasurements(78, 90, 29.2f);
  35. }
  36. }

好像差不多了,我们来创建一个气象站并开始天气测量,然后验证下布告栏的显示内容:

  1. public static void main(String[] args) {
  2. WeatherStation station = new WeatherStation();
  3. station.onCreate();
  4. station.start();
  5. station.onDestroy();
  6. }
  1. ===第1次测量===
  2. 温度:80.0,湿度65.0,气压:30.4
  3. 最高/最低/平均(温度):80.0/80.0/80.0
  4. 无法预测
  5. ===第2次测量===
  6. 温度:82.0,湿度70.0,气压:29.2
  7. 最高/最低/平均(温度):82.0/80.0/81.0
  8. 气压正在降低
  9. ===第3次测量===
  10. 温度:78.0,湿度90.0,气压:29.2
  11. 最高/最低/平均(温度):82.0/78.0/80.0
  12. 气压平稳

从打印日志来看,我们总共测量了三次天气,每次更新测量结果的时候布告栏的内容也发生了变化。如果你在这三次测量过程中新增或移除布告栏,是不会对其它布告栏造成影响的,可以理解为,订阅者之间也是松耦合的。

扩展

在前文中,我们是从无到右完成了观察者模式的设计和开发。但实际上,Java 本身已经内置了一套观察者模式相关的类,相关源码如下:

  1. package java.util;
  2. public class Observable {
  3. private boolean changed = false;
  4. private Vector<Observer> obs;
  5. public Observable() {
  6. obs = new Vector<>();
  7. }
  8. public synchronized void addObserver(Observer o) {
  9. if (o == null)
  10. throw new NullPointerException();
  11. if (!obs.contains(o)) {
  12. obs.addElement(o);
  13. }
  14. }
  15. public synchronized void deleteObserver(Observer o) {
  16. obs.removeElement(o);
  17. }
  18. public void notifyObservers() {
  19. notifyObservers(null);
  20. }
  21. public void notifyObservers(Object arg) {
  22. Object[] arrLocal;
  23. synchronized (this) {
  24. if (!changed)
  25. return;
  26. arrLocal = obs.toArray();
  27. clearChanged();
  28. }
  29. for (int i = arrLocal.length-1; i>=0; i--)
  30. ((Observer)arrLocal[i]).update(this, arg);
  31. }
  32. public synchronized void deleteObservers() {
  33. obs.removeAllElements();
  34. }
  35. protected synchronized void setChanged() {
  36. changed = true;
  37. }
  38. protected synchronized void clearChanged() {
  39. changed = false;
  40. }
  41. public synchronized boolean hasChanged() {
  42. return changed;
  43. }
  44. public synchronized int countObservers() {
  45. return obs.size();
  46. }
  47. }
  1. package java.util;
  2. public interface Observer {
  3. void update(Observable o, Object arg);
  4. }

Java 内置的观察者模式在实际应用上跟我们在前文中自创的姿势是差不多的,只不多多了一些同步上的控制,所以这里不再作相关代码的演示。接下来,我们来分析下 Java 内置的观察者模式具体是怎样运作的?
观察者模式 - 图1
如何把对象变成观察者?
该对象需要实现观察者接口 java.util.Observer,然后通过 Observable 具体类的 addObserver() 方法在被观察者与观察者之间建立订阅关系。如果建立订阅关系后,我们想要移除某个观察者,调用对应的 deleteObserver() 方法来取消订阅关系即可。
被观察者如何发出通知?
首先,你要有一个被观察者(通过继承 java.util.Observable 类来实现),然后调用被观察者对象的 setChanged() 方法设置更改标识,最后调用 notifyObservers()notifyObservers(Object arg) 方法来发出通知。

注:Observable#notifyObservers(Object arg) 方法中的 arg 参数刚好对应 Observer#update(Observable o, Object arg) 方法的 arg 参数。 你可以这样理解 update(Observable o, Object arg) 方法中的两个参数:

  • arg 参数是被观察者额外「推送」过来的数据。
  • 如果你想获取被观察者其它数据的话就需要从 o 参数中进行「拉取」。

为什么要有个碍事的 **setChanged()** 方法?
嗯,这哥们确实是有点碍事,但如果你仔细想想的话,就会发现它的存在并不是没有意义的。setChanged() 方法可以让我们在通知观察者时有着更多的弹性,比如什么情况下不通知、什么情况下通知等。
举个栗子,实际生活中,气象站温度计的精度是很高的。如果每发现 0.001 度变化就更新数据的话,就会使得 Weather 对象持续不停地通知各个布告板。这不是我们想看到的,因为我们的预期可能是温度差至少达到 0.5 度时才更新数据。在这种情况下,我们就可以在满足某种条件时调用 setChanged() 方法,来掌控通知发出的时机。

总结

通过这篇文章,我们对观察者模式的基本思想已经有了一个基本了解,我们能从无到有、也能通过利用 Java 内置的观察者模式相关类进行代码设计和开发。