• Head First设计模式(阅读笔记)-02.观察者模式


    气象监测应用

    建立一个应用,利用WeatherData对象取得气象站的数据,并更新三个布告板:目前状况、气象统计和天气预报


    要求

    • WeatherData类具有getter方法获取温度、湿度和气压
    • 获取到新的数据时会调用measurementsChanged方法
    • 当有新数据时三个布告板需要更新
    • 系统需要具有扩展性,比如添加新的布告板
    简单实现

    未使用设计模式的代码如下,存在许多问题:

    public class WeatherData{
    	public void measurementsChanged(){
            // 获取最新数据
            float temp = getTemperature();
            float humidity = getHumidity();
            float pressure = getPressure();
            
            // 更新布告板
            currentConditionDisplay.update(temp, humidity, pressure);
            statisticDisplay.update(temp, humidity, pressure);
            forcecastDisplay.update(temp, humidity, pressure);
        }
        // 其他方法
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    • 针对具体实现编程,意味着在后续增加或删除布告板时需要修改程序
    • 没有封装改变的部分,比如更新布告板的实现代码

    观察者模式


    从订阅报纸入手

    先了解下报纸的订阅/取消订阅过程:

    • 当你向某家报社订阅报纸后,每次它们出版新报纸时就会送一份给你
    • 如果你不想在看报纸就可以取消订阅,报社也就不再送了

    在观察者模式中,报纸这样的出版者称为主题,我们这样的订阅者称为观察者:

    • 主题对象管理数据,当主题内的数据改变时就会通知观察者
    • 观察者订阅(注册)主题后,每当主题数据改变时能收到更新
    • 同时每个观察者也可以把自己从该主题的观察者集合中删除

    定义观察者模式

    观察者模式定义了对象之间一对多的依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新


    在这里插入图片描述

    观察者模式的优势

    让主题和观察者之间松耦合


    • 主题只知道观察者实现了Observer接口,它并不需要知道具体观察者是谁以及做了什么
    • 主题唯一依赖的东西是一个实现Observer接口的对象列表,所以任何时候都可以新增/删除观察者
    • 有新的观察者出现时,主题的代码也无需修改,新的类只需要实现观察者接口并且注册为观察者即可
    • 可以单独复用主题或观察者,因为它们非紧耦合

    重新设计应用


    实现气象站

    // 主题接口
    public interface Subject{
        public void registerObserver(Observer o);
        public void removeObserver(Observer o);
        public void notifyObservers();
    }
    
    // 观察者接口
    public interface Observer{
        // 这些数值发生变化,主题就会把新数值作为参数传递给观察者
        public void update(float temp, float humidity, float pressure);
    }
    
    // 展示内容接口
    public interface DisplayElment{
    	public void display();  // 布告板需要显示时就调用该方法
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    在WeatherData中实现主题接口

    // 具体的主题类WeatherData
    public class WeatherData implements Subject{
        private ArrayList observers;  // 记录观察者,在构造器中初始化
        private float temperature;
        private float humidity;
        private float pressure;
        
        public WeatherData(){
            observers = new ArrayList();
        }
        
        public void registerObserver(Observer o){
            observers.add(o);  // 观察者的注册即加入到观察者列表中即可
        }
        public void removeObserver(Observer o){
            int i = observers.indexOf(o);
            if(i >= 0){
                Observers.remove(i);  // 观察者的删除即从列表中移除即可
            }
        }
        public void notifyObservers(){
        	for(int i = 0; i < observers.size(); i++){
                Observer Observer = (Observer)observers.get(i);
                // 调用列表中每个Observer的update方法
                Observer.update(temperature, humidity, pressure);
            }   
        }   
        
        public void measurementsChanged(){  // 在最初的要求中提到了使用该方法进行更新
            notifyObservers();
        }
        public void setMeasurements(float temperature, float humidity, float pressure){
            this.temperature = temperature;
            this.humidity = humidity;
            this.pressure = pressure;
            measurementsChanged();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    建立布告板

    以目前状况布告板为例,其他两个类似


    public class CurrentConditionsDisplay implements Observer, DisplayElement{
        private float temperature;
        private float humidity;
        private Subject weatherData;
        
        public CurrentConditionsDisplay(Subject weatherData){
            // 目前状况布告板需要把自己注册给weatherData
            this.weatherData = weatherData;
            weatherData.registerObserver(this);
        }
        
        public void update(float temperature, float humidity, float pressure){
            // 目前状况布告板只需要temperature和humidity
            this.temperature = temperature;
            this.humidity = humidity;
            display();
        }
        public void display(){
            System.out.println("目前状况: " + temperature + "度 " + humidity);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    启动

    public class WeatherStation{
        public static void main(String[] args){
            WeatherData weatherData = new WeatherData();
            CurrentConditionsDisplay c = new CurrentConditionsDisplay(weatherData);
            weatherData.setMeasurements(10,10,10.3f);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    Java内置的观察者模式

    在上述操作中每次都是主题去送所有数据给注册好的观察者们,但是并不是每个观察者需要所有数据,为什么不能让观察者自己选择取需要的数据呢?Java API中内置的观察者模式就实现了两种方式传递数据


    在WeatherData中继承Observable

    import java.util.Observable;
    public class WeatherData extends Observable{
        private float temperature;
        private float humidity;
        private Subject weatherData;
        public WeatherData(){} // 因为这里实现拉取方式,WeatherData也就不需要记录观察者们
        
        // 使用推方式的时候需要使用setMeasurements和measurementsChanged
        public void measurementsChanged(){
            // 该方法用于标记状态状态已经改变,让notifyObservers方法知道该方法被调用时应该更新观察者
            // 这样就不会出现每次更新一点数据就通知观察者,可以设置每次更新到某个阈值再去调用setChanged方法
            setChanged();  
            notifyObservers();
        }
        
        public void setMeasurements(float temperature, float humidity, float pressure){
            this.temperature = temperature;
            this.humidity = humidity;
            this.pressure = pressure;
            measurementsChanged();
        }
        
        // getter用于拉方式的时候使用
        public float getTemperature(){
            return temperature;
        }
        public float getHumidity(){
            return humidity;
        }
        public float getPressure(){
            return pressure;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    重新建立布告板

    import java.util.Observable;
    import java.util.Observer;
    public class CurrentConditionsDisplay implements Observer, DisplayElement{
        Observable Observable;
        private float temperature;
        private float humidity;
        
        public CurrentConditionsDisplay(Observable observable){
            // 目前状况布告板需要把自己注册给weatherData
            this.observable = observable;
            observable.addObserver(this);
        }
        
        // 主题作为第一个变量,好让观察者知道是哪个主题通知它
        public void update(Observable obs, Object arg){
            if(obs instanceof WeatherData){
                WeatherData weatherData = (WeatherData)obs;
                this.temperature = weatherData.getTemperature();
                this.humidity = weatherData.getHumidity();
                display();
            }
        }
        public void display(){
            System.out.println("目前状况: " + temperature + "度 " + humidity);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    缺点

    java.util.ObservableJDK9之后已被标记为过时类


    • java.util.Observable是一个类并非接口,由于Java的单继承机制,所以限制了它的复用
    • setChanged方法被protected修饰,所以要创建Observable实例并组合到自己对象中就必须继承Observable(不是很明白),违法了多用组合,少用继承的设计原则

    参考

    Head First 设计模式-观察者模式

  • 相关阅读:
    本地部署_语音识别工具_Whisper
    Golang 区块链开发指南
    Win11如何更改默认下载路径?Win11更改默认下载路径的方法
    .NET Core 实现后台任务(定时任务)Longbow.Tasks 组件(三)
    docker 安装 jenkins
    Ubuntu screen命令,使终端在断开或关闭后依然存在
    03-关系和非关系型数据库对比
    355. 设计推特 -- 合并k个有序链表的应用
    西门子6ES72881ST200AA1
    【Educoder作业】问题求解——网页数据获取
  • 原文地址:https://blog.csdn.net/qq_41398418/article/details/127965479