设计模式——观察者模式

认识观察者模式

什么是观察者模式呢?我们先看看看报纸和杂志的订阅是怎么回事:

  1. 报社的业务就是出版报纸
  2. 向某家报社订阅报纸,只要报社有新的的报纸出版,就会给你送来,只要你是他们的客户,你就会一直收到新的报纸
  3. 当你不想再看报纸的时候,可以取消订阅,他们就不再送新的报纸过来。
  4. 只要报社还一直在运营,就会一直有人(或单位)向他们订阅报纸或取消订阅报纸

如果你了解报纸的订阅是怎么回事,其实就知道观察者模式是怎么回事,只是名称不太一样: 出版社改为”主题(Subject)”,订阅者改称为”观察者(Observer)”。

看下面的图例:
observer

案例

有一个气象监测的应用,由WeatherData对象负责追踪目前的天气状况(温度、湿度、气压)。建立一个应用,有三种布告板,分别显示目前的状况、气象统计、天气预测。当WeatherData对象获取最新的测量数据时,三种布告必须实时更新。

一个错误的示范

上面的要求,假设WeatherData中有以下的属性和方法:

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
public class WeatherData {
//实例变量声明
private float temperaure;
private float humidity;
private float pressure;
public void measuremntsChanged(){
float temp = getTemperaure();
float humidity = getHumidity();
float pressure = getPressure();
// 三种布告的实例对象,将从WeatherData中的数据传入布告中。
currentCOnditionDisplay.update(temp, humidity, pressure);
statiscsDisplay.update(temp, humidity, pressure);
forecastDispaly.update(temp, humidity, pressure);
}
// WeatherData的其他方法
public float getTemperaure() {
return temperaure;
}
public float getHumidity() {
return humidity;
}
public float getPressure() {
return pressure;
}
}

上面的方式实现从WeatherData中的测量变化后得到相对应的数据,再通过三种布告的实例,将数据传入进行。这种做法好像是可以达到WeatherData数据更新,三种布告数据也同步更新的功能,但是这样的实现有什么不对吗?请看下面的分析:

  • measuremntsChanged中的三个实例变量中,针对的是具体实现编程,会导致我们以后再增添或减少布告的时候必须修改程序了。
  • 三种实例都有一个公用的update, 像这样的,应该抽取出来,封装变化的部分。
  • 我们无法在运行时动态的增加或删除布告。

正确的实例:

定义观察者模式

定义观察者模式

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

观察者模式提供了一种对象设计,让主题和观察者之间松耦合。为什么呢?

因为关于观察者的一切,主题只知道观察者实现了某个接口(也就是Observer接口),主题不需要知道观察者的具体类是谁、做了什么或其他任何细节。

任何时候我们都可以增加新的观察者,因为主题唯一依赖的东西是一个实现Observer接口的对象列表,,所以我们可以随时增加观察者,事实上,在运行时我们可以用新的观察者取代现有的观察者,主题不会受到任何影响。同样的,也可以在任何的时候删除某些观察者。

有新类型的观察者出现的时,主题代码不需要修改。假如我们有个新的具体类需要当观察者,我们不需要为了兼容新类型而修改主题的代码,所有要做的就是在新的类里实现此观察者接口,然后注册为观察者即可,主题不在乎别的,它只是会发送通知给所有实现了观察者接口的对象。

松耦合的设计之所以能让我们建立有弹性的OOP系统,能够应对变化,是因为对象之间的相互依赖降到了最低。

设计气象站

设计气象站

实现

观察者接口:

1
2
3
4
5
package com.wcc.observer.inferface;
public interface Observer {
//观察者中的更新数据,当数据更新时将会传递给观察者
public void update(float temp, float humidity, float pressure);
}

主题接口:

1
2
3
4
5
6
7
8
package com.wcc.observer.inferface;
public interface Subject {
//这两个方法 需要一个观察者作为参数,用于注册和删除
public void registerObserver(Observer o);
public void removeObserver(Observer o);
// 当主题改变时,这个方法会被调用来通知所有的观察者
public void notifyObsevers();
}

