观察者模式

使用场景

一个对象的属性发生改变时,需要通知到依赖它的对象并自动更新

实现原理

针对使用场景,如何用代码去实现这种效果,只要理解了观察者模式的基本原理,代码一目了然

  1. 首先明确有两个对象: 观察者,被观察者,我更喜欢理解为监视器和目标
  2. 目标对象里维护一个注册列表,里面记录了注册过的监视器,对外提供注册列表的添加和移除api
  3. 目标发生改变时,遍历这个列表里的所有监视器,通过调用监视器的一个方法通知监视器

代码思路

原理并不难,可以说十分简单,两个类,注册列表用一个List集合表示,是否发生改变用一个布尔值表示,很容易手写出来。

首先是目标类,读者根据上述思路阅读代码应该没有什么障碍

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
public class Target {

// 要发生改变的属性
private String name;

// 监视器的注册列表
private List<Monitor> monitors = new ArrayList<>();

// 是否发生改变的标志位
private boolean isChanged = false;

// 注册监视器
public void registMonitor(Monitor monitor){
Objects.requireNonNull(monitor);
monitors.add(monitor);
}

// 注销监视器
public void deregistMonitor(Monitor monitor){
Objects.requireNonNull(monitor);
monitors.remove(monitor);
}

// 改变属性
public void setName(String name) {
this.name = name;
this.isChanged = true;
this.notifyMonitor();
}

// 通知监视器
public void notifyMonitor(){
if (monitors.size() > 0 && this.isChanged){
for (Monitor monitor: monitors){
monitor.update(this);
}
// 通知结束后清除标志位
this.isChanged = false;
}
}

public String getName() {
return this.name;
}
}

然后是监视者类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Monitor {

// 起个名字
private String name;

// 根据目前对象发生的改变,做出反应
public void update(Target target) {
System.out.println("当前监视器为:" + getName() + ",监视的对象已发生改变,目标名称为:" + target.getName());
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

然后通过测试类测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
public static void main(String[] args) {
Monitor monitor1 = new Monitor();
monitor1.setName("1号");
Monitor monitor2 = new Monitor();
monitor2.setName("2号");

Target target = new Target();
// 测试两个监视器
target.registMonitor(monitor1);
target.registMonitor(monitor2);

// 改变属性
target.setName("jim");

// 注销其中一个
target.deregistMonitor(monitor1);
target.setName("tom");
}
}

输出结果如下:

1
2
3
当前监视器为:1号,监视的对象已发生改变,目标名称为:jim
当前监视器为:2号,监视的对象已发生改变,目标名称为:jim
当前监视器为:2号,监视的对象已发生改变,目标名称为:tom

说明通知成功,并且注销监视器对象后,不再接收到通知

JDK自带的观察者模式接口

为什么要用JDK自带的Observable(被观察者),Observer(观察者)呢?

点开Observable的源码,since JDK1.0就有的一个类,十分古老,所以出现Vector也不足为奇了,再看里面的方法,关于修改Vector和changed的地方都被synchronized修饰,说明JDK对线程安全性考虑的很好

JDK源码

Observable类中用于通知观察者的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void notifyObservers(Object arg) {
Object[] arrLocal;
/**
* 自己翻译了下注释:我们不希望观察者在处理自己的监视器时,
* 回调到所有的代码。我们从集合里取出每一个被观察者,并且
* 存储观察者的需要同步的状态,但是不应该通知观察者们。
* 任意竞争锁的最糟糕结果是
* 1.一个新增的观察者可能错过通知
* 2.一个最近注销的观察者在它不需要的时候被通知了
*/
synchronized (this) {
if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
}

for (int i = arrLocal.length - 1; i >= 0; i--)
((Observer) arrLocal[i]).update(this, arg);
}

实现流程大致类似

JDK方式代码实现

Step 1: 被观察者

继承Observable

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Target extends Observable {
private String name;

public void setName(String name) {
this.name = name;
this.setChanged();
this.notifyObservers("name has been changed, now is " + name);
}

public String getName() {
return name;
}
}

Step 2: 观察者

1
2
3
4
5
6
public class Watcher implements Observer {
@Override
public void update(Observable o, Object arg) {
Target target = (Target) o;
System.out.println("I am be notified by " + target.getName() + ", message is " + arg);
}}

Step 3: 验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
Target target = new Target();

Watcher foo = new Watcher();
Watcher bar = new Watcher();

// 注册
target.addObserver(foo);
target.addObserver(bar);

// 目标发生改变, 自动通知监视器,并调用update方法
target.setName("Jim");

// 移除监视器
target.deleteObserver(bar);
target.setName("Kim");
}

控制台输出

第一次setName通知了两个观察者,然后移除了bar观察者,下一次通知就只有一个观察者收到了

1
2
3
I am be notified by Jim, message is name has been changed, now is Jim
I am be notified by Jim, message is name has been changed, now is Jim
I am be notified by Kim, message is name has been changed, now is Kim

如果对JDK自带的线程安全实现方式不满意,可以自行实现这两个类

Author: 紫夜
Link: https://greedypirate.github.io/2018/07/26/观察者模式/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
支付宝打赏
微信打赏