2.观察者模式

1.什么是观察者模式?

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

2.结合需求案例来理解此模式

注意:完整项目代码地址在文章末尾

借用设计模式head first书本中的例子,我们根据一次简单的项目设计来体现观察模式的妙处。

2.1首次需求

一家著名的气象站想要委托我们建立一个应用,应用中有三种布告板,分别显示目前的状况的布告板,气象统计布告板以及简单的预报布告板。
他们提供一个WeatherData给我们,当WeatherData对象获得最新的测量数据时,三种布告板需要实时更新。
而且,他们希望我们的应用是可扩展的,他们希望公布一组API供其他开发人员使用,好让其他开发人员写出自己的布告板,并插入此应用中。

2.1.1 首次需求实现设计

首先,我们先看一下他们提供的WeatherData类源码:

package observerPattern.first;
import java.util.ArrayList;
import java.util.List;

public class WeatherData {
    //温度
    private float temperature;
    //湿度
    private float humidity;
    //压强
    private float pressure;

    public WeatherData() {
    }

    public void setMeasurements(float temperature,float humidity,float pressure){
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    public float getTemperature() {
        return temperature;
    }

    public void setTemperature(float temperature) {
        this.temperature = temperature;
        measurementsChanged();
    }

    public float getHumidity() {
        return humidity;
    }

    public void setHumidity(float humidity) {
        this.humidity = humidity;
        measurementsChanged();
    }

    public float getPressure() {
        return pressure;
    }

    public void setPressure(float pressure) {
        this.pressure = pressure;
        measurementsChanged();
    }
    /**
     * 此方法嵌入WeatherData中的set方法中,
     * 只要数据发生改变,就会调用此方法
     */
    public void measurementsChanged(){
       //对方希望我们的实现在这儿
    }
}

在进行设计之前,我们有必要自己模拟一个气象站,并将WeatherData与其绑定,否则后面的测试将令人头疼。

模拟气象站
建立一个类,其中组合了WeatherData对象,并写了一个start方法,其实现非常简单,就是建立三个线程分别随机的更新WeatherData中的属性达到模拟气象站的效果。

package observerPattern.first;

public class WeatherStation {

    private WeatherData weatherData;

    public WeatherStation(WeatherData weatherData){
        this.weatherData = weatherData;
    }

