美文网首页AndroidAndroid开发
观察者模式(从放弃到入门)

观察者模式(从放弃到入门)

作者: DakerYi | 来源:发表于2016-10-20 10:37 被阅读175次

    今天分享第二个模式,观察者模式。相信作Android开发或者Java开发的童鞋都听说过这个模式,而且有很多流行的框架都是使用了观察者模式,例如著名的RxJava。而且Java中直接就自带了观察者模式,可见它有多常用。

    然后文中的例子也是来自 Head First 设计模式,很棒的一本书,推荐大家看看。

    废话不多说了,直接上需求:

    需求1:错误示范

    系统中有3个东东:气象站(温度,湿地,气压等),WeatherData对象(获取各种消息的变化),广告板(显示变化)。直接上一张书上的图好了:

    1.png

    注意一定,公告板只是一种展示,可能还有其他功能的公告板,获取相同的数据,但是做不同的处理。图中是做目前的状态展示,还可能有气候统计天气预测等。
    意思就是说:我们可以将 WeatherData对象看作一个消息的来源,它的各项参数会不停的变化,他的每一次变化,都需要通知广告板做出相应的处理。

    那么最简单的设计就来了,既然后很多公告板,那么我的WeatherData对象中,存放他们的引用,然后当我的值变化的时候,依次调用公告板的change() 函数通知变化不久搞定了。代码如下:

    WeatherData .java

    public class WeatherData {
        
        CurrentConditionDisplay cDisplay;
        StaticsDisplay sDisplay;
        ForecastDisplay fDisplay;
        
        public WeatherData(){
            cDisplay = new CurrentConditionDisplay();
            sDisplay = new StaticsDisplay();
            fDisplay = new ForecastDisplay();
        }
        
        // 冲气象站获取到数据时执行更新
        public void measurementsChanged(){
            double temp = getTemperature();
            double humidity = getHumidity();
            double pressure = getPressure();
            
            cDisplay.update(temp, humidity, pressure);
            sDisplay.update(temp, humidity, pressure);
            fDisplay.update(temp, humidity, pressure);
        }
        
        public float getTemperature(){
            //TODO: 冲气象站获取温度
            return 1.0f;
        }
        
        public float getHumidity(){
            //TODO: 冲气象站获取湿度
            return 2.0f;
        }
        
        public float getPressure(){
            //TODO: 冲气象站获取气压
            return 3.0f;
        }
    }
    

    其他代码都不用看,我们关注measurementsChanged() 方法,通过 getXXX() 方法获取到变化之后,通知本类中的3个广告板对象调用 update(...) 方法,做出相应的更新。

    IUpdate.java

    public interface IUpdate {
        void update(double temp, double humidity, double presure);
    }
    

    ForecastDisplay.java

    public class ForecastDisplay implements IUpdate{
        public void update(double temp, double humidity, double presure) {
            System.out.println("I doing forecast work:"+temp+","+humidity+","+presure);
        }
    
    }
    

    StaticsDisplay.java

    public class StaticsDisplay implements IUpdate{
    
        public void update(double temp, double humidity, double presure) {
            System.out.println("I doing statics work:"+temp+","+humidity+","+presure);
        }
    }
    

    CurrentConditionDisplay .java

    public class CurrentConditionDisplay implements IUpdate{
    
        public void update(double temp, double humidity, double presure) {
            System.out.println("I doing current work:"+temp+","+humidity+","+presure);
        }
    }
    

    客户端调用:
    Main.java

    public class Main {
    
        public static void main(String[] args) {
            WeatherData wd = new WeatherData();
            wd.measurementsChanged();
        }
    }
    

    输出

    I doing current work:1.0,2.0,3.0
    I doing statics work:1.0,2.0,3.0
    I doing forecast work:1.0,2.0,3.0
    

    首先必须说,这样写问题真是太大了:

    1. 没有面相接口编程的思想(WeatherData类中直接使用CurrentConditionDisplay ,其实可以使用 IUpdate接口
    2. 如果还有新的广告板,需要修改WeatherData
    3. 如果通知的信号(温度,湿度,气压)再增加一个风力,那么IUpdate需要修改,所有的广告版都需要修改。

    需求2:观察者模式

    先上一张图:

    2.png

    现在我们要解决上面的3个问题:

    认识观察者模式

    在上面的示范中,我们其实可以看作,广告板(观察者Observer),时刻关注着 WeatherData (主题Subject) 的变化,如果有变化,则通知广告板更新。看看代码:

    首先提供主题和观察者两个接口:

    ISubject.java

    public interface ISubject {
        void registObserver(IObserver o);
        void removeObserver(IObserver o);
        void notifyObservers();
    }
    

    IObserver .java

    public interface IObserver {
        public void update(double temp, double humidity, double pressure);
    }
    

    WeatherData.java

    public class WeatherData implements ISubject {
        private Set<IObserver> observers = new HashSet<IObserver>();
    
        double temp;
        double humidity;
        double pressure;
    
        public void registObserver(IObserver o) {
            observers.add(o);
        }
    
        public void removeObserver(IObserver o) {
            observers.remove(o);
        }
    
        public void notifyObservers() {
            for (IObserver o : observers) {
                o.update(temp, humidity, pressure);
            }
        }
    
        public void mesurementsChanged(double temp, double humidity, double pressure) {
            this.temp = temp;
            this.humidity = humidity;
            this.pressure = pressure;
            
            notifyObservers();
        }
    }
    

    用一个 Set<IObserver> observers 来装所有关注这个Subject的观察者,在 mesurementsChanged() 中先更新自己的数据,然后通知所有观察者并做出反应。

    CurrentConditionDisplay.java

    public class CurrentConditionDisplay implements IObserver{
        
        private ISubject weatherData;
        
        public CurrentConditionDisplay(ISubject weatherData){
            this.weatherData = weatherData;
            weatherData.registObserver(this);
        }
    
        public void update(double temp, double humidity, double presure) {
            System.out.println("I doing current work:"+temp+","+humidity+","+presure);
        }
    }
    

    ForecastDisplay.java

    public class ForecastDisplay implements IObserver{
        
        private ISubject weatherData;
        
        public ForecastDisplay(ISubject weatherData){
            this.weatherData = weatherData;
            weatherData.registObserver(this);
        }
    
        public void update(double temp, double humidity, double presure) {
            System.out.println("I doing forecast work:"+temp+","+humidity+","+presure);
        }
    }
    

    StaticsDisplay.java

    public class StaticsDisplay implements IObserver{
        
        private ISubject weatherData;
        
        public StaticsDisplay(ISubject weatherData){
            this.weatherData = weatherData;
            weatherData.registObserver(this);
        }
    
        public void update(double temp, double humidity, double presure) {
            System.out.println("I doing statics work:"+temp+","+humidity+","+presure);
        }
    }
    

    3个广告板类,每个包含一个要观察的主题,在初始化时添加要观察的主题,并将自己添加到主题的观察者列表中。

    客户端调用:

    Main.java

    public class Main {
    
        public static void main(String[] args) {
            
            WeatherData weatherData = new WeatherData();
            
            IObserver cDisplay = new CurrentConditionDisplay(weatherData);
            IObserver sDisplay = new StaticsDisplay(weatherData);
            IObserver fDisplay = new ForecastDisplay(weatherData);
            
            weatherData.mesurementsChanged(1.0, 2.0, 3.0);
        }
    }
    

    改进的地方:

    1. 首先现在 WeatherData 可以添加任意多个观察者
    2. 主题和观察者解耦,类中保持的是接口对象。
    3. 观察主题的具体内容解耦了,WeatherData获取的信息,和Observer关心的数据分开。(简单的说就是,如果另外一种Observer之关心温度,也可是使用WeatherData,只是update方法不一样就可以了),但是也有不足指出,因为数据是由IObserver中的update方法决定的,这里感觉还可以改进。

    需求3:Java中自带的观察者模式

    Java 的 java.util包 中自带了 Observer接口和Observable类和我们前面的 Subject接口和Observer接口很像。

    我们来看看使用Java的观察者模式,重写上面的例子:

    WeatherData.java

    public class WeatherData extends Observable {
    
        private double temperature;
        private double humidity;
        private double pressure;
    
        public WeatherData() {
        }
    
        public void measurementsChanged() {
            setChanged();
            notifyObservers();
        }
    
        public void setMeasurements(double temperature, double humidity,
                double pressure) {
            this.temperature = temperature;
            this.humidity = humidity;
            this.pressure = pressure;
            
            measurementsChanged();
        }
    
        public double getTemperature() {
            return temperature;
        }
    
        public double getHumidity() {
            return humidity;
        }
    
        public double getPressure() {
            return pressure;
        }
    }
    

    主要修改了几个地方:

    1. 继承自Observable,等下讲完,我们来看看Observable里面写了什么。
    2. setMeasurements()还是更新数据,然后调用notifyObservers()通知所有注册的Observer修改数据。所以我们应该关注 Observerable类中的两个方法:update()notifyObservers()

    CurrentConditionDisplay.java

    public class CurrentConditionDisplay implements Observer {
    
        private Observable observable;
    
        public CurrentConditionDisplay(Observable observable) {
            this.observable = observable;
            observable.addObserver(this);
        }
    
        public void update(Observable o, Object arg) {
            if (o instanceof WeatherData) {
                WeatherData wd = (WeatherData) o;
                display(wd.getTemperature(), wd.getHumidity(), wd.getPressure());
            }
        }
    
        public void display(double temp, double humidity, double presure) {
            System.out.println("I doing current work:" + temp + "," + humidity
                    + "," + presure);
        }
    }
    

    修改的地方:

    1. 与之前一样,首先还是在构造方法中添加 主题对象,并注册自己。
    2. 然后看我们重写的方法 void update(Observable o, Object arg),先判断发送update消息的主题对象是不是WeatherData,然后从主题中获取并更新数据。

    思考

    至于调用和之前一样,看到这里有些童鞋可能看出一掉猫腻,我们在需求2中最后提到,我们关心的消息是由 IObserver中的update 方法中的具体参数决定的,不够完善。但是这里呢?完全跟参数无关,到底从 WeatherData 中获取什么数据,可以任意获取。

    我们还可以做一定的解耦,就是说 WeatherData 这个主题现在提供3种数据,但是我们完全可以将3中数据分开为3个主题,然后所有的观察者都关心自己的数据,而不需要通过WeatherData 同时获取3中数据。至于 update 中我们就需要判断 到底是 TemperatureData, HumidityData,还是 PressureData,将关心的消息也解耦了,不知道你理解没有。

    源码Observable , Observer 解析

    public class Observable {
        private boolean changed = false;
        private Vector<Observer> obs;
    
        public Observable() {
            obs = new Vector<>();
        }
    
        public synchronized void addObserver(Observer o) {
            if (o == null)
                throw new NullPointerException();
            if (!obs.contains(o)) {
                obs.addElement(o);
            }
        }
    
        public synchronized void deleteObserver(Observer o) {
            obs.removeElement(o);
        }
    
        
        public void notifyObservers() {
            notifyObservers(null);
        }
    
     
        public void notifyObservers(Object arg) {
           
            Object[] arrLocal;
    
            synchronized (this) {
                if (!changed)
                    return;
                arrLocal = obs.toArray();
                clearChanged();
            }
    
            for (int i = arrLocal.length-1; i>=0; i--)
                ((Observer)arrLocal[i]).update(this, arg);
        }
    
    
        public synchronized void deleteObservers() {
            obs.removeAllElements();
        }
    
    
        protected synchronized void setChanged() {
            changed = true;
        }
    
    
        protected synchronized void clearChanged() {
            changed = false;
        }
    
    
        public synchronized boolean hasChanged() {
            return changed;
        }
    
    
        public synchronized int countObservers() {
            return obs.size();
        }
    }
    

    为了节省篇幅,我狠心的把注释全删掉了,其实有很多注释的。

    如果前面的内容都理解的童鞋,看这个代码也应该很好理解。套路一样,但是有一些点我们应该注意:

    1. addObserver(), deleteObserver()添加和删除观察者对象
    2. 代码中用一个 changed 关键字来表示是否有改变,在发送改变前我们需要先使用setChanged()将其设置为true。 在 notifyObservers()之后自动调用 clearChanged() 恢复 changed 为false。所以不需要我们自己来重置。
    3. 我们应该注意所有的方法都是 **synchronized **的,而且存储观察者对象的集合类也是 Vector<Observer> obs,所以整个 **Observable **都是线程安全的。

    还需要看一下 Observer 接口吗?

    public interface Observer {
        void update(Observable o, Object arg);
    }
    

    确实精简,只有一个 update 方法。看看 Obsererable 对象中的 notifyObservers 方法 调用的 update:

     ((Observer)arrLocal[i]).update(this, arg);
    

    对没错,就是这样调用的。

    一些体会

    感觉设计模式确实很有意思,但是上大学的时候,我们居然没有看这门课,我的天!!这个例子完整的结合了书本,自己的思考,以及Java的源码,感觉写下来我自己也受益匪浅。

    相关文章

      网友评论

      • 阿卡莎喵喵:我刚开始看这本书,看了你的总结感觉理解的更多了一些

      本文标题:观察者模式(从放弃到入门)

      本文链接:https://www.haomeiwen.com/subject/xxsfuttx.html