Observer Pattern(观察者模式)定义:
在对象之间定义一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象都会收到通知,并自动更新。
干说定义肯定没有举例理解的透彻。想到Observer Pattern(观察者模式)就来举个生活中的例子来帮助我们更好消化和理解其具体含义。
举例:
订阅杂志或者报纸,这里面有两个主角,一个是报纸杂志的供应商(报社),一个是报纸杂志的订阅者。这里就是被观察者也叫主题(供应商)和观察者(订阅者)。主题(Subject)应该有观察者名单,当主题有新的报纸售出时将按主体持有的观察者名单一个一个发送新报纸(发送没有先后顺序,一切按存储顺序发送)。
同时主题还应该有三个方法:
一、将观察者写入名单中(registerObserver())
二、将观察者从名单中删除(removeObserver())
三、当有新消息发送及时通知名单中所有观察者(notifyObservers())
1 public interface Subject {//主题接口,所有报社都要实现该接口 2 3 /*没有存储订阅者的列表,是因为我们不想在接口中写死存储方式, 4 让编程人员自己在实现接口的时候写入想要的存储方式(如:链表,数组,栈,队列等) 5 这样更合理。*/ 6 7 public void registerObserver(Observer o);//将订阅者登记在列表中 8 public void removeObserver(Observer o);//将订阅者从列表中移除 9 public void notifyObservers(Object arg);//有参通知方法,有新的消息即使通知列表中所有订阅者 10 public void notifyObservers();//无参通知方法 11 }
观察者所具有的东西就会少一些:
首先,内部需要有存储主题的对象,这样知道观察者所订阅的报社是哪一家,具有主题对象还有一个重要的原因,把登记、删除、通知观察者的功能全部委托给主题去做。
其次,还需要有更新自己消息的方法(update())新的消息发送过来,观察者也要及时更新自己内部消息,将旧的消息替换成新的消息。
1 public interface Observer{//所有订阅者要实现的接口 2 /*在接口中,没有主题对象,也是因为不想将主题对象写死在接口中, 3 在具体类中写入更好*/ 4 5 public void update(Subject sub, Object args);//接收到新消息,及时更新 6 }
现在,我们来以具体的生活例子来介绍如何实现观察者模式(Observer Pattern):
有一家气象站,气象站本身已经具有WeatherData对象(相当于报社功能,可以获得目前的温度、湿度、气压三种数据)。
我们需要编写一个应用,该应用有很多种显示模式(从温度、湿度、气压中任选一到三个组合就是一种模式)。
当WeatherData对象获得最新的测量数据时,我们的应用可以及时更新显示模式中的数据。
根据要求写程序:
主题接口:
1 public interface Subject {//主题接口,所有报社都要实现该接口 2 3 /*没有存储订阅者的列表,是因为我们不想在接口中写死存储方式, 4 让编程人员自己在实现接口的时候写入想要的存储方式(如:链表,数组,栈,队列等) 5 这样更合理。*/ 6 7 public void registerObserver(Observer o);//将订阅者登记在列表中 8 public void removeObserver(Observer o);//将订阅者从列表中移除 9 public void notifyObservers(Object arg);//有参通知方法,有新的消息即使通知列表中所有订阅者 10 public void notifyObservers();//无参通知方法 11 }
主题接口实体类:
1 import java.util.ArrayList; 2 3 public class WeatherData implements Subject{//气象站的实现类 4 private ArrayList observers;//观察者列表 5 private float temperature;//数据之一:温度 6 private float humidity;//数据之二:湿度 7 private float pressure;//数据之三:气压 8 private boolean status;//数据是否更新的标志 9 10 public WeatherData(){//初始化时,为观察者列表赋值 11 observers = new ArrayList(); 12 } 13 14 public float getTemperature(){//获取温度的方法 15 return this.temperature; 16 } 17 18 public float getHumidity(){//获取湿度的方法 19 return this.humidity; 20 } 21 22 public float getPressure(){//获取气压的方法 23 return this.pressure; 24 } 25 26 public void registerObserver(Observer o){//将观察者记录在列表中 27 observers.add(o); 28 } 29 30 public void removeObserver(Observer o){//将观察者从列表中删除 31 int i = observers.indexOf(o); 32 observers.remove(i); 33 } 34 35 public void notifyObservers(Object args){//有参通知观察者方法 36 if(status){//判断数据是否有更新 37 for(int i = 0; i < observers.size(); i++){ 38 Observer observer = (Observer) observers.get(i); 39 observer.update(this, args); 40 } 41 status = false;//消息发送成功后,将更新标志位重置 42 } 43 } 44 45 public void notifyObservers(){//无参通知观察者方法 46 notifyObservers(null); 47 } 48 49 public void setChange(){//数据是否更新的标志 50 status = true; 51 } 52 53 public void setMeasurements(float temperature, float humidity, float pressure){//数据更新方法 54 this.temperature = temperature; 55 this.humidity = humidity; 56 this.pressure = pressure; 57 measurementsChanged(); 58 } 59 60 public void measurementsChanged(){//数据改变后调用该方法 61 setChange(); 62 notifyObservers(); 63 } 64 65 66 }
观察者接口:
1 public interface Observer{//所有订阅者要实现的接口 2 /*在接口中,没有主题对象,也是因为不想将主题对象写死在接口中, 3 在具体类中写入更好*/ 4 5 public void update(Subject sub, Object args);//接收到新消息,及时更新 6 }
显示更新数据的接口:
1 public interface DisplayElement{//显示更新数据的接口 2 public void display(); 3 }
观察者接口实体类:
1 public class CurrentConditionsDisplay implements Observer, DisplayElement{//显示当前温度、湿度的类 2 private WeatherData weatherData;//定义订阅的主题对象 3 private float temperature;//温度数据 4 private float humidity;//湿度数据 5 6 public CurrentConditionsDisplay(WeatherData weatherData){ 7 this.weatherData = weatherData; 8 weatherData.registerObserver(this);//将该观察者对象登记在主题的观察者列表中 9 } 10 11 public void update(Subject sub, Object args){//数据更新方法 12 if(sub instanceof WeatherData){ 13 WeatherData weatherData = (WeatherData) sub; 14 this.temperature = weatherData.getTemperature(); 15 this.humidity = weatherData.getHumidity(); 16 display(); 17 } 18 } 19 20 public void display(){//显示数据的方法 21 System.out.println("Current conditions:" + temperature + "F degrees and " + humidity + "%humidity"); 22 } 23 }
测试类:
1 public class WeatherStation{ 2 public static void main(String[] agrs){ 3 WeatherData weatherData = new WeatherData(); 4 5 CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData); 6 7 weatherData.setMeasurements(80, 65, 30.4f);//更新数据 8 weatherData.setMeasurements(82, 70, 29.2f);//更新数据 9 weatherData.setMeasurements(79, 90, 29.2f);//更新数据 10 } 11 }
编译运行结果:
上面代码已经很完善了,而且不知道你有没有发现,其实每次WeatherData更新数据都是把所有数据都更新,但是我们的CurrentConditionsDispaly只获取温度和湿度两个数据,并且从来不获取多余的气压数据。这就是数据推送(Push)和数据抽取(Pull)的区别。
Push:不管你有没有订阅该数据,主题都会将该数据发送给订阅者,再由订阅者决定数据的取舍,没用的数据就不会记录在自己的数据中。
Pull:订阅者想要什么数据由订阅者说了算,主题只需提供获取数据的方法(get...())就好,而上面我们的气象站就是使用了数据抽取方式。
思想提炼:
1.多用组合,少用继承
2.为交互对象之间的松耦合设计而努力