定义
定义对象间的一种一对多的依赖关系。当一个对象的状态发生变化时,所有依赖它的对象都得到通知并自动更新。
结构和说明
示例代码
public class ObserverDemo {
/**
* 目标对象,它知道它的观察者,并提供注册和删除观察者的接口
*/
public static class Subject {
/**
* 用来保存注册的观察者对象
*/
protected List<Observer> observerList = new ArrayList<>();
/**
* 注册观察者对象
*
* @param observer 观察者对象
*/
public void attach(Observer observer) {
observerList.add(observer);
}
/**
* 删除观察者对象
*
* @param observer 观察者对象
*/
public void detach(Observer observer) {
observerList.remove(observer);
}
/**
* 通知所有注册的观察者对象
*/
protected void notefiyObservers() {
observerList.parallelStream().forEach(observer -> {
observer.update(this);
});
}
}
/**
* 具体的目标对象,负责把有关状态存入到相应的观察者对象
*/
public static class ConcreteSubject extends Subject {
/**
* 示意,目标对象的状态
*/
@Getter
private String subjectState;
public void setSubjectState(String subjectState) {
this.subjectState = subjectState;
// 状态发生了改变,痛着各个观察者
this.notefiyObservers();
}
}
/**
* 观察者接口,定义一个更新的接口在那些目标发生改变的时候被痛着的对象
*/
public static interface Observer {
/**
* 更新的接口
*
* @param subject 传入目标对象,方便获取相应的目标对象的状态
*/
void update(Subject subject);
}
/**
* 具体的观察者对象,实现更新的方法,使自身的状态和目标的状态保持一致
*/
public static class ConcreteObserver implements Observer {
/**
* 示意,观察者的状态
*/
private String observerState;
@Override
public void update(Subject subject) {
// 具体的更新实现
// 这里可能需要更新观察者的状态,使其与目标的状态保持一致
ConcreteSubject concreteSubject = (ConcreteSubject) subject;
this.observerState = concreteSubject.getSubjectState();
}
}
}
命名建议
- 观察者模式又被称为发布-订阅模式。
- 目标接口的定义,建议在名称后面跟 Subject。
- 观察者接口的定义,建议在名称后面跟 Observer。
- 观察者接口的更新方法,建议名称为 update,当然方法的参数可以根据需要定义,参数个数不限、参数类型不限。
调用顺序
准备阶段示意图
运行阶段示意图
推模型和拉模型
- 推模型
目标对象主动向观察者推送目标的详细信息,不管观察者是否需要,推送的信息通常是目标对象的全部或部分数据,相当于在广播通信。
- 拉模型
目标对象在通知观察者的时候,只传递少量信息。如果观察者需要更具体的信息,由观察者主动到目标对象获取,相当于是观察者从目标对象中拉数据。一般这种模型的实现中,会把目标对象自身通过 update 方法传递给观察者,这样在观察者需要获取数据的时候,就可以通过这个引用来获取了。
比较
- 推模型是假定目标对象知道观察者对象需要的数据,而拉模型是目标对象不知道观察者具体需要什么数据,没有办法的情况爱,干脆把自身传递给观察者,让观察者自己去按需取值。
- 推模型可能会使得观察者对象难以复用,因为观察者定义的 update 方法是按需而定义的,可能无法兼顾没有考虑到的情况。这就意味着出现新情况的时候,就可能需要提供新的 update 方法,或者干脆重新实现观察者。
Java 中的观察者模式
改变
- 不需要定义观察者和目标的接口了,JDK 帮忙定义了。
- 具体的目标实现里面不需要再维护观察者的注册信息了,这个在 Java 中的 Observable 类里面,已经帮忙实现好了。
- 触发通知的方式有一点变化,要先调用 setChanged 方法,这个是 Java 为了帮忙实现更精确的触发控制而提供的功能。
- 具体观察者的实现里面, update 方法其实能同时支持推模型和拉模型,这个是 Java 在定义的时候,就已经考虑进去了。
示例代码
public class JavaObserverDemo {
/**
* 具体的目标对象,负责把有关状态存入到相应的观察者对象
*/
public static class ConcreteSubject extends Observable {
/**
* 示意,目标对象的状态
*/
@Getter
private String subjectState;
public void setSubjectState(String subjectState) {
this.subjectState = subjectState;
// 注意在用 java 中的 observer 模式的时候,下面这句话不可少
this.setChanged();
// 状态发生了改变,通知各个观察者
this.notifyObservers();
}
}
/**
* 具体的观察者对象,实现更新的方法,使自身的状态和目标的状态保持一致
*/
public static class ConcreteObserver implements Observer {
/**
* 示意,观察者的状态
*/
private String observerState;
@Override
public void update(Observable o, Object arg) {
// 具体的更新实现
// 这里可能需要更新观察者的状态,使其与目标的状态保持一致
ConcreteSubject concreteSubject = (ConcreteSubject) o;
this.observerState = concreteSubject.getSubjectState();
System.out.println(observerState);
}
}
public static class Client {
public static void main(String[] args) {
ConcreteSubject subject = new ConcreteSubject();
ConcreteObserver observer1 = new ConcreteObserver();
subject.addObserver(observer1);
ConcreteObserver observer2 = new ConcreteObserver();
subject.addObserver(observer2);
subject.setSubjectState("hello, world");
}
}
}
优缺点
优点
- 观察者模式实现了观察者和目标之间的抽象耦合
原本目标对象在状态发生改变的时候,需要直接调用所有的观察者对象,但是抽象出观察者接口后,目标和观察者就只是在抽象层面耦合了,也就是说目标只是知道观察者接口,并不知道具体的观察者类,从而实现目标类和具体的观察者类之间解耦。
- 观察者模式实现了动态联动
所谓联动,就是做一个操作会引起其他相关的操作。由于观察者模式对观察者注册实行管理,那就可以在运行期间,通过动态地控制注册的观察者,来控制某个动作的联动范围,从而实现动态联动。
- 观察者模式支持广播通信
由于目标发送通知给观察者是面向所有注册的观察者,所以每次目标通知的信息就要对所有注册的观察者进行广播。当然,也是可以在目标上添加新的功能来限制广播的范围。
在广播通信的时候要注意一个问题,就是相互广播造成死循环的问题。比如 A 和 B 两个对象互为观察者和目标对象, A 对象发生状态改变,然后 A 来广播信息,B 对象接口到通知后,在处理过程中,使得 B 对象的状态也发生了变化,然后 B 来广播信息,然后 A 对象接到通知后,又触发广播信息…,如此 A 引起 B 变化, B 又引起 A 变化,从而一直相互广播信息,就造成死循环。
缺点
- 可能会引起无谓的操作
由于观察者模式每次都是广播通信,不管观察者需要不要,每个观察者都会被调用 update 方法,如果观察者不需要执行相应处理,那么这次操作就浪费了。其实浪费了还好,最怕引起误更新,那就麻烦了,比如,本应该在执行这次状态更新前把某个观察者删除掉,这样通知的时候就没有这个观察者了,但是现在忘掉了,那么就会引起误操作。
思考
本质
触发联动
何时选用
- 当一个抽象模型有两个方面,其中一个方面的操作依赖于另外一个方面的状态变化,那么就可以选用观察者模式,将这两者封装成观察者和目标对象,当目标对象变化时,依赖它的观察者对象也会发生相应的变化。这样就把抽象模型的这两个方面分离开了,使得它们可以独立地改变和复用。
- 如果在更改一个对象的时候,需要同时连带改变其他对象,而且不知道究竟应该有多少对象需要被连带改变,这种情况可以选用观察者模式,被更改的那一个对象很明显就相当于是目标对象,而需要连带修改的多个其他对象,就作为多个观察者对象了。
- 当一个对象必须要通知其他的对象,但是你又希望这个对象和其他被通知的对象是松散耦合的。也就是说这个对象其实不想知道具体被通知的对象。这种情况可以选用观察者模式,这个对象就相当于是目标对象,而被它通知的对象就是观察者对象了。
相关模式
- 观察者模式和状态模式
观察者模式和状态模式是有相似之处的。
观察者模式是当目标状态发生改变时,触发并通知观察者,让观察者去执行相应操作。而状态模式是根据不同的状态,选择不同的实现,这个实现类的主要功能就是针对状态相应地操作,它不像观察者,观察者本身还有很多其他的功能,接收通知并执行相应处理只是观察者的部分功能。
当然观察者模式和状态模式是可以结合使用的。观察者模式的重心在于触发联动,但是到底决定是哪些观察者会被联动,这时就可以采用状态模式来实现了,也可以采用策略模式来进行选择需要联动的观察者。
- 观察者模式和中介者模式
观察者模式和中介者模式是可以结合使用的。
前面的例子中目标只是简单地通知一下,然后让各个观察者自己去完成更新就结束了。如果观察者和被观察的目标之间交互关系很负责,比如,有一个界面,里面有三个下拉列表组件,分别是选择国家、省份/州、具体的城市,很明显这是一个三级联动,当你选择一个国家的时候,省份/州应该相应改变数据,省份/州一改变,具体的城市也需要改变。
这种情况下,很明显需要相关的状态都联动准备好了,然后再一次性地通知观察者。也就是界面做更新处理,不会仅国家改变一下,省份和城市还没改,就通知界面更新,这种情况就可以使用中介者模式来封装观察者和目标的关系。在 Swing 的小型应用里面,也可以使用中介者模式,比如,把一个界面所有的事件用一个对象来处理,把一个组件触发事件以后,需要操作其他组件的动作都封装到一起,这个对象就是典型的中介者。