设计模式——观察者模式(海姆达尔与仙宫人民)

本文主要对观察者模式进行讲解,并使用观察者模式来模拟海姆达尔在发现敌人来袭后通知雷神托尔和洛基的过程。

image-20181209130344471

一、概念

定义

观察者模式也叫作发布-订阅模式,也就是事件监听机制。观察者模式定义了对象之间的依赖关系,让多个观察者对象同时监听某一个主题对象,这个主题对象在状态上发生变化时,会通知所有观察者对象,使他们能够自动更新自己,并采取相应活动

主要结构

  1. 抽象主题角色

被观察者,把所有观察者对象引用保存在一个集合里,对外提供增加删除的接口

  1. 具体主题角色

将有关状态存入具体观察者对象,在内部状态发生改变时给所有注册过的观察者发送信息

  1. 抽象观察者角色

为所有具体观察者定义更新接口

  1. 具体观察者角色

储存与主题状态自恰的状态,实现更新接口,与主题状态协调

UML类图

image-20181209111356949

特点:松耦合

观察者让主题对象好观察者之间松耦合,他们可以相互交互,但不清楚彼此的细节。
  • 观察者只知道观察者实现了某个接口,并不需要知道观察者具体类,实现了哪些细节。

  • 任何时候可以增加观察者。主题唯一依赖的东西是储存观察者实现的列表。所以在运行时添加新的观察者也不会对主题造成影响。

  • 有新的观察者加入时,主题代码不需要改变。主题在意的只是发送通知给观察者。

  • 可以独立的使用主题和观察者,因为他们之间是松耦合的。

  • 改变主题或观察者其一并不需要修改另一方,只要遵守接口就可以。

二、海达姆斯与仙宫人民

海姆达尔(Heimdall)是希芙的哥哥。他是能眼视万物和耳听一切的仙宫守护哨兵,他站在彩虹桥比弗罗斯特上,并注意观察任何对仙宫的袭击。他作为仙宫的守卫站立着,保卫这个城市的大门使任何闯入者远离,是奥丁最为信任的仆人之一。

对于仙宫的住民来说Heimdall是他们的可观察者,当海姆达尔观察到危机,会向所有他需要通知的人发送通知。现在我们用观察者模式来模拟海姆达斯发现危机通知仙宫住民这种情况。

下面是一个UML图,实现了海姆达斯与仙宫人民的解耦,去除强耦合关系,同时哨兵接口可以给仙宫所有哨兵使用,观察者们根据需要实现Action接口

image-20181209115326591

定义哨兵接口

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
/**
* 阿斯加德所有的哨兵都有的行为
* @author cdream
* @date 2018/12/9
*/
public interface Sentinel {
/**
* 注册需要通知的阿斯加德人
* @param asgardManObserver
*/
void registerObserver(AsgardManObserver asgardManObserver);

/**
* 移除需要通知的阿斯加德人
* @param asgardManObserver
*/
void removeObserver(AsgardManObserver asgardManObserver);

/**
* 通知所有asgard人
*/
void notifyObservers();

/**
* 观察到了信息
* @param message
*/
void setMessage(String message);
}

海姆达斯实现哨兵接口

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
/**
* 海姆达尔类,他向仙宫内的人民来传递信息,是仙宫内人民的看观察者
* @author cdream
* @date 2018/12/9
*/
public class Heimdall implements Sentinel {
// 维系所有需要通知的人,这是观察者和被观察者唯一关联的地方
private ArrayList<AsgardManObserver> lists=new ArrayList<>();
private String message;
@Override
public void registerObserver(AsgardManObserver asgardManObserver) {
lists.add(asgardManObserver);
}

@Override
public void removeObserver(AsgardManObserver asgardManObserver) {
lists.remove(asgardManObserver);
}

@Override
public void notifyObservers() {
lists.forEach(asgardMan -> asgardMan.update(message));
}

@Override
public void setMessage(String message) {
this.message = message;
System.out.println("Heimdall:"+message);
notifyObservers();
}
}

AsgardManObserver接口,所有的想接收信息的人都要实现

1
2
3
4
5
6
7
public interface AsgardManObserver {
/**
* 接收来自海姆达尔的信息,并更新状态
* @param message
*/
void update(String message);
}