    public void start(){
        //快速创建线程的方式
        new Thread(() -> {
            while (true) {
                try {
                    Thread.currentThread().sleep((long) (10000 * Math.random()));
                    weatherData.setHumidity((float) (100 * Math.random()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        
        new Thread(()->{
            while (true){
                try {
                    Thread.currentThread().sleep((long)(10000*Math.random()));
                    weatherData.setTemperature((float) (40*Math.random()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }).start();

        new Thread(()->{
            while (true) {
                try {
                    Thread.currentThread().sleep((long) (10000 * Math.random()));
                    weatherData.setPressure((float) (100 * Math.random()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

下面对这个气象站测试一下:

package observerPattern.first;

public class WeatherTest {
    public static void main(String[] args) throws InterruptedException {
        //新建weatherData并与气象站绑定
        WeatherData weatherData = new WeatherData();
        new WeatherStation(weatherData).start();

        while (true){
            Thread.sleep(10000);
            System.out.println(weatherData.toString());
        }
    }
}

2.观察者模式_第1张图片
气象站模拟生效!!

准备工作完成后,我们现在分析下用户的需求,从上面的需求,我们可以提炼出三点:

1.需要建立三个布告板,分别显示目前的状况,气象统计以及简单的预报。

根据这个需求,我们可以简单的设计出三个类,分别代表三种不同的布告板:
目前状况布告板

package ObserverPatternStudy.first;

/**
 * 目前状况布告板
 */
public class CurrentConditionsDisplay{

    private float temp;
    private float humidity;
    private  float pressure;

    public void update(float temp,float humidity,float pressure){
        this.temp=temp;
        this.humidity = humidity;
        this.pressure = pressure;
    }

    public void display(){
        System.out.println("目前状况布告板:温度:"+temp+" 湿度:"+humidity);
    }
}

气象统计布告板

package ObserverPatternStudy.first;

/**
 * 天气统计布告板
 */
public class StatisticsDisplay{
    private float temp;
    private float humidity;
    private  float pressure;

    public void update(float temp,float humidity,float pressure){
        this.temp=temp;
        this.humidity = humidity;
        this.pressure = pressure;
    }

    public void display(){
        System.out.println("气象统计布告板:温度:"+temp+" 湿度:"+humidity+" 压强:"+pressure);
    }
}

天气预报布告板

package ObserverPatternStudy.first;
/**
 * 天气预报预告版
 */
public class ForecastDisplay implements Display{

    private float temp;
    private float humidity;
    private  float pressure;

    public void update(float temp,float humidity,float pressure){
        this.temp=temp;
        this.humidity = humidity;
        this.pressure = pressure;
    }
    public void display(){
        System.out.println("天气预报布告板:温度:"+temp+" 湿度:"+humidity+" 压强:"+pressure);
    }
}

2.并根据用户提供的WeatherData与三个布告板绑定,当WeatherData的数据发生变化时,布告板要实时更新。

根据这个需求,我们需要对WeatherData源码进行修改,首先为了能在WeatherData数据发送变化时实时更新布告板,上面写的三种布告板必须组合进WeatherData中:

package ObserverPatternStudy.first;

import java.util.ArrayList;
import java.util.List;

public class WeatherData {

    //温度
    private float temperature;
    //湿度
    private float humidity;
    //压强
    private float pressure;
    CurrentConditionsDisplay current;
    StatisticsDisplay statis;
    ForecastDisplay forecast;

    public WeatherData(CurrentConditionsDisplay current ,StatisticsDisplay statis,
    ForecastDisplay forecast) {
        this.current = current;
        this.statis = statis;
        this.forecast = forecast;
    }

    public void setMeasurements(float temperature,float humidity,float pressure){
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    public float getTemperature() {
        return temperature;
    }

    public void setTemperature(float temperature) {
        this.temperature = temperature;
        measurementsChanged();
    }

    public float getHumidity() {
        return humidity;
    }

    public void setHumidity(float humidity) {
        this.humidity = humidity;
        measurementsChanged();
    }

    public float getPressure() {
        return pressure;
    }

    public void setPressure(float pressure) {
        this.pressure = pressure;
        measurementsChanged();
    }

    /**
     * 此方法嵌入WeatherData中的set方法中,
     * 只要数据发生改变,就会调用此方法
     */
    public void measurementsChanged(){
      current.update(getTemperature(),getHumidity(),getPressure());
      statis.update(getTemperature(),getHumidity(),getPressure());
      forecast.update(getTemperature(),getHumidity(),getPressure());
      current.display();
      statis.display();
      forecast.display();
    }
}

ok,设计完毕,但是有没有感觉不对劲呢? 我们是不是在面向实现编程? 这个不好啊。
我们应该面向抽象编程!! 当我们意识到这一点,我们会发现第三点需求原来如此简单。

3.用户希望WeatherData与布告板的绑定是可扩展的,也就是当有新的布告板时,不需要修改WeatherData的源码也能进行绑定。

好了,在第二步的基础上,我们对它进行抽象化来实现可扩展,首先我们新建一个Display接口:

package ObserverPatternStudy.first;

public interface Display {
    void update(float temp,float humidity,float pressure);
    void display();
}

然后让前面三个布告板都实现它:

public class CurrentConditionsDisplay implements Display

public class ForecastDisplay implements Display

public class StatisticsDisplay implements Display

接着修改WeatherData为如下:

package ObserverPatternStudy.first;

import java.util.ArrayList;
import java.util.List;

public class WeatherData {

    //温度
    private float temperature;
    //湿度
    private float humidity;
    //压强
    private float pressure;

    List<Display> displayList;

    public WeatherData() {
        displayList = new ArrayList<>();
    }

    /**
     * 添加布告板对象给WeatherData
     * @param display
     */
    public int addDisplay(Display display){
        displayList.add(display);
        return displayList.size()-1;
    }

    /**
     * 删除指定订阅的布告板
     * @param display
     */
    public void removeDisplay(int display){
        displayList.remove(display);
    }

    public void setMeasurements(float temperature,float humidity,float pressure){
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    public float getTemperature() {
        return temperature;
    }

    public void setTemperature(float temperature) {
        this.temperature = temperature;
        measurementsChanged();
    }

    public float getHumidity() {
        return humidity;
    }

    public void setHumidity(float humidity) {
        this.humidity = humidity;
        measurementsChanged();
    }

    public float getPressure() {
        return pressure;
    }

    public void setPressure(float pressure) {
        this.pressure = pressure;
        measurementsChanged();
    }
    /**
     * 此方法嵌入WeatherData中的set方法中,
     * 只要数据发生改变,就会调用此方法
     */
    public void measurementsChanged(){
        for(Display display:displayList){
            display.update(getTemperature(),getHumidity(),getPressure());
            display.display();
        }
    }

    @Override
    public String toString() {
        return "WeatherData{" +
                "temperature=" + temperature +
                ", humidity=" + humidity +
                ", pressure=" + pressure +
                '}';
    }
}

最后进行测试:

package ObserverPatternStudy.first;

public class WeatherTest {
    public static void main(String[] args) throws InterruptedException {
        //新建weatherData并与气象站绑定
        WeatherData weatherData = new WeatherData();
        new WeatherStation(weatherData).start();

        //加入布告板
        int currentDis = weatherData.addDisplay(new CurrentConditionsDisplay());
        int foreDis= weatherData.addDisplay(new ForecastDisplay());
        int staticDis =  weatherData.addDisplay(new StatisticsDisplay());

    }
}

2.观察者模式_第2张图片

成功!! 我们也实现了可扩展!!
上面设计的UML如下:
2.观察者模式_第3张图片

做到这里我们也许能察觉到,我们已经通过这次需求在我们的设计中涉及到了一种模式:观察者模式。

我们再看下观察者模式定义:定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖都会收到通知并自动更新。

显然,WeatherData是一, 三个布告板是多,而且在WeatherData中的set方法中加入了通知执行方法,这也保证了三个布告板能及时收到更新。

但是,我们好像知道了一点,但又不完全知道,为什么呢? 因为没有规范设计。

2.1.2 用正了八经的观察者模式规范进行设计

2.观察者模式_第4张图片
上面的UML省略了DisplayElement(非必要元素)

首先,观察者模式分为两个角色,观察者和主题 ,分别对应两个接口:

主题

package ObserverPatternStudy.second;

public interface Subject {
    //注册方法
    void registerObserver(Observer observer);
    //注销方法
    void removeObserver(Observer observer);
    //通知方法
    void notifyObservers();
}

观察者

package ObserverPatternStudy.second;

public interface Observer {
    //更新方法
    void update(Object o);
}

所有的主题都应该实现Subject接口,所有的观察者都应该实现Observer接口。

我们很容易知道,WeatherData类对应主题, 布告板对应观察者。
于是我们可以编写如下代码:

WeatherData.java

package ObserverPatternStudy.second;

import java.util.ArrayList;
import java.util.List;

public class WeatherData implements Subject{

    //温度
    private float temperature;
    //湿度
    private float humidity;
    //压强
    private float pressure;

    List<Observer> observers;

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


    public void setMeasurements(float temperature,float humidity,float pressure){
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    public float getTemperature() {
        return temperature;
    }

    public void setTemperature(float temperature) {
        this.temperature = temperature;
        measurementsChanged();
    }

    public float getHumidity() {
        return humidity;
    }

    public void setHumidity(float humidity) {
        this.humidity = humidity;
        measurementsChanged();
    }

    public float getPressure() {
        return pressure;
    }

    public void setPressure(float pressure) {
        this.pressure = pressure;
        measurementsChanged();
    }

    /**
     * 此方法嵌入WeatherData中的set方法中,
     * 只要数据发生改变,就会调用此方法
     */
    public void measurementsChanged(){
        notifyObservers();
    }

    @Override
    public String toString() {
        return "WeatherData{" +
                "temperature=" + temperature +
                ", humidity=" + humidity +
                ", pressure=" + pressure +
                '}';
    }

    @Override
    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        int i = observers.indexOf(observer);
        if(i>=0) observers.remove(i);

    }

    @Override
    public void notifyObservers() {
        for(Observer observer:observers){
            observer.update(this);
        }
    }
}

在编写布告板之前,我们需要定义一个接口,用来给布告板的显示行为抽象化:很简单

package ObserverPatternStudy.second;

public interface DisplayElement {
    void display();
}

三种布告板如下:
这里只展示其中一个,其他的类似

package ObserverPatternStudy.second;
/**
 * 最新天气布告板
 */
public class CurrentConditionDisplay implements Observer, DisplayElement {

    //天气数据
    private Subject weatherData;
    private float temp;
    private float humidity;

    public CurrentConditionDisplay(Subject weatherData){
        this.weatherData = weatherData;
        this.weatherData.registerObserver(this);
    }

    @Override
    public void display() {
        System.out.println("当前温度:"+temp+"\n"
        +"当前湿度:"+humidity+"\n");
    }

    @Override
    public void update(Object object) {
        WeatherData weatherData = (WeatherData) object;
        temp = weatherData.getTemperature();
        humidity = weatherData.getHumidity();
        display();
    }
}

下面进行测试:

public class WeatherTest {
    public static void main(String[] args) {
        //新建weatherData并与气象站绑定
        Subject weatherData = new WeatherData();
        new WeatherStation((WeatherData) weatherData).start();
        CurrentConditionDisplay currentConditionDisplay = new CurrentConditionDisplay(weatherData);
    }}

2.观察者模式_第5张图片

成功!!

如果你也复现出来了,那么你可以将这种实现和第一种实现对比一下,看一下第二种实现的优点在哪里。

另外:JDK也提供了对观察者模式编程的支持,但是也具有一定的局限性,建议感兴趣的同学用JDK的观察者模式的api再复现一边上面的需求。

下面让我们呢再巩固下观察者模式的定义:定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖都会收到通知并自动更新

项目地址:
https://gitee.com/yan-jiadou/design-mode/blob/master/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/src/main/java/ObserverPattern/DisplayTest.java

你可能感兴趣的:(设计模式,观察者模式,java,开发语言)