【设计模式】4. 观察者模式

设计模式学习笔记


版权声明:本文由 Hov 所有,发布于 https://chenhy.com ,转载请注明出处。



1 观察者模式


1.1 简介

观察者模式是对象之间实现多对一依赖的一种设计方案,其可让多个观察者同时监听某一个主题(被观察者)。当主题的状态发生变化时,可通知所有观察者对象,使它们能自动更新状态。同时,观察者对象的增加或减少不会影响主题对象,也不会影响其它观察者。观察者模式对象之间的低耦合非常有利于项目功能扩展以及后期维护。

1.2 思路

观察者模式主要可以分为四种角色:

  • 主题:此角色可以是接口,也可以是抽象类或具体类。主题必须包含三种方法,分别是添加观察者对象、移除观察者对象、通知观察者对象。
  • 观察者:此角色一般是接口,里面一般只有一个 update 方法,在被观察者的状态发生变化时, update 方法就会被触发调用。
  • 具体的主题对象:此角色是为了便于扩展,可以定义被观察者的一些具体的业务逻辑。
  • 具体的观察者:此角色是观察者接口的具体实现,可以定义被观察者对象发生变化时,观察者所要处理的业务逻辑。

1.3 使用场景

观察者模式适用于在软件系统的某一个对象发生变化时,依赖它的其它对象也要做出相应改变的场景。

举个实际应用场景:

  1. 气象站发布气象数据,天气软件 A 、天气软件 B 都要获取气象站发布的气象数据。
  2. 那么,气象站就可以作为主题对象(被观察者),而天气软件 A 、天气软件 B 就可以作为观察者。
  3. 天气软件 A 、天气软件 B 都可以监听气象站的数据,气象站每次发布气象数据,就可以通知天气软件 A 、 B 更新的数据。
  4. 当然,如果天气软件 B 不想再获取气象站的数据,就退出观察者队列。而天气软件 A 依然在观察者队列,还可以继续获取气象数据,气象站和天气软件 A 都不受影响。

1.4 优缺点

优点:

  1. 观察者与被观察者之间耦合度低;
  2. 观察者模式可以实现广播;
  3. 观察者模式符合“开闭原则”的要求;
  4. 实现表示层与数据逻辑层的分离,定义了稳定的消息更新传递机制。

缺点:

  1. 观察者和被观察者之间若存在循环依赖,会使它们循环调用,可能导致系统崩溃;
  2. 若主题对象有很多直接或间接的观察者时,通知所有观察者将花费大量时间。


2 实例


假设我们想通过程序模拟气象站发布气象数据,天气软件 A 和天气软件 B 获取数据。

2.1 普通的解决方案

气象站将数据保存在局部变量,通知天气软件 A 获取,天气软件 A 将数据保存并显示出来。

(1)气象站

/* WeatherData.java */

public class WeatherData {          //气象站
    private float mTemperature;
    private float mPressure;
    private float mHumidity;
    private WeatherSoftwareA wfa;

    public WeatherData(WeatherSoftwareA wfa){
        this.wfa = wfa;             //天气软件A
    }

    public float getmTemperature() {
        return mTemperature;
    }

    public float getmPressure() {
        return mPressure;
    }

    public float getmHumidity() {
        return mHumidity;
    }

    public void setData(float mTemperature,float mPressure,float mHumidity){    //发布数据
        this.mTemperature = mTemperature;
        this.mPressure = mPressure;
        this.mHumidity = mHumidity;
        dataChange();               //更新数据
    }

    public void dataChange(){       //更新数据
        wfa.update(getmTemperature(),getmPressure(),getmHumidity());    //天气软件A获取数据
    }
}

(2)天气软件A

/* WeatherSoftwareA.java */

public class WeatherSoftwareA {     //天气软件A
    private float mTemperature;     //温度
    private float mPressure;        //气压
    private float mHumidity;        //湿度

    public void update(float mTemperature,float mPressure,float mHumidity){     //获取数据
        this.mTemperature = mTemperature;
        this.mPressure = mPressure;
        this.mHumidity = mHumidity;
        display();                  //显示数据
    }

    public void display(){          //显示数据
        System.out.println("***** Current Temperature: " + mTemperature + "*****");
        System.out.println("***** Current Pressure: " + mPressure + "*****");
        System.out.println("***** Current Humidity: " + mHumidity + "*****");
    }
}