Action接口,需要采取行动的人实现

1
2
3
4
public interface Action {
// 采取行动
void takeAction();
}

两个需要用到的常量

1
2
3
4
// 灭霸
public static final String THANOS="Thanos";
// 冰霜巨人
public static final String FROST_GIANTS="Frost Giants";

观察者1:雷神托尔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Thor implements AsgardManObserver,Action {
private String message;

@Override
public void takeAction() {
// 如果是灭霸
if (message!=null && message.contains(Const.THANOS)){
System.out.println("Thor:准备对抗灭霸");
// 如果是冰霜巨人
}else if(message !=null && message.contains(Const.FROST_GIANTS)){
System.out.println("Thor:准备对抗冰霜巨人");
}else{
System.out.println("Thor:我没听懂你说什么");
}
}
// 一旦海姆达尔发送敌人袭击消息,托尔立即采取行动
@Override
public void update(String message) {
this.message = message;
takeAction();
}
}

观察者2:洛基

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Lokey implements AsgardManObserver,Action {
private String message;
@Override
public void takeAction() {
// 如果是灭霸
if (message!=null && message.contains(Const.THANOS)){
System.out.println("Lokey:准备逃走");
// 如果是爸爸
}else if(message !=null && message.contains(Const.FROST_GIANTS)){
System.out.println("Lokey:准备反叛");
}else{
System.out.println("Lokey:我继续做我的闲鱼~");
}
}

@Override
public void update(String message) {
this.message = message;
takeAction();
}
}

观察者3:咸鱼

1
2
3
4
5
6
7
8
public class SaltedFish implements AsgardManObserver {
private String message;
@Override
public void update(String message) {
this.message = message;
System.out.println("闲鱼:继续做咸鱼");
}
}

开始模拟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
public static void main(String[] args) {
// 仙宫建立,阿斯加德人诞生
Sentinel heimdall = new Heimdall();
AsgardManObserver thor = new Thor();
AsgardManObserver lokey = new Lokey();
AsgardManObserver saltedFish = new SaltedFish();
// 三个人都去海尔达姆那里去注册
heimdall.registerObserver(thor);
heimdall.registerObserver(lokey);
heimdall.registerObserver(saltedFish);
// 冰霜巨人来袭
heimdall.setMessage(Const.FROST_GIANTS + "来袭");
// 洛基叛变,海达姆斯不再通知洛基
heimdall.removeObserver(lokey);
System.out.println("-------------");
//灭霸来袭
heimdall.setMessage(Const.THANOS + "来袭");

}
}

结果:

1
2
3
4
5
6
7
8
Heimdall:Frost Giants来袭
Thor:准备对抗冰霜巨人
Lokey:准备反叛
闲鱼:继续做咸鱼
-------------
Heimdall:Thanos来袭
Thor:准备对抗灭霸
闲鱼:继续做咸鱼

对抗冰霜巨人一战,洛基叛变,海姆达尔不在向其发送通知~

这是一个典型的观察者模式的推模式,海姆达尔一旦得到敌人来袭的消息就会通知他所维系的观察者,海尔达姆与仙宫住民是松耦合的,都可以独立行动,又不用关注各自的细节(所以他也不知道洛基得到消息后会做什么:)),新来了观察者直接维系到list里扩展性强。

拉模式:将整个被观察者对象引用送给观察者,由观察者获取需要的信息。如下,注意notifyObservers方法的修改

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
/**
* 海姆达尔类,他向仙宫内的人民来传递信息,是仙宫内人民的看观察者
* @author cdream
* @date 2018/12/9
*/
public class Heimdall implements Sentinel {
// 维系所有需要通知的人,这是观察者和被观察者唯一关联的地方
private ArrayList<AsgardManObserver> lists=new ArrayList<>();
private String message;
@Override
public void registerObserver(AsgardManObserver asgardManObserver) {
lists.add(asgardManObserver);
}

@Override
public void removeObserver(AsgardManObserver asgardManObserver) {
lists.remove(asgardManObserver);
}

@Override
public void notifyObservers() {
// 只要在遍历这里传递this就可以,观察者的update方法需要修改一下
lists.forEach(asgardMan -> asgardMan.update(this));
}

@Override
public void setMessage(String message) {
this.message = message;
System.out.println("Heimdall:"+message);
notifyObservers();
}
}

