0%

观察者模式

有一个设计模式帮助你的对象知悉现状,不会错过该对象感兴趣的事情,甚至在对象运行时可决定是否要继续被通知,观察者模式是JDK中使用最多的设计模式之一,非常有用。无论是在JDK还是Android开发当中,我们很容易发现观察者模式的运用之处,如我们经常遇到的点击事件,通过Button控件的诸如Listener 的方法,onClickListener就是观察/订阅到了按钮的点击事件,从而就可以执行对相应的逻辑,不同的动作会有不同的观察者,如单击、长按、连续两次点击等都有对应的Listener。

观察者模式概述

主题 + 订阅者 = 观察者设计模式

现在假设有一个气象站,气象站会根据天气的变化设置新的气象数据 (温度、湿度、气压) ,这些数据会展示在气象看板上面,一旦气象站发布了新的数据,则看板也必须立马更新展示的数据。

mark

在这个例子中,主题就是天气数据,订阅者就是显示装置。一旦有新的天气数据,显示装置立马展示新的数据。

观察者模式代码实现

mark

首先定义一个主题的接口,所有的主题都需要实现这个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 主题
*/
public interface Subject {
/**
* 注册观察者
* @param o 观察者对象
*/
void registerObserver(Observer o);

/**
* 移除观察者
* @param o 观察者对象
*/
void removeObserver(Observer o);

/**
* 通知观察者
*/
void notifyObserver();
}

天气数据就是一个主题,因此定义出天气主题的类:

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
46
47
48
49
50
51
52
53
import java.util.ArrayList;
import java.util.List;

public class WeatherData implements Subject {
// 存储了此主题的观察者
private List<Observer> observers;

// 温度
private float temp;
// 湿度
private float humidity;
// 气压
private float pressure;

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

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

@Override
public void removeObserver(Observer o) {
int index = observers.indexOf(o);
if(index >= 0) observers.remove(index);
}

@Override
public void notifyObserver() {
for(Observer o: observers){
o.update(temp, humidity, pressure);
}
}

/**
* 从气象站得到更新的观测值,通知观察者
*/
public void measurementsChanged(){
notifyObserver();
}

/**
* 气象站设置新的值
*/
public void setMeasurements(float temp, float humidity, float pressure){
this.temp = temp;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
}

上面的观察者还未定义呢,还是先定义一个统一的观察者数据更新方法的接口

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 观察者
*/
public interface Observer {
/**
* 更新展示板
* @param temp 温度
* @param humidity 湿度
* @param pressure 气压
*/
void update(float temp, float humidity, float pressure);
}

接下来定义一个展示数据的接口,作为显示装置都需要实现的接口:

1
2
3
public interface DisplayElement {
void display();
}

然后就是显示装置的具体实现,目前只实现一种那就是展示最新的气象数据:

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 CurrentConditionDisplay implements Observer, DisplayElement{
private float temp;
private float humidity;

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

public void stopDisplay(Subject weatherData){
weatherData.removeObserver(this);
}

@Override
public void display() {
System.out.println(temp + "℃ " + humidity + "%");
}

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

接下来测试一下写的观察者模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
// 把显示装置注册到WeatherData的观察者列表
CurrentConditionDisplay display = new CurrentConditionDisplay(weatherData);

weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(85, 70, 32.2f);
weatherData.setMeasurements(86, 72, 36.3f);
weatherData.setMeasurements(89, 76, 38.0f);

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

// 停止观察
display.stopDisplay(weatherData);
weatherData.setMeasurements(91, 77, 39.5f);
weatherData.setMeasurements(97, 79, 40.2f);
}
}

mark

JDK内置的观察者模式

观察者模式是对象的行为模式,在对象之间定义了一对多的依赖关系,就是多个观察者和一个被观察者之间的关系,当被观察者发生变化的时候,会通知所有的观察者对象,他们做出相对应的操作。 在观察者模式,我们又分为推模型和拉模型两种方式,上面演示的内容是推模型。

在JDK内已经有实现好的观察者模式API,java.util包内包含最基本的Observer接口和Observable类,这与我们的Subject接口和Observer接口很相似。实际上Observer接口与Observable类使用起来更方便,因为很多功能已经提前准备好了。下面演示一个通过JDK的API实现拉模型的例子。

1、如何把对象变成观察者

实现观察者接口java.util.Observer,然后调用任何Observable对象的addObserver()方法,不想当观察者的时候,调用deleteObserver()方法即可。

2、被观察者如何送出通知