(3)测试类

/* TestMain.java */

public class TestMain {                                 //测试类
    public static void main(String[] args) {
        WeatherSoftwareA wfa = new WeatherSoftwareA();  //天气软件A
        WeatherData weatherData = new WeatherData(wfa); //气象站
        weatherData.setData(27,150,60);                 //气象站发布数据
    }
}

程序运行结果:

  1. weatherData 通过调用 setData 方法更新气象数据;
  2. setData 方法内部通过 dataChange 方法通知天气软件 wfa 气象数据已经改变;
  3. wfa 调用 update 方法获取最新数据,并显示数据。
***** Current Temperature: 30.0 *****
***** Current Pressure: 150.0 *****
***** Current Humidity: 40.0 *****

上面的代码虽然实现了预设的功能,但你会发现气象站和天气软件 A 之间的耦合度很高,这样对代码维护、功能扩展来说是极其不便的。

设想一下,如果天气软件 B 也想获取气象数据,那么就必须要改动气象站(WeatherData)的代码,然后像天气软件A那样再增加天气软件B的代码逻辑。显然,WeatherData 的代码需要较大的改动,需要重新编译,这是很致命的,总不能中断服务吧。


2.2 基于观察者模式的解决方案

通过前面的方案,我们发现气象站和天气软件之间的耦合度很高。现在我们看看通过观察者模式是如何解决耦合问题的。

观察者模式通过抽象出两个接口 Subject 和 Observer ,气象站实现接口 Subject ,各天气软件实现接口 Observer 。当添加新的第三方,如天气软件 A ,气象站将天气软件 A 添加到观察者队列,天气软件 B 也是如此。实际上,气象站只需和观察者队列交互即可,而不关心观察者队列里面有谁。

因为具体的观察者对象对气象站来说是透明的,所以第三方的增加减少不影响气象站。

(1)接口 Subject (主题)

/* Subject.java */

public interface Subject {                      //被观察者
    public void registerObserver(Observer o);   //添加观察者
    public void removeObserver(Observer o);     //移除观察者
    public void notifyObserver();               //通知观察者
}

(2)接口 Observer (观察者)

/* Observer.java */

public interface Observer {                     //观察者
    public void update(float mTemperature,float mPressure,float mHumidity);     //更新
}

(3)WeatherStation 类(具体主题)实现 Subject 接口(主题)

/* WeatherStation.java */

import java.util.ArrayList;

public class WeatherStation implements Subject{     //气象站
    private float mTemperature;
    private  float mPressure;
    private float mHumidity;
    private ArrayList<Observer> observers;          //观察者队列

    public WeatherStation(){
        observers = new ArrayList<Observer>();
    }

    public float getmTemperature() {
        return mTemperature;
    }

    public float getmPressure() {
        return mPressure;
    }

    public float getmHumidity() {
        return mHumidity;
    }

    public void setData(float mTemperature,float mPressure,float mHumidity){    //发布数据
        this.mTemperature = mTemperature;
        this.mPressure = mPressure;
        this.mHumidity = mHumidity;
        dataChange();
    }

    public void dataChange(){                   //数据更新
        notifyObserver();                       
    }

    @Override
    public void registerObserver(Observer o) {  //添加观察者
        observers.add(o);
    }

    @Override
    public void removeObserver(Observer o) {    //移除观察者
        if(observers.contains(o)){
            observers.remove(o);
        }
    }

    @Override
    public void notifyObserver() {              //通知观察者
        for(int i=0;i<observers.size();i++){
            observers.get(i).update(getmTemperature(),getmPressure(),getmHumidity());
        }
    }
}

(4)天气软件 A 、天气软件 B(具体观察者)实现 Observer 接口(观察者)

/* WeatherSoftwareA.java */

public class WeatherSoftwareA implements Observer{      //天气软件A
    private float mTemperature;
    private  float mPressure;
    private float mHumidity;

    @Override
    public void update(float mTemperature, float mPressure, float mHumidity) {  //更新数据
        this.mTemperature = mTemperature;
        this.mPressure = mPressure;
        this.mHumidity = mHumidity;
        display();
    }

