时隔一年多之后,我又回来更新这个系列了==。==
这次我们来看一下被广为使用,即使没接触过设计模式同学也一定听说过的,观察者模式。
JDK版本:11(Java8的语言特性)
参考书籍:《HEAD FIRST 设计模式》
IDE: IntelliJ IDEA
实例引入
接触过Web前端的同学都知道,DOM API中,可以给事件绑定回调函数 (callback
)
const node = document.createElement("div")
node.onclick = () => console.log("clicked!")
简单解释一下,代码第一行创建一个DOM,第二行给 onclick
设置一个函数,每当触发 click
事件时都会执行该函数【还有个闭包的效果,这里没体现,当然这也不是这篇的重点,不展开讲】。
来看一下点击之后的效果
同理我们还可以给其他事件绑定回调函数
node.ondrag = () => console.log("dragged!")
通俗来讲,上述代码做的是类似 trigger
一样的工作:当目标对象发生变化时,我们需要一个函数/方法去对其进行响应。
在上述语句中我们引入几个名词:
- 被观察对象(
observerable
):即目标对象node
- 观察者(
observer
):对observable
的变化进行响应的对象(示例代码中的匿名函数) - 订阅(
subscribe
):对象成为observer
的一员这一动作(node.onclick = ...
) - 广播(
boardcast
):observerable
向所有的observer
发送消息这一动作(node.click()
)
并给出观察者模式的正式定义:
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。https://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/observer.html “3. 观察者模式— Graphic Design Patterns - Read the Docs”
可以看到,上述JS代码其实就已经应用了一个简易的观察者模式。但需要注意有些许不同:
- JS代码中,虽然对一个
node
有多个观察者,但对一个事件只设了一个观察者,不是很符合 一对多的依赖关系定义。 - JS代码中使用了匿名函数(
anonymous function
),而Java中并没有函数,意味着需要使用接口来曲线救国,或者使用JDK8+的lambda
表达式来实现,但是本质还是接口。
具体实现
思路非常简单,目标对象内部维护一个观察者列表,当发生变化时,调用观察者提供的方法即可。
直接看UML图
Subject
接口有 attach
, detach
, notifyObservers
这几个方法。
Observer
接口有 update
一个方法即可。
// 被观测对象接口
public interface Observable {
void attach(Observer observer);
void detach(Observer observer);
void notifyAllObservers();
}
// 观察者接口
public interface Observer {
void update();
}
// 实现类
public class ObservableImpl implements Observable {
private List<Observer> observers = new ArrayList<>();
@Override
public void attach(Observer observer) {
this.observers.add(observer);
}
@Override
public void detach(Observer observer) {
this.observers.remove(observer);
}
@Override
public void notifyAllObservers() {
this.observers.forEach(Observer::update);
}
}
前面的部分如砍瓜切菜般写完,可 update
接口该怎么实现?是直接把整个目标对象传过去(或者干脆观察者内部保存目标对象应用),还是仅把需要的字段传过去?由此引出了观察者模式的两种实现—— Push
和 Pull
模型
Pull模型——数据自己取
在Pull模型中,update
的声明大概如下
void push(Observerable subject);
目标对象把自己作为参数直接传给观察者,观察者调用 getter
获取状态,然后进行操作。
所以这里要给 Observerable
接口 getter
方法
// 被观测对象接口
public interface Observable {
void attach(Observer observer);
void detach(Observer observer);
void notifyAllObservers();
State1 getState1();
State2 getState2();
}
当然了,如果想把代码写的ugly一点,也可以直接强制类型转换成真实对象的类型。
Push模型——主动送数据
在Push模型中,update
的声明大概如下
void update(int data1, int data2,...);
直接把需要的数据放在形参列表里写死,这样的好处是 Observer
拿到数据就能干活,不同的观察者所需要的数据不一样,需要创建多个 Observer
接口来重载 update
Pros and Cons
推模型是假定主题对象知道观察者需要的数据;而拉模型是主题对象不知道观察者具体需要什么数据,没有办法的情况下,干脆把自身传递给观察者,让观察者自己去按需要取值。
推模型可能会使得观察者对象难以复用,因为观察者的update()方法是按需要定义的参数,可能无法兼顾没有考虑到的使用情况。这就意味着出现新情况的时候,就可能提供新的update()方法,或者是干脆重新实现观察者;而拉模型就不会造成这样的情况,因为拉模型下,update()方法的参数是主题对象本身,这基本上是主题对象能传递的最大数据集合了,基本上可以适应各种情况的需要。https://www.cnblogs.com/KongkOngL/p/6849859.html “Java设计模式の观察者模式(推拉模型)”
简单以一表分析二者优劣
优势 | 劣势 | |
---|---|---|
Pull | 易于扩展 | 封装性受到破坏 |
Push | 松耦合 | 多需求下需要多个update方法 |
多线程解决观察者模型
观察者模型看似已经完美,但有一点值得引起注意:观察者很可能会造成阻塞,影响当前线程的继续执行,所以我们要请出多线程来帮忙,每当有 broadcast
发出时,每个观察就开一个线程来执行update。
下面来看一个例子,这里有一个简易的 Observable
Counter,观察者订阅其计数器变化的消息。
public class Counter implements Observable {
private List<Observer> observers = new ArrayList<>();
private int cnt = 0;
@Override
public void attach(Observer observer) {
this.observers.add(observer);
}
@Override
public void detach(Observer observer) {
this.observers.remove(observer);
}
@Override
public void notifyAllObservers() {
this.observers.forEach(observer -> observer.update(cnt));
}
void increase() {
cnt++;
notifyAllObservers();
}
void decrease() {
cnt--;
notifyAllObservers();
}
}
public class ObserverImpl implements Observer {
@Override
public void update(int cnt) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(cnt);
}
}
这里为了模拟,给update强行 sleep
,造成阻塞的效果。
再来看主函数
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
counter.attach(new ObserverImpl());
counter.increase();
counter.increase();
}
}
运行函数,发现线程被卡住了,第二个 increase
被阻塞。
提出改进:
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
counter.attach(cnt -> new Thread(() -> {
System.out.println(cnt);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start());
counter.increase();
counter.increase();
}
}
这里偷懒使用了 lambda表达式
,相信各位能够看懂。开新线程来执行update有效防止了阻塞调用。当然同步问题就要去考虑了2333
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!