布告接口:

1
2
3
4
5
package com.wcc.observer.inferface;
public interface DisplayElement {
//当布告要显示的时候,调用此方法
public void display();
}

WeatherData中实现主体接口

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
39
40
41
42
43
44
45
/**
* WeatherData 实现了Subject接口
*/
public class WeatherData implements Subject {
// 用数组来记录观察者
private ArrayList observers;
private float temperature;
private float humdity;
private float pressure;
public WeatherData(){
observers = new ArrayList();
}
//当注册观察者时,我们将观察者添加到数组中
public void registerObserver(Observer o) {
this.observers.add(o);
}
//当观察者想取消注册,我们将观察者从数组中删除
public void removeObserver(Observer o) {
int i = observers.indexOf(0);
if (i >= 0){
observers.remove(o);
}
}
//因为每一个观察者都实现了update方法,所以我们在这里可以通知所有的观察者
public void notifyObsevers() {
for (int i = 0; i < observers.size(); ++i){
Observer observer = (Observer) observers.get(i);
observer.update(temperature, humdity, pressure);
}
}
//当从气象站更新观测值时,我们通知观察者
public void measurementChanged(){
notifyObsevers();
}
//
public void setMeassurements(float temperature,
float humdity, float pressure){
this.temperature = temperature;
this.pressure = pressure;
this.humdity = humdity;
measurementChanged();
}
// WeatherData的其他方法
}

当前状况的布告板实现:

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
/**
* Created by WCC on 2016/12/26.
* 目前状况布告
实现了Observer接口,所以可以从WeatherData对象中获得改变
*/
public class CurrentConditionDisplay implements Observer, DisplayElement{
private float temperature;
private float humifity;
private Subject weatherData;
public CurrentConditionDisplay(Subject weatherData){
/*
这里为什么要保存Subject的引用呢?构造完似乎用不着了呀?
的确如此,但是我们可能以后想要取消注册,如果已经有了对
Subject的引用会比较方便
*/
this.weatherData = weatherData;
// 注册
weatherData.registerObserver(this);
}
public void update(float temperature,
float humidity, float pressure) {
this.temperature = temperature;
this.humifity = humidity;
//获取数据后就在布告板中显示。也就调用display方法
display();
}
public void display() {
//布告板中显示就:直接简单输出
System.out.println("Current conditions:"
+ temperature + "F degrees " +
"and " + humifity + "% humifity");
}
}

预测布告实现:

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
*/
public class ForecastDisplay implements Observer, DisplayElement {
private WeatherData weatherData;
private float temperature;
private float humidity;
private float pressure;
public ForecastDisplay(WeatherData weatherData){
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
public void update(float temp,
float humidity, float pressure) {
this.temperature = temp;
this.humidity = humidity;
this.pressure = pressure;
display();
}
public void display() {
temperature+= 1;
humidity+= 0.5;
pressure+= 0.7;
System.out.println("Forecast tempreature:"+
temperature+ " humkidity :"+ humidity
+" pressure" + pressure);
}
}

测试:

1
2
3
4
5
6
7
8
9
10
@Test
public void testCurrentConditionDispay(){
WeatherData weatherData = new WeatherData();
CurrentConditionDisplay conditionDisplay =
new CurrentConditionDisplay(weatherData);
ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
weatherData.setMeassurements(30, 56, 78);
System.out.println("-------------------------------------");
weatherData.setMeassurements(23,45,34);
}

上面测试代码输出如下图:
java内置类观察者模式输出

使用Java内置的观察者模式

Java API中有内置的观察者模式。java.util包中含有最基本的Observer接口与Observable类,这个和我们的Subject接口与Observable接口很相似。Observer接口与Observable类使用上更方便,因为许多功能都已经事先准备好了。如下图:

Java内置的观察者模式如何运作

Java内置观察者的运作模式是怎么样的?
1、如何把对象变成观察者?
和以前一样, 实现观察者接口(java.uitl.Observer),然后调用任何Observable对象的addObserver()方法,不想再当观察者时,调用deleteObserver()方法就可以。

2、可观察者(主题)如何送出通知

1、先调用setChanged()方法,标记状态已经改变的事实。
2、然后调用两种notifyObservers()方法中的一个notifyObservers()或notifyObservers(Object arg)。

3、观察者如何送出通知
同和我们写的一样,观察者实现了更新的方法,但是方法签名不太一样: update(Observale o, Object arg), 参数中,主题本身当做一个变量,好让观察者知道是哪一个主题通知他的。第二个参数是可观察者中传入notifyObservers(Object arg)的数据对象.
如果你想”推”(push)数据给观察者,你可以把数据当做数据对象传递给notifyObserver(arg) 方法,否则,观察者就必须从可观察者对象中”拉”(pull)数据。如何“”拉”数据?下面的实现就知道了。

java内置类观察者模式

利用Java内置类实现观察者模式案例

可观察者WeatherData:

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
public class WeatherData extends Observable {
private float temperature;
private float humidity;
private float pressure;
public WeatherData(){}
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();
}
public float getHumidity() {
return humidity;
}
public float getPressure() {
return pressure;
}
public float getTemperature() {
return temperature;
}
}

