观察者模式: defines a one-to-many dependency between objects so that when one object changes state,all of its dependents are notified and updated automatically.
遇到的问题
一方数据的变动,与之相关的其他功能检测到该数据变动,也要做相应的变化,若一直相关的其他功能中的某一个不再因为其数据变化而变化也就是说切断联系,又怎么办?如何实现可维护,可扩展的代码?就像生活中你订阅了公众号消息,公众人有新的内容,推送给了订阅的你,以及订阅的其他人,若你取消订阅,则以后不再接收其公众号新消息内容。
主题(Subject)+ 观察者(Observer)= 观察者模式。这里主题相当于公众号,观察者相当于微信用户。
在这个过程中,观察者有增加也有减少,就像微信公众号有新用户关注和老用户取消关注。
值得一提的是,观察者也可以作为其他观察者的主题,就像公司部门人员层级划分那样。
-
注册,注销,通知,而且对其他开发者放开了开放能力;注册和通知,可以通过一个ArrayList来记录观察者
-
面向接口编程,而不是面向具体编程
{ // 面向具体编程的硬编码会很难维护和扩展,耦合度太高 clientA.update(data); clientB.updata(data); clientC.update(data); }
-
观察者能够知晓主题对象数据的状态,Keeping your Objects in the know
主题的推送push消息,与观察者拉取pull消息
代码模拟
在这里我们模拟气象数据展示,气象台通过物理传感器收集数据,然后将数据传到WeatherData对象,不同的布告板根据这些更新的数据及时展示不同的信息: 如天气预报布告板,当前天气数据布告板,目前天气统计布告板等,他们的更新都需要更具气象检测数据的更新而更新。Github: Observer Demo Source Code
-
主题接口,定义对观察者的管理
/** * 主题接口 */ public interface Subject { /** * 注册观察者 */ public void registerObserver(Observer observer); /** * 删除观察者 */ public void removeObserver(Observer observer); /** * 通知观察者 */ public void notifyObserver(); }
-
WeatherData对Subject接口的实现,通过定义一个ArrayList来存储Observer观察者
public class WeatherData implements Subject { /** * 组合观察者 */ private List<Observer> observers = null; /** * 气象站数据 */ private int temperature; // 温度 private int humidity; // 湿度 private int pressure; // 气压 public WeatherData(){ this.observers = new ArrayList<>(); } @Override public void registerObserver(Observer observer) { observers.add(observer); } @Override public void removeObserver(Observer observer) { int index = observers.indexOf(observer); if(index>= 0) { observers.remove(index); } } @Override public void notifyObserver() { for(Observer observer: observers){ observer.update(temperature,humidity,pressure); } } /** * 气象站数据更新,通知公布版 */ public void measurementsChange(){ notifyObserver(); } /** * 模拟物理基站给这个对象赋数据 */ public void setMeasurementsData(int temperature,int humidity,int pressure){ this.temperature = temperature; this.humidity = humidity; this.pressure = pressure; measurementsChange(); } }
-
Observer订阅者接口,为了与主题解耦,定义统一方法
/** * 观察者接口 */ public interface Observer { /** * 主题统一调用观察者接口 * @param temperature 温度 * @param humidity 湿度 * @param pressure 气压 */ public void update(int temperature,int humidity,int pressure); }
-
布告板DIsplayElemnet统一接口,用于展示信息
/** * 展示接口 */ public interface DisplayElement { /** * 用于展示数据 */ public void display(); }
-
目前的三种布告板订阅者,更具气象发布站检测的数据,分别展示各自的内容,如果以后还有其他布告板继承接口即可,而不用修改主题代码,非常低耦合。
/** * 显示当前气象数值的布告板 */ public class CurrentConditionsDisplay implements Observer,DisplayElement { private int temperature; private int humidity; /** * 主题引用,方便以后开发注销功能时调用 */ private Subject weatherData; public CurrentConditionsDisplay(Subject subject){ this.weatherData = subject; // 注册该观察者 weatherData.registerObserver(this); } @Override public void update(int temperature, int humidity, int pressure) { this.temperature = temperature; this.humidity = humidity; display(); } @Override public void display() { System.out.println("---------------当前气象数据------------------"); System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity"); } }
/** * 气象统计数据布告板展示 */ public class StatisticsDisplay implements Observer, DisplayElement { private int maxTemp = 0; private int minTemp = 200; private int tempSum= 0; private int numReadings; private Subject weatherData; public StatisticsDisplay(Subject subject){ this.weatherData = subject; this.weatherData.registerObserver(this); } @Override public void display() { System.out.println("---------------气象统计数据------------------"); System.out.println("Avg/Max/Min temperature = " + (tempSum / numReadings) + "/" + maxTemp + "/" + minTemp); } @Override public void update(int temperature, int humidity, int pressure) { this.tempSum += temperature; numReadings++; if (temperature > maxTemp) { maxTemp = temperature; } if (temperature < minTemp) { minTemp = temperature; } display(); } }
/** * 天气预报布告板 */ public class ForecaseDisplay implements Observer,DisplayElement { private int currentPressure = 27; private int lastPressure; private Subject weatherData; public ForecaseDisplay(Subject subject){ this.weatherData = subject; weatherData.registerObserver(this); } @Override public void display() { System.out.println("---------------当前天气预报------------------"); if (currentPressure > lastPressure) { System.out.println("Improving weather on the way!"); } else if (currentPressure == lastPressure) { System.out.println("More of the same"); } else if (currentPressure < lastPressure) { System.out.println("Watch out for cooler, rainy weather"); } } @Override public void update(int temperature, int humidity, int pressure) { lastPressure = currentPressure; currentPressure = pressure; display(); } }
-
场景的模拟: 气象站检测到数据,主题数据状态更新,通知订阅的布告板,布告板更新展示相关数据信息
/** * 模拟气象站物理基站检测到数据,到不同布告板上显示 */ public class WeatherStation { public static void main(String[] args) { // 主题 WeatherData weatherData = new WeatherData(); // 三个布告板订阅者,需要注册到主题中 CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData); StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData); ForecaseDisplay forecaseDisplay = new ForecaseDisplay(weatherData); // 模拟物理气象基站数据更新-> 导致主题数据状态更新 System.out.println("主题第一次更新"); weatherData.setMeasurementsData(80,65,30); System.out.println("主题第二次更新"); weatherData.setMeasurementsData(82,70,20); System.out.println("主题第三次更新"); weatherData.setMeasurementsData(78,90,29); } }
-
输出结果
主题第一次更新 ---------------当前气象数据------------------ Current conditions: 80F degrees and 65% humidity ---------------气象统计数据------------------ Avg/Max/Min temperature = 80/80/80 ---------------当前天气预报------------------ Improving weather on the way! 主题第二次更新 ---------------当前气象数据------------------ Current conditions: 82F degrees and 70% humidity ---------------气象统计数据------------------ Avg/Max/Min temperature = 81/82/80 ---------------当前天气预报------------------ Watch out for cooler, rainy weather 主题第三次更新 ---------------当前气象数据------------------ Current conditions: 78F degrees and 90% humidity ---------------气象统计数据------------------ Avg/Max/Min temperature = 80/82/78 ---------------当前天气预报------------------ Improving weather on the way!
分析
- 从上面的结果中我们可以看到,主题数据状态的更新,会自动通知订阅观察者,观察者也各自展示其负责的信息内容,非常灵活,简洁。
- 主题数据是通过推push的方式,将数据给订阅者,不难看到这些数据对于有些订阅者来说并没有用,反而成了垃圾数据。所以还存在另外一中订阅者获取数据的方式那就是拉pull,通过getter方法从主题数据状态中获取,这就要求将主题对象发送过来,让订阅自己选择数据。
Java中的Observable类与Observer接口
在java.util中定义了相关的主题Observable类和Observer接口,我们可以使用他们来实现观察者模式:Github Java observer demo source code
-
WeatherData主题通过继承java.util.Observale类,Observable类帮我们实现了管理观察者的功能,注意增加了getter方法,方便在订阅者中获取主题的状态数据。
import java.util.Observable; public class WeatherData extends Observable { /** * 气象站数据 */ private int temperature; // 温度 private int humidity; // 湿度 private int pressure; // 气压 /** * 气象站数据更新,通知公布版 */ public void measurementsChange(){ // 在调用notifyObservers之前需要调用该方法,表示状态已经该改变 setChanged(); // set change = true //notifyObservers(); // 订阅者拉数据 notifyObservers(this); //主题推数据 } /** * 模拟物理基站给这个对象赋数据 */ public void setMeasurementsData(int temperature,int humidity,int pressure){ this.temperature = temperature; this.humidity = humidity; this.pressure = pressure; measurementsChange(); } /** * 添加getter方法方便订阅者获取数据pull */ public int getTemperature() { return temperature; } public int getHumidity() { return humidity; } public int getPressure() { return pressure; } }
-
展示当前气象数据的布告板
import java.util.Observable; import java.util.Observer; public class CurrentConditionsDisplay implements DisplayElement, Observer { private int temperature; private int humidity; private Observable observable; public CurrentConditionsDisplay(Observable observable){ this.observable = observable; observable.addObserver(this); } @Override public void display() { System.out.println("---------------当前气象数据------------------"); System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity"); } /** * * @param o 主题 * @param arg 传递的参数 */ @Override public void update(Observable o, Object arg) { if(o instanceof WeatherData){ WeatherData weatherData = (WeatherData)o; temperature = weatherData.getTemperature(); humidity = weatherData.getHumidity(); display(); } } }
-
场景模拟:
/** * 模拟气象站物理基站检测到数据,到不同布告板上显示 */ public class WeatherStation { public static void main(String[] args) { // 主题 WeatherData weatherData = new WeatherData(); // 三个布告板订阅者,需要注册到主题中 CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData); // 模拟物理气象基站数据更新-> 导致主题数据状态更新 System.out.println("主题第一次更新"); weatherData.setMeasurementsData(80,65,30); System.out.println("主题第二次更新"); weatherData.setMeasurementsData(82,70,20); System.out.println("主题第三次更新"); weatherData.setMeasurementsData(78,90,29); } }
-
测试结果
主题第一次更新 ---------------当前气象数据------------------ Current conditions: 80F degrees and 65% humidity 主题第二次更新 ---------------当前气象数据------------------ Current conditions: 82F degrees and 70% humidity 主题第三次更新 ---------------当前气象数据------------------ Current conditions: 78F degrees and 90% humidity
原则
- 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起
- 针对接口编程,而不是实现编程
- 多用组合,少用继承
- 为了交互对象之间的松耦合设计而努力(松耦合能够降低对象之间的相互依赖,能够建立有弹性的OO系统)