    public void display(){              //显示数据
        System.out.println("***** Current Temperature: " + mTemperature + " *****");
        System.out.println("***** Current Pressure: " + mPressure + " *****");
        System.out.println("***** Current Humidity: " + mHumidity + " *****");
    }
}
/* WeatherSoftwareB.java */

public class WeatherSoftwareB implements Observer{      //天气软件B
    private float mTemperature;
    private  float mPressure;
    private float mHumidity;

    @Override
    public void update(float mTemperature, float mPressure, float mHumidity) {  //更新数据
        this.mTemperature = mTemperature;
        this.mPressure = mPressure;
        this.mHumidity = mHumidity;
        display();
    }

    public void display(){              //显示数据
        System.out.println("***** Forcast Temperature: " + (mTemperature+Math.random()) + " *****");
        System.out.println("***** Forcast Pressure: " + (mPressure+Math.random()) + " *****");
        System.out.println("***** Forcast Humidity: " + (mHumidity+Math.random()) + " *****");
    }
}

(4)测试类

/* TestMain.java */

public class TestMain {
    public static void main(String[] args) {
        WeatherStation weatherStation = new WeatherStation();   //被观察者:气象站
        WeatherSoftwareA wfa = new WeatherSoftwareA();          //观察者:天气软件A
        WeatherSoftwareB wfb = new WeatherSoftwareB();          //观察者:天气软件B

        weatherStation.registerObserver(wfa);       //气象站将天气软件A加入观察者队列
        weatherStation.registerObserver(wfb);       //气象站将天气软件B加入观察者队列
        weatherStation.setData(30,150,40);          //气象站发布数据,A、B都监听到数据

        System.out.println("*******************");

        weatherStation.removeObserver(wfa);         //气象站将天气软件A移出观察者队列
        weatherStation.setData(20,120,60);          //气象站发布数据,只有B监听到数据
    }
}

程序运行结果:

  1. 实例化气象站 weatherStation 、天气软件 A (wfa)、天气软件 B (wfb);
  2. 将 wfa 和 wfb 加入观察者队列,也就是在 weatherStation 中注册 wfa 和 wfb ;
  3. 气象站发布数据, wfa 和 wfb 获取到最新数据并显示;
  4. 将 wfa 移除观察者队列,也就是在 weatherStation 中移除 wfa ;
  5. 气象站发布数据,只有 wfb 获取到最新数据并显示,因为 wfa 已不在观察者队列,所以无法接收到通知。
***** Current Temperature: 30.0 *****
***** Current Pressure: 150.0 *****
***** Current Humidity: 40.0 *****
***** Forcast Temperature: 30.208610294628656 *****
***** Forcast Pressure: 150.57421015488487 *****
***** Forcast Humidity: 40.57110950563163 *****
*******************
***** Forcast Temperature: 20.14950930027541 *****
***** Forcast Pressure: 120.40962863921413 *****
***** Forcast Humidity: 60.601112555287365 *****

观察以上代码,你会发现在气象站的代码里面基本不包含和天气软件 A 、天气软件 B 有关的代码,这就实现了气象站与具体天气软件之间解耦。

如果我们需要增加天气软件 C ,只需动态地将天气软件 C 加入观察者队列即可,无需改动气象站的代码,可以较好地实现动态扩展。


2.3 Java内置的观察者

前面我们通过自定义的方式实现了观察者模式,但其实我们也可以使用 Java 内置的类和接口来实现观察者模式。通过 Java 内置的 Observable 抽象类Observer 接口,气象站继承抽象类 Observable ,里面已经包含添加、移除、通知观察者三个方法,不需我们自己写这三个方法,各天气软件实现接口 Observer 。

(1)气象站

/* WeatherStation.java */

//记得导入对应的包
import java.util.Observable;    

public class WeatherStation extends Observable{         //气象站
    private float mTemperature;
    private  float mPressure;
    private float mHumidity;

    public float getmTemperature() {
        return mTemperature;
    }

    public float getmPressure() {
        return mPressure;
    }

    public float getmHumidity() {
        return mHumidity;
    }

    public void setData(float mTemperature,float mPressure,float mHumidity){    //发布数据
        this.mTemperature = mTemperature;
        this.mPressure = mPressure;
        this.mHumidity = mHumidity;
        dataChange();
    }