当前状况布告(观察者):

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
public class CurrentCoditionDispaly implements Observer, DisplayElement {
private float temperature;
private float humidity;
Observable observable;
public CurrentCoditionDispaly(Observable observable){
this.observable = observable;
observable.addObserver(this);
}
public void display() {
System.out.println("Current conditions:"
+ temperature + "F degrees and " + humidity + "% humidity");
}
/*
实现 java.util.Observer接口中的update方法。
*/
public void update(Observable obs, Object arg) {
if (obs instanceof WeatherData){
WeatherData weatherData = (WeatherData) obs;
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
display();
}
}
}

测试:

1
2
3
4
5
6
7
8
9
10
@Test
public void testJavaCurrentConditionDispay(){
com.wcc.observer.javacalss.WeatherData weatherData = new com.wcc.observer.javacalss.WeatherData();
CurrentCoditionDispaly coditionDispaly
= new CurrentCoditionDispaly(weatherData);
weatherData.setMeasurements(21,22,12);
System.out.println("-----------------------------");
weatherData.setMeasurements(33,33,33);
}

上面测试代码输出如下图:
java内置类观察者模式输出

总结

  1. 观察者设计模式——在对象之间定义一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象都会收到通知,并自动更新。
  2. 观察者模式定义了对象之间一对多的关系。
  3. 主题(也就是可观察者)用一个共同的接口来更新观察者。
  4. 主题(也就是可观察者)用一个共同的接口来更新观察者。
  5. 使用此模式时,你可以从被观察者处(push)。 或拉(pull)数据(然而。推的方式被认为更“正确”)。
  6. 有多个观察者时,不可以依赖特定的通知次序。
  7. ava有多个观察者模式的实现,包括了通用的java.util.Observable。
  8. 要注意java.util.Observale实现上所带来的问题:

    Observale 是一个类,这个可不是一件好的事情,因为它是一个类,你必须设计一个类继承它,如果某个类想同时具有Observable类和另外一个超类的行为。就会陷入两难,毕竟Java不支持多继承。这限制了Observable的复用潜力(增加复用潜力正是我们使用模式的最原始的动机)。
    再看看Observable API, 会发现setChanged()方法被保护起来了(被定义成protected). 那么这意味着:除非你继承自Observale,否则你是无法创建Observale 实例并组合到你自己的对象中来。这个设计规则违反了第二个设计原则:多用组合,少用继承。

  9. Swing大量使用观察者模式,许多GUI框架也是如此。
  10. 此模式被应用在许多地方,例如:JavaBean、RMI。
坚持原创技术分享,您的支持将鼓励我继续创作!