推模式与拉模式
1.推模型是假定主题对象知道观察者需要的数据;而拉模型是主题对象不知道观察者具体需要什么数据,没有办法的情况下,干脆把自身传递给观察者,让观察者自己去按需要取值。
2.推模型可能会使得观察者对象难以复用,因为观察者的update()方法是按需要定义的参数,可能无法兼顾没有考虑到的使用情况。这就意味着出现新情况的时候,就可能提供新的update()方法,或者是干脆重新实现观察者;而拉模型就不会造成这样的情况,因为拉模型下,update()方法的参数是主题对象本身,这基本上是主题对象能传递的最大数据集合了,基本上可以适应各种情况的需要。
3.拉模式会使观察者获取所有被观察者信息,同时可能会多次获取才能会得到需要的所有信息
4.但是想象其实,拉模式也是推模式的一种,只不过是推送个引用过去,里面包含了更多的信息

三、jdk对观察者模式的支持

jdk本身提供了对观察者模式的支持,并且支持推、拉两种方案。主要类或接口是java.util.Observable(抽象类)和java.util.Observer(接口)

这里就举个Head First 设计模式的一个例子吧,天气数据和天气显示器。不同的天气显示器会显示不同的信息。

用来储存天气信息的实体类

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
public class WeatherPojo {
// 温度
private float temperature;
// 湿度
private float humidity;
// 气压
private float pressure;

public WeatherPojo() {
}

public WeatherPojo(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
}

public float getTemperature() {
return temperature;
}

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

public float getHumidity() {
return humidity;
}

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

public float getPressure() {
return pressure;
}

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

被观察者:天气数据

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
public class WeatherData extends Observable {
private float temperature;
private float humidity;
private float pressure;

public WeatherData() {}

public void measurementsChanged(){
// 这个是observable中的标志,使类更加灵活
// 想象天气变个0.01度你都提醒是吧是烦死了
// 这里进行限制,达到一定条件再发送通知
setChanged();
notifyObservers();
}
public void updateData(WeatherPojo pojo){
this.temperature = pojo.getTemperature();
this.humidity = pojo.getHumidity();
this.pressure = pojo.getPressure();
// 被观察者数据发生了改变,提醒观察者
measurementsChanged();
}

public float getTemperature() {
return temperature;
}

public float getHumidity() {
return humidity;
}

public float getPressure() {
return pressure;
}
}

观察者:天气显示板

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 CurrentDisplay implements Observer {
Observable observable;
private float temperature;
private float humidity;

public CurrentDisplay(Observable observable) {
this.observable = observable;
observable.addObserver(this);
}
public void display(){
System.out.println("温度是:"+temperature+"; 湿度是:"+humidity);
}
// 注意这里有两个参数,前面是传递观察者对象
// 后面可以传递需要的参数,是notifyObservers()方法中的参数
// 这种就是参数和被观察者饮用都传过去
@Override
public void update(Observable o, Object arg) {
if (o instanceof WeatherData){
WeatherData weatherData = (WeatherData) o;
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
display();
}
}
}

jdk对观察者模式的实现需要被观察者继承Observable,对代码有一定的侵入,例如如果海姆达尔还要继承阿斯加德人这个类,那就需要我们手动来实现观察者模式了。

四、总结

观察者模式是比较常见的设计模式,我们常见的MVC就是标准的观察者模式,如果感兴趣看以google”使用观察者模式实现mvc”。此外向消息队列的发布订阅模式也是使用的观察者模式,而且是异步的性能更好,像我们上面实现的这种简单遍历,如果观察者实现复杂那性能看就会受到影响,毕竟要等待一个观察者执行完才能通知下一个观察者。

本文首发于cdream个人博客

欢迎转载,转载请注明出处!


参考资料:

  1. Head First 设计模式,Eric Freeman &Elisabeth Freeman with Kathy Sierra & Bert Bates
  2. java设计模式精讲 Debug 方式+内存分析
  3. 《JAVA与模式》之观察者模式
0%