观察者模式

25 阅读10分钟

回顾

上次我们讲到了策略模式,我们略微一回顾: 策略模式是指将项目中经常变化的部分,抽取定义为算法族接口,这样它们互不影响并可以相互替换,独立于使用算法的客户。 冰冻三尺,非一日之寒 仅仅知道OO当中的继承、抽象、多态这些概念,并不会让你立马成为一个好的设计者。要构造具有健壮、可扩展、弹性好的系统绝不是一日之功。事实证明只有不断艰苦实践才有可能。 我们接下来继续学习观察者模式。

观察者(Observer)模式

观察者模式是一种对象行为模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会接到通知,并更新自己。 我发现有些人急性子,可能是只想看下概念,所以我这次直接先把比较严格的定义放在前面。

它适合的场景

当一个对象改变时需要通知其它对象,而且不知道具体有多少对象。

优点:

1.将对象解耦,把观察者与被观察对象完全隔离。 2.可以快速接入多个观察者,而不影响其它观察者。

缺点

1.多个观察者会增加类的数量,造成维护上难度增加。 2.Java中消息通知一般是顺序执行的,其中一个观察者阻塞,会影响整体的效率。 3.另外还要特别注意,在被观察者之间循环的问题,要避免这一点。

气象监测应用

我们来看一个气象检测应用,此系统中的三个部分是:

1.气象站(获取实际气象数据的物理装置)。

2.WeatherData对象(追踪来自气象站的数据,并更新到布告板)。

3.三个布告板(显示目前天气状况给用户看的),未来可能会增加数量。

WeatherData对象跟物理气象站的联系程序已拥有,WeatherData对象拿到数据后会更新到三个布告板上,显示的内容分别是:目前状况、气象统计、天气预报。

我们看下WeatherData的结构(伪代码):

class WeatherData
{
	/*下面这三个方法各自返回最近的气象数据
	*分别为温度、湿度、气压,我们不在乎它怎么获得的
	*只需要知道它能从气象站获取更新
	*/
	getTemperature();//温度
	getHumidity();//湿度
	getPressure();//气压
	//我们要实现的
	measurementsChanged()
	{
		//一旦气象测量数据更新 这个方法就会被调用
	}
}

我们的工作就是实现measurementsChanged()方法,好让它更新目前的状况、气象统计、天气预报三块板子。

image.png

再总结下我们手中的条件:

1.WeatherData类具有getter方法,可以获取三个值:温度、湿度、气压。
2.当有新数据时,measurementsChanged()方法就会被调用(我们不在乎它是如何被调用,只需知道它被调用了)。
3.需要实现三个使用天气数据的布告板:目前状况、气象统计、天气预报。一旦有新数据必须马上更新。
4.此系统满足可拓展,可以让开发人员定制布告板,随意的添加、删除布告板。

先看一个错误🙅示范

我们在measurementsChanged()方法中添加我们的代码:

public class WeatherData
{
    //实例变量声明省略...
    
    public void measurementsChanged()
    {
        //先调用本类的三个getXXX方法取得最新数据,set方法默认已实现
        float temp = gettemperature();
        float humidity = getHumidity();
        float pressure = getPressure();
        //现在传入数据更新布告板 有三块布告板顺序更新
        currentConditionsDisplay.update(temp, humidity, pressure);
        statisticsDisplay.update(temp, humidity, pressure);
        forecastDisplay.update(temp, humidity, pressure);
    }
    //一些其它方法...


}

想想这有什么不对吗 我们上一篇中将策略模式时提到,要封装变化的部分,与不变者隔离,上面实现的代码很明显针对具体实现编程,导致以后如果删除、增加布告板都要改这部分代码,侵入了这个类。
从三个布告板的更新方法都是xxx.update()方法来看,好像可以统一接口?

在继续之前我们先来看下报纸和杂志的订阅:
1.报社的业务就是出版报纸
2.你向某家报社订阅报纸,只要出新报纸,就会给你发报。
3.当你取消订阅,不交money后,他不就不会再给你发了。
4.只要报社还在运营,就一直会有人订阅或者取消订阅。
那么:出版者 + 订阅者 = 观察者模式
出版者我们改为“主题”(Subject),订阅者我们叫观察者(Observe)。
我们再形象一点,课堂上,老师对学生的讲课:
我们把教师看作主题(Subject),当老师获取到教案上新的内容时,用嘴巴讲出来(更新数据),下面的学生可以看作一群观察者,如果有不听讲睡大觉的😴或请假了的,我们可以看作该观察者暂时取消订阅该主题😄。如图:

image.png

我们可以看出观察者模式主要就是一对多的关系。
定义观察者模式的类图:

classDiagram
class Observer{
<<interface>>
+所有潜在的观察者必须实现观察者接口
+update()
}
Observer <|-- Subject
Observer <|.. ConcreteObserver
class Subject
<<Interface>> Subject

Subject: +//每个主题可以有许多观察者
Subject : +注册观察者registerObserver()
Subject : +移除观察者removeObserver()
Subject: +通知所有观察者notifyObserver()
Subject <|.. ConcreteSubject
class ConcreteSubject{
+//具体主题总是实现主题接口
+注册观察者registerObserver()
+移除观察者removeObserver()
+取消订阅notifyObserver()
+获取状态getState()
+设置状态setState()
}
ConcreteSubject <|-- ConcreteObserver
class ConcreteObserver{
//其它观察者的具体类型
//必须注册主题以便接收通知
+update()
}

从图中我们可以看到,观察者是主题的依赖者,在数据变化时,通知观察者,这样比把观察者一股脑的去控制数据好多了。
松耦合的威力
当两个对象之间松耦合,它们仍然可以交互,但是不太清楚彼此的细节。观察者模式提供了这种设计让其松耦合
为什么呢
任何时候,我们可以增添新的观察者,且主题并不需要改变代码,设计变得更加具有复用与弹性。
设计原则4
为了交互对象之间的松耦合设计而努力!
(另外设计原则1到3且看上篇博文)
重新回到气象站应用
我们发现气象站应用很符合用观察者模式,weatherData正是主题,多个布告板就是观察者,布告板需要向weatherData注册,并且自身要有update()方法供weatherData调用更新数据。
由于每个布告板都有些许不同,所以update方法应该定义在布告板共同的接口中。
设计气象站
设计图:

image.png

根据我们的设计图,我们开始实现气象站

public interface Subject
{
    //注册与移除观察者都需要一个观察者作为变量Observer
    public void registerObserver(Observer o);
    public void removeObserver(Observer o);
    //刚主题改变时 这个方法会被调用以通知观察者
    public void notifyObserver(Observer o);
}

public interface Observer
{
    //所有观察者都需实现的update方法
    //这里是把温湿度等传进来,想想还有别的什么封装方法(对象)
    public void update(float temp, float humidity, float pressure);

}
public interface DisplaElement
{
    //当布告板需要显示时调用此方法
    public void display();
}

编写一下weatherData,省略了package、import等语句

public class WeatherData implements Subject
{
    //ArrayList用来保存观察者
    private ArrayList<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;
    //在构造方法中初始化了observers
    public WeatherData()
    {
        observers = new ArrayList();
    }
    public void registerObserver(Observer o)
    {
        //向集合中注册观察者
        observers.add(o);
    }
    public void removeObserver(Observer o)
    {
        //观察者取消订阅 就从集合中删除
        int index = observers.indexOf(o);
        if(index >= 0)
        {
            observers.remove(index);
        }
    }
    public void notifyObserver()
    {   //通知所有的观察者(或可以采用并行流)
        observers.forEach(item->{
            item.update(temperature, humidity, pressure);
        })
    }
    public void measurementsChanged()
    {
        //当气象站得到最新数据后会调用此方法,通知观察者
        notifyObserver();
    }
    public void setMeasurements(float temp, float humidity, float  pressure)
    {
        //使用此方法set观测数据 模拟从气象站获得数据后的自动调用measurementsChanged方法
        this.temperature = temp;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
    //一些其它方法......

}

weatherData已经写出来了,我们接着再把布告板程序写出来,一共三个布告板:目前情况、统计情况、天气预测。

public class CurrentConditionsDisplay implements Observer, DisplayElement
{
    private float temperature;
    private float humidity;
    private Subject weatherData;
    //构造器需要weatherData 订阅主题(注册)
    public CurrentConditionsDisplay(Subject weatherData )
    {
        this.weatherData = weatherData;
        //将当前对象注册到主题
        weatherData.registerObserver(this);
    }
    
