文章目录
- 一、前言
- 二、观察者模式
- 1、基本概念
- 2、应用举例
- 3、结构
- 3.1、Observer和ConcreteObserver
- 3.2、Subject和ConcreteSubject
- 4、代码展示
- 4.1、主题接口 WeatherData 和观察者接口 Observer
- 4.2、具体主题 WeatherStation,它实现了 WeatherData 接口:
- 4.3、具体观察者类
- 4.4、主程序中使用这些组件来模拟天气App的运行
- 三、总结
一、前言
观察者(Observer)模式的定义:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。
二、观察者模式
1、基本概念
观察者(Observer)模式中包含两种对象,分别是目标对象和观察者对象。在目标对象和观察者对象间存在着一种一对多的对应关系,当这个目标对象的状态发生变化时,所有依赖于它的观察者对象都会得到通知并执行它们各自特有的行为。
通俗地说,就好像这些观察者对象在时刻注视着目标对象(被观察)。无论何时该目标对象的状态发生变化,这些观察者对象都能够马上知道,并根据目标对象的新状态执行相应的任务。
观察者模式又叫发布-订阅(Publish-Subscribe)模式,其中的订阅表示这些观察者对象需要向目标对象进行注册,这样目标对象才知道有哪些对象在观察它。发布指的是当目标对象的状态改变时,它就向它所有的观察者对象发布状态更改的消息,以让这些观察者对象知晓。
一个目标对象的观察者对象数量是不固定的,可以随时增加新的观察者对象或取消已有的观察者对象。观察者模式的主要优点就是极大地降低了目标对象和观察者对象间的耦合,二者可以独自地改变和复用,让对系统增加功能或删除功能都很方便。
2、应用举例
我们举一个实际的例子来说明对观察者模式的运用,假设我们有一个天气App,它有很多个界面组件,这些组件的作用分别是:显示摄氏温度、显示华氏温度、显示气温感受(比如炎热、凉爽和寒冷等等)。当然该App应该还有另一个对象用于获取实时的天气数据,我们称它为天气对象。
这个天气对象和界面组件之间的依赖关系就可以用观察者模式实现,该天气对象就是目标对象,天气数据就是它的状态。这些界面组件就是观察者对象,当天气对象获取到新的天气数据时(此时它的状态改变了),它就通知所有依赖于它的界面组件,这些组件就更新它们显示的内容。
现在我们想扩展这个天气App的功能,让它可以向用户建议如何穿衣。因此我们需要一个新的界面组件,当温度高时显示穿薄点,当温度低时显示穿羽绒服。我们让这个新的界面组件注册成为天气对象(目标对象)的观察者,这样当天气数据改变时,它就会自动得到通知并显示新的穿衣建议。而天气对象和其余的界面组件都不会受到影响,也无需改变。
现在让我们再次修改这个天气App,这次是要删除显示华氏温度的功能。因为我们中国人更习惯摄氏温度,显示华氏温度不但多此一举,还可能让用户误将它当作摄氏温度。此时,我们只需向天气对象(目标对象)取消注册该界面组件再删除它就可以了,天气对象在数据更新时就不会再通知该界面组件了。同样的,该App的其余部分也不会受到本次修改的影响。
3、结构
Subject:目标类,它是一个抽象类,也是所有目标对象的父类。它用一个列表记录当前目标对象有哪些观察者对象,并提供增加、删除观察者对象和通知观察者对象的接口。
Observer:观察者类,它也是一个抽象类,是所有观察者对象的父类;它为所有的观察者对象都定义了一个名为update的方法(也叫成员函数)。当目标对象的状态改变时,它就是通过调用它的所有观察者对象的update方法来通知它们的。
ConcreteSubject:具体目标类,可以有多个不同的具体目标类,它们同时继承Subject类。一个目标对象就是某个具体目标类的对象,一个具体目标类负责定义它自身的事务逻辑,并在状态改变时通知它的所有观察者对象。
ConcreteObserver:具体观察者类,可以有多个不同的具体观察者类,它们同时继承Observer类。一个观察者对象就是某个具体观察者类的对象。每个具体观察者类都要重定义Observer类中定义的update方法,在该方法中实现它自己的任务逻辑,当它被通知的时候(目标对象调用它的update方法)就执行自己特有的任务。
3.1、Observer和ConcreteObserver
首先,目标对象需要知道有哪些观察者对象在观察它,这样它才知道状态改变时应该通知哪些观察者对象,并且它还要能随时添加和删除观察者对象。所以目标对象应该要有一个列表,来保存对它的所有观察者对象的引用。但在C++或Java这样的编程语言中,一个列表中的所有项都必须是同一类型的。这说明所有的观察者对象都必须是同一个类,但这样会限制程序的灵活性。因为不同的观察者对象要执行不同的任务,我们应该让它们属于不同的类。解决这一矛盾的途径是让所有的观察者类都有一个共同的父类(Observer),这些观察者类(ConcreteObserver)都是该父类的不同子类。因为所有子类的对象都可以被当作父类的对象,因此它们即可以保持不同又可以保存在同一列表中。
同时,这也解决了另一个问题,那就是目标对象是如何通知它的观察者对象的呢?当然这是通过调用它的观察者对象的一个方法(也叫成员函数)来实现的。目标对象只需要负责通知它的观察者对象它的状态改变了,而对该观察者对象如何处理新状态以及属于哪个具体观察者类都不需要了解。这就是说目标对象只能以同一种方式对待它的所有观察者对象,即这些观察者对象都要有一个相同的方法供目标对象调用,我们称该方法为update方法。
我们在抽象观察者类(Observer)中定义该方法,并在所有具体观察者类(ConcreteObserver)中重定义它。当目标对象的状态改变时,它在它的列表中每遍历到一个观察者对象,就调用该观察者对象的update方法。对目标对象来说,所有的观察者对象都是Observer类的,并且该类确实有一个update方法,所以能调用成功。又因为多态,实际执行的却是该观察者对象所属的具体观察者类中重定义的那个update方法。
要在观察者类中建立这样的继承关系的另一大原因是为了能方便地扩展功能。因为目标对象将所有的观察者对象都当作Observer类的对象来处理,所以要增加某种新的观察者对象时,我们只需创建一个新的类,让它继承Observer类并重定义update方法,在该update方法中实现它自己的任务逻辑就行了。这样所有由该新类创建的观察者对象都可以很容易地融入到当前的观察者模式中,程序的其余部分都不需要改变。
3.2、Subject和ConcreteSubject
那么为什么目标类(目标对象所属的类)也需要有一个抽象目标类(Subject)和多个具体目标类(ConcreteSubject)这样的继承结构呢?如果整个系统中只有一个目标对象,那么确实可以只用一个目标类实现观察者模式。我们之前说的都是多个观察者对象观察一个目标对象,但其实一个观察者对象也可以同时观察多个目标对象。
既然要同时观察多个目标对象,那么它们很可能有不同的状态以及不同的功能,即这些目标对象可能属于不同的类。也就是说一个观察者对象要被多个不同类的目标对象通知到,注意目标对象通知观察者对象是通过调用观察者对象的一个方法实现的。我们先考虑一个笨办法,在观察者类中为每一个目标类都提供一个版本的update方法。这样不仅麻烦而且代码难以维护,想象一下我们要增加一个新的目标类,那么就需要在所有要观察它的观察者类中都增加一个对应版本的update方法;当我们想删掉一个目标类的时候,又要在这些观察者类中删除那个对应版本的update方法。
一个通用的解决方式是让所有的目标类都调用同一个update方法,但是将自身的引用作为该方法的一个参数传递给观察者对象,这样观察者对象就知道是哪一个目标对象在通知它。一个方法(或成员函数)的参数的类型是确定的,也就是说所有的目标类都应该是同一个类。
这就又出现了观察者类中开始的矛盾局面,即所有的目标类既要是同一个类也要是不同的类。解决的方式也是一样的,即所有的目标类都有同一个父类(抽象目标类Subject),而不同的目标类(具体目标类ConcreteSubject)都是该父类的不同子类。
无论一个观察者对象可以同时观察多个目标对象还是只能观察一个目标对象,目标类的这种继承关系都有助于增加新的目标类(即扩展程序的功能)。当我们想增加新的具体目标类时,就创建一个新类,再让它继承Subject类并实现它自己特有的事务逻辑就可以了。这样由该新类所创建的目标对象就可以很轻松地融入到当前的观察者模式中,程序的其余部分都不会受到影响。
抽象目标类(Subject)和具体目标类(ConcreteSubject)以及抽象观察者类(Observer)和具体观察者类(ConcreteObserver)之间的继承关系降低了目标对象(它属于某个具体目标类)和观察者对象(它属于某个具体观察者类)之间的耦合度。
4、代码展示
4.1、主题接口 WeatherData 和观察者接口 Observer
import java.util.List;public interface WeatherData {void registerObserver(Observer observer);void removeObserver(Observer observer);void notifyObservers();void setMeasurements(float temperature, float humidity, float pressure);
}public interface Observer {void update(float temperature, float humidity, float pressure);
}
4.2、具体主题 WeatherStation,它实现了 WeatherData 接口:
import java.util.ArrayList;
import java.util.List;public class WeatherStation implements WeatherData {private List<Observer> observers;private float temperature;private float humidity;private float pressure;public WeatherStation() {observers = new ArrayList<>();}@Overridepublic void registerObserver(Observer observer) {observers.add(observer);}@Overridepublic void removeObserver(Observer observer) {observers.remove(observer);}@Overridepublic void notifyObservers() {for (Observer observer : observers) {observer.update(temperature, humidity, pressure);}}@Overridepublic void setMeasurements(float temperature, float humidity, float pressure) {this.temperature = temperature;this.humidity = humidity;this.pressure = pressure;measurementsChanged();}private void measurementsChanged() {notifyObservers();}
}
4.3、具体观察者类
public class CelsiusDisplay implements Observer {@Overridepublic void update(float temperature, float humidity, float pressure) {float celsius = (temperature - 32) * 5 / 9;System.out.println("摄氏温度:" + celsius + " ℃");}
}public class FahrenheitDisplay implements Observer {@Overridepublic void update(float temperature, float humidity, float pressure) {System.out.println("华氏温度:" + temperature + " ℉");}
}public class WeatherConditionDisplay implements Observer {@Overridepublic void update(float temperature, float humidity, float pressure) {String condition = "";if (temperature >= 30) {condition = "炎热";} else if (temperature >= 10) {condition = "凉爽";} else {condition = "寒冷";}System.out.println("气温感受:" + condition);}
}
4.4、主程序中使用这些组件来模拟天气App的运行
public class WeatherApp {public static void main(String[] args) {WeatherStation weatherStation = new WeatherStation();CelsiusDisplay celsiusDisplay = new CelsiusDisplay();FahrenheitDisplay fahrenheitDisplay = new FahrenheitDisplay();WeatherConditionDisplay conditionDisplay = new WeatherConditionDisplay();// 注册观察者weatherStation.registerObserver(celsiusDisplay);weatherStation.registerObserver(fahrenheitDisplay);weatherStation.registerObserver(conditionDisplay);// 模拟天气数据变化weatherStation.setMeasurements(86.0f, 70.0f, 101.3f);weatherStation.setMeasurements(50.0f, 65.0f, 100.0f);weatherStation.setMeasurements(5.0f, 45.0f, 99.5f);}
}
当运行 WeatherApp 后,你将看到不同的观察者显示了相应的温度和气温感受,这是因为它们订阅了主题 WeatherStation 并在主题状态变化时得到通知并更新自己的显示。
这个示例演示了如何使用观察者模式实现一个天气App,其中多个界面组件订阅了天气数据的变化。这种模式使得各个组件之间解耦,易于扩展和维护,同时保持了代码的可重用性。
三、总结
在某些平台上,可以向目标对象同时注册一个观察者对象和它的某个方法(成员函数)。这样当目标对象的状态改变时,它就调用这个观察者对象的这个注册的方法,而不一定要去调用它的update方法。这就允许这些观察者对象没有update方法,即它们不用都继承自同一个父类。当然一个目标对象可能要求它的所有观察者注册的回调方法都具有相同的函数原型,即这些方法要有相同数量和类型的参数以及相同类型的返回值。
观察者模式还有很多其它的实现方式,但这些方式都只是在一些细节上有所不同。只要理解了观察者模式的主要概念,就能够很容易理解这些细节差异。