    public void dataChange(){           //更新数据
        //这步很重要,将设置bool变量表示数据是否有更新,notifyObservers方法会先判断 bool 的值是否为真再执行相应操作
        this.setChanged();  
        //通知所有观察者   
        this.notifyObservers(new Data(getmTemperature(),getmPressure(),getmHumidity()));    
    }

    public class Data{                  //封装数据
        public float mTemperature;
        public float mPressure;
        public float mHumidity;
        public Data(float mTemperature,float mPressure,float mHumidity){
            this.mTemperature = mTemperature;
            this.mPressure = mPressure;
            this.mHumidity = mHumidity;
        }
    }
}

(2)天气软件 A 、天气软件 B

/* WeatherSoftwareA.java */

//记得导入对应的包
import java.util.Observable;
import java.util.Observer;

public class WeatherSoftwareA implements Observer {         //天气软件A
    private float mTemperature;
    private  float mPressure;
    private float mHumidity;

    @Override
    public void update(Observable arg0, Object arg1) {      //获取数据
        this.mTemperature = ((WeatherStation.Data)(arg1)).mTemperature;
        this.mPressure = ((WeatherStation.Data)(arg1)).mPressure;
        this.mHumidity = ((WeatherStation.Data)(arg1)).mHumidity;
        display();
    }

    public void display(){                                  //显示数据
        System.out.println("***** Current Temperature: " + mTemperature + "*****");
        System.out.println("***** Current Pressure: " + mPressure + "*****");
        System.out.println("***** Current Humidity: " + mHumidity + "*****");
    }
}
/* WeatherSoftwareB.java */

//记得导入对应的包
import java.util.Observable;
import java.util.Observer;

public class WeatherSoftwareB implements Observer {         //天气软件B
    private float mTemperature;
    private  float mPressure;
    private float mHumidity;

    @Override
    public void update(Observable arg0, Object arg1) {      //获取数据
        this.mTemperature = ((WeatherStation.Data)(arg1)).mTemperature;
        this.mPressure = ((WeatherStation.Data)(arg1)).mPressure;
        this.mHumidity = ((WeatherStation.Data)(arg1)).mHumidity;
        display();
    }

    public void display(){                                  //显示数据
        System.out.println("***** Forcast Temperature: " + (mTemperature+Math.random()) + "*****");
        System.out.println("***** Forcast Pressure: " + (mPressure+Math.random()) + "*****");
        System.out.println("***** Forcast Humidity: " + (mHumidity+Math.random()) + "*****");
    }
}

(3)测试类

/* TestMain.java */

public class TestMain {
    public static void main(String[] args) {
        WeatherStation weatherStation = new WeatherStation();   //被观察者:气象站
        WeatherSoftwareA wfa = new WeatherSoftwareA();          //观察者:天气软件A
        WeatherSoftwareB wfb = new WeatherSoftwareB();          //观察者:天气软件B

        weatherStation.addObserver(wfa);        //气象站将天气软件A加入观察者队列
        weatherStation.addObserver(wfb);        //气象站将天气软件B加入观察者队列
        weatherStation.setData(30,150,40);      //气象站发布数据,A、B都可监听到数据

        System.out.println("*******************");

        weatherStation.deleteObserver(wfa);     //气象站将天气软件A移出观察者队列
        weatherStation.setData(20,120,60);      //气象站发布数据,只有B可监听到数据
    }
}

程序运行结果:

***** Forcast Temperature: 30.208610294628656 *****
***** Forcast Pressure: 150.57421015488487 *****
***** Forcast Humidity: 40.57110950563163 *****
***** Current Temperature: 30.0 *****
***** Current Pressure: 150.0 *****
***** Current Humidity: 40.0 *****
*******************
***** Forcast Temperature: 20.14950930027541 *****
***** Forcast Pressure: 120.40962863921413 *****
***** Forcast Humidity: 60.601112555287365 *****

这里实现的功能和 2.2 节的完全一致,只不过在这里我们借助了 Java 内置的抽象类、接口来实现观察者模式。

值得注意的是,这里的观察者队列输出的顺序和我们前面自定义方式的输出是相反的,变成先进后出。


3 总结


  1. 观察者模式可以较好地实现对象之间的解耦,有利于功能扩展和后期维护;
  2. 主题对象必须包含添加观察者、移除观察者、通知观察者三种方法;
  3. Java 有内置的观察者接口,但其 Observable 是抽象类,这带来了一些弊端。


 
comments powered by Disqus