    public void update(float temp, float humidity, float pressure)
    {
        this.temperature = temp;
        this.humidity = humidity;
        //显示
        display();
        
    }
    //显示结果
    public void display()
    {
        System.out.println("Current conditions: "+ temperature + "F degress and "+ humidity + "% humidity");
    }
}

建立一个测试程序:

public class WeatherStation
{
    public static void mian(String[]args]
    {
        //首先建立一个WeatherData
        WeatherData weatherData = new WeatherData();
        //先建立一个布告板 其余同理
        CurrentConditionsDisplay = currentDisplay = new CurrentConditionsDisplay(weatherData);
        weatherData.setMeasurements(80,65,30.4f);
    }

}

其它的布告板大家可以自行试一下。
其实Java中就内置了这种观察者模式,但跟我们设计的稍有差别,比如通知观察者时,可能有些信息是冗余的,在Java自带的类库中,可以选择通知那些内容给观察者。

image.png

你可能会注意到有一个setChange方法,是用来标记状态已经改变的事实,好让notify Observers方法知道它被调用时应该更新观察者,反之,如果在notifyObservers方法之前没有调用setChange,则不会通知观察者。
利用内置的这些支持,我们重做一下啊气象站的应用。

import java.util.Observer;
import java.util.Observabel;
public class weatherData extends Observable
{
    private float temperature;
    private float humidity;
    private float pressure;
    //构造方法不需要用来注册观察者了
    public WeatherData(){}
    public void measurementsChanged()
    {
        setChanged();//确认状态改变
        //没有传送数据对象,说明采用拉取数据方式
        notifyObservers();
    }
    public void setMeasurements(float temperature, float humidity, float pressure)
    {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
    //省略一些get、set方法
}

重做CurrentConditionsDisplay

public class CurrentConditionsDisplay implements Observer,DisplayElement
{
    private float temperature;
    private float humidity;
    private Observer observer;
    pubic CurrentCondition(Observer observer)
    {
        this.observer = observer;
        observer.addObserver(this);
    }
    public void update(Observable, obs, Object arg)
    {   //传入了Observable 与 数据对象Object
        if(obs instanceof WeatherData)
        {
           WeatherData weatherData = (WeatherData)obs;
           this.temperature = weatherData.getTemperature();
           this.humidity = weatherData.getHumidity();
           display();
        }
        else
        {
            System.out.println("需要传入合适的对象属于WeatherData子类");
        }
    }
    public void display()
    {
        System.out.println("Current conditions : "+ temperature + " F degress and "+ humidity + "% humidity");
    }
}

不知道大家有没有发现,两种类似设计输出效果顺序不一致。 我们 不能依赖观察者通知的顺序,这种设计违背了一个原则“面向接口编程,而不是实现。”
因为Observable是一个类,有些时候会陷入两难,因为Java无法通过类来多重继承。
在Java当中还有哪些地方可以用到观察者模式,我们再看一个例子,很简单,假如你有一个摁钮,上面写着“Should I do it?”(我该做吗)。
当你摁下这个摁钮,倾听者(观察者)必须回答此问题,我们实现了两个倾听者,一个是天使,一个是恶魔。程序的行为如下:

image.png 来看下代码。用到了Java中的GUI组件,在当今Java的使用环境,它的GUI组件基本没什么企业在用了,看官们大体知道一些即可。

//天使
class AngelListener implements ActionListener
{
    public void actionPerformed(ActionEvent event)
    {
        //不要这样做,你可能会后悔。
        System.out.println("Don't do it, you might regret it");
    }
}
//恶魔
class DevilListener implements ActionListener
{
    public void actionPerformed(ActionEvent event)
    {
            System.out.println("Come on, do it!");
    }

}
public class SwingObserverExample
{
    JFrame frame;
    
    public static void main(String[]args)
    {
       SwingObserverExample example = new SwingObserverExample();
       example.go();
    }
   
    public void go()
    {
        JButton button = new JButton("Should I do it?");
        //制造两个监听者
        button.addActionListener(new AngelListener());
        button.addActionListener(new DevilListener());
        setFrame(button);
    }
    public void setFrame(JButton button)
    {
        frame = new JFrame();
        //添加至frame容器
        frame.getContentPane().add(BorderLayout.CENTER, button);
        //点击X退出否则在后台运行
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        //窗体大小
        frame.setSize(800,500);
        //位置 中间
        frame.setLocationRelativeTo(null);
        //设置窗体可见,否则不显示
        frame.setVisible(true);
    }

}

Java中自带的类库使用观察者模式的有很多,如javaBeans,RMI,包括后来推出的MVC模式,也使用了观察者模式的思想。
记住咱们的新设计原则:
为交互对象之间的松耦合设计而努力。

再次重申一遍正式的定义:观察者模式 在对象之间定义一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象都会收到通知,并自动更新。

就到这里吧,下次再一起回顾一下装饰者模式。

本文部分图片与内容引自《head first 设计模式》 <未完待续>