首先扩展java.util.Observer接口产生被观察者类,然后调用两个方法:

  • 先调用setChanged()方法,标记状态已经改变的事实
  • 然后调用notifyObservers()方法中的一个,notifyObservers()或者notifyObservers(Object arg)

3、观察者如何接收通知

同以前的update()方法一样,只是方法参数略有不同:

1
update(Observable o, Object arg)

第一个参数Observable就是主题对象,好让观察者知道是哪个主题通知它的;第二个参数就是上面的例子中的参数,即数据对象。

如果使用推模式,则可以把数据当做数据对象传入notifyObservers(Object arg)中。否则观察者就必须从被观察者对象中拉取数据,我们把上面气象站的例子重做一次。

4、关于setChanged()

setChanged()方法用于标记状态已经改变的事实,好让notifyObservers()知道当它被调用时就应该更新观察者。如果调用notifyObservers()之前没有调用setChanged(),则观察者不会被通知,伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
setChaged(){
changed = true
}

notifyObservers(Object arg){
if(changed){
for obs in obsList {
call update(this, arg)
}
changed = false
}
}

notifyObservers(){
notifyObservers(null)
}

这样做的目的就是在更新观察者的时候能有更多的弹性,比如在你想在气象温度变化0.5度以上才通知观察者,就需要通过调用setChanged这样的方式进行数据的有效更新。

JDK内置观察者重做气象站

mark

WeatherData.java

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
import java.util.Observable;

public class WeatherData extends Observable {
// 温度
private float temp;
// 湿度
private float humidity;
// 气压
private float pressure;

public WeatherData() { }

public void measurementsChanged(){
setChanged();
notifyObservers();
}

/**
* 气象站设置新的值
*/
public void setMeasurements(float temp, float humidity, float pressure){
this.temp = temp;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}

// 观察者会利用这些Getter方法取得WeatherData的状态
public float getTemp() {
return temp;
}

public float getHumidity() {
return humidity;
}

public float getPressure() {
return pressure;
}
}

CurrentConditionDisplay.java (DisplayElement和前面的例子一样)

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
import java.util.Observable;
import java.util.Observer;

public class CurrentConditionDisplay implements Observer, DisplayElement {
private Observable observable;
private float temp;
private float humidity;

public CurrentConditionDisplay(Observable observable) {
this.observable = observable;
observable.addObserver(this);
}

@Override
public void display() {
System.out.println(temp + "℃ " + humidity + "%");
}

@Override
public void update(Observable o, Object arg) {
//System.out.println("被观察者:" + o.getClass().getName());
if(o instanceof WeatherData){
WeatherData weatherData = (WeatherData) o;
this.humidity = weatherData.getHumidity();
this.temp = weatherData.getTemp();
display();
}
}
}

主题对象在通知观察者的时候,只传递少量信息。如果观察者需要更具体的信息,由观察者主动到主题对象中获取,相当于是观察者从主题对象中拉数据。一般这种模型的实现中,会把主题对象自身通过update()方法传递给观察者,这样在观察者需要获取数据的时候,就可以通过这个引用来获取了。

观察者模式的优缺点

1、优点

首先是松耦合,当两个对象之间松耦合,它们依旧可以交互,但是不太清楚彼此的细节,观察者模式就提供了这样一种对象设计,让主题和观察者之间松耦合。

主题值需要知道观察者实现了某个接口,也就是Observer接口,不需要知道具体观察者实现类是什么,也不用关系观察者的实现细节。任何时间我们都可以动态的添加或者移除观察者、也包括替换新的观察者等操作,主题都不会受到影响。改变被观察者和观察者任意一方都不会影响另一方,这就是松耦合特点。

2、缺点

接下来说说缺点, 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间,也就是说同一个主题的观察者不能太多,太多了每次通知都是需要消耗时间的。

而且如果在被观察者之间有循环依赖的话,被观察者会触发它们之间进行循环调用,导致系统崩溃。在使用观察者模式是要特别注意这一点。

接下来讨论一个问题,但是却不是观察者模式的问题,而是JDK内置的观察者模式的问题。java.util.Observable是一个类而不是一个接口,如果要使用必须继承这个类,这其实限制了Observable的复用能力,而且通过源码可以看到setChanged()是受保护的权限,这意味着只能继承java.util.Observable,这违反了“多用组合、少用继承”的原则。平时使用的时候应该多注意这个问题,有必要的话最好自己实现一套观察者模式。

参考资料

《Head First 设计模式》

  • 本文作者: Tim
  • 本文链接: https://zouchanglin.cn/2586075670.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!