在本讲,我们来学习一下行为型模式里面的第六个设计模式,即观察者模式。
概述
我们先来看一看观察者模式的概念。
观察者模式又被称为发布/订阅(Publish/Subscribe)模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象(所以,观察者对象是多的一方,主题对象是一的一方)。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。
以上观察者模式的概念,大家可以理解成之前学习过的监听机制。简单来说,就是我们创建一个监听器,并让该监听器去监听一个按钮,当该按钮的状态发生变化时,例如点击该按钮,监听器自动地执行某一操作。当然了,我们可以创建多个监听器去监听同一个按钮。
结构
在观察者模式中有如下角色:
-
Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每一个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加或者删除观察者对象。
这里我得多说一嘴,如果你将抽象主题角色定义成了一个接口而不是一个抽象类,那么在该接口里面是不能定义一个集合来存储多个观察者对象的,因此你可以将其推迟到子类(也即具体主题角色)中来实现。
-
ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知,这样,具体观察者对象里面的方法就会被自动调用了。
-
Observer:抽象观察者,是观察者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。
说白了,我们会为该角色定义一个接口,并在该接口中声明一个功能,当具体主题的内部状态发生改变时,该功能就会被自动调用。
-
ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。
说白了,具体观察者会实现抽象观察者定义的更新接口,并重写里面的方法,而该方法会在具体主题的内部状态发生改变时被自动调用。
观察者模式案例
接下来,我们通过一个案例来让大家再去理解一下观察者模式的概念,以及它里面所包含的角色,这个案例就是微信公众号。
分析
在使用微信公众号时,大家都会有这样的体验,当你关注的公众号中有新内容更新的话,它就会推送给关注公众号的微信用户端,因此,我们就可以使用观察者模式来模拟这样的场景了。下面我们就来分析一下观察者模式里面的角色都是由谁来充当的。
对于微信公众号而言,如果它的内容发生改变的话,那么它是不是要通知关注它的微信用户端啊!所以对于微信公众号来说,它是属于主题角色,即被观察者,而对于微信用户端来说,它是属于观察者角色。
以上内容分析清楚了之后,接下来我们就来看一看下面的这张类图。
先看以上类图的右侧部分,上面定义了一个Subject接口,该接口充当的就是观察者模式里面的抽象主题角色,而且该接口里面声明有三个方法,它们分别是:
- attach(Observer observer):添加观察者对象
- detach(Observer observer):删除观察者对象
- notify(String message):通知观察者对象进行更新
不过它们都是抽象方法,需要子实现类来实现。所以,在Subject接口下面我们又定义了它的一个子实现类,即SubscriptionSubject,该子实现类重写了父接口中的所有抽象方法。当然,它里面还声明有一个List集合,里面存储的都是Observer类型的对象,只不过Observer是属于抽象观察者角色,而且还是一个接口,所以该子实现类里面的List集合存储的肯定就是Observer接口的子实现类对象了。从以上类图中,我们也能看到在SubscriptionSubject类里面聚合了Observer接口。
然后,我们来看以上类图的左侧部分,可以看到上面定义了一个Observer接口,该接口充当的就是观察者模式里面的抽象观察者角色,它里面声明了一个update方法,表示更新操作,该方法会在具体主题的内部状态发生改变时被自动调用。
由于Observer接口里面的update方法是一个抽象方法,所以在Observer接口下面我们又定义了它的一个子实现类,即WeiXinUser,该子实现类充当的就是观察者模式里面的具体观察者角色。而且,该子实现类里面还声明了一个String类型的name成员变量,它是用来记录微信用户的名称的,除此之外,该子实现类还提供了一个有参的构造方法,并且去重写了父接口中的update方法。
分析完以上类图之后,接下来我们就要编写代码实现以上微信公众号案例了。
实现
首先,打开咱们的maven工程,并在com.meimeixia.pattern包下新建一个子包,即observer,也即实现以上微信公众号案例的具体代码我们是放在了该包下。
然后,创建抽象观察者接口,这里我们将其命名为了Observer。
package com.meimeixia.pattern.observer;
/**
* 抽象观察者接口
* @author liayun
* @create 2021-09-17 17:17
*/
public interface Observer {
/**
* @param message:主题推送的内容
*/
void update(String message);
}
接着,创建具体观察者类,这里我们将其命名为了WeiXinUser。
package com.meimeixia.pattern.observer;
/**
* 具体的观察者角色类
* @author liayun
* @create 2021-09-17 17:38
*/
public class WeiXinUser implements Observer {
private String name;
public WeiXinUser(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + "-" + message); // 谁接收到了什么更新的消息
}
}
紧接着,创建抽象主题接口,这里我们将其命名为了Subject。
package com.meimeixia.pattern.observer;
/**
* 抽象主题角色接口
* @author liayun
* @create 2021-09-17 17:16
*/
public interface Subject {
// 添加订阅者(即添加观察者对象)
void attach(Observer observer);
// 删除订阅者(即删除观察者对象)
void detach(Observer observer);
// 通知订阅者更新消息
void notify(String message);
}
抽象主题接口创建完毕之后,接下来我们就要创建具体主题类了,这里我们不妨将其命名为SubscriptionSubject。
package com.meimeixia.pattern.observer;
import java.util.ArrayList;
import java.util.List;
/**
* 具体主题角色类
* @author liayun
* @create 2021-09-17 17:27
*/
public class SubscriptionSubject implements Subject {
// 定义一个集合,用来存储多个观察者对象
private List<Observer> weiXinUserList = new ArrayList<Observer>();
@Override
public void attach(Observer observer) {
weiXinUserList.add(observer);
}
@Override
public void detach(Observer observer) {
weiXinUserList.remove(observer);
}
/**
* 不知你有没有想过该方法什么时候执行呢?是不是当微信公众号里面的内容发生变化时,该方法才会执行啊?
* 而且,当该方法执行时,它会通知所有的观察者对象去自动调用update方法。
*
* @param message:通知的内容
*/
@Override
public void notify(String message) {
// 遍历集合
for (Observer observer : weiXinUserList) {
// 调用观察者对象中的update方法
observer.update(message);
}
}
}
最后,我们来创建一个客户端类用于测试。
package com.meimeixia.pattern.observer;
/**
* @author liayun
* @create 2021-09-17 19:13
*/
public class Client {
public static void main(String[] args) {
// 1. 创建公众号对象
SubscriptionSubject subject = new SubscriptionSubject();
// 2. 订阅公众号
subject.attach(new WeiXinUser("孙悟空"));
subject.attach(new WeiXinUser("猪悟能"));
subject.attach(new WeiXinUser("沙悟净"));
// 3. 公众号更新,发出消息给订阅者(即观察者对象)
subject.notify("李阿昀的专栏更新了!");
}
}
此时,运行以上客户端类,打印结果如下图所示,可以看到当咱们公众号里面的内容发生变化时,所有的订阅者都能接收到这个消息。
至此,微信公众号案例我们就算是实现完了。你会发现,对于微信用户来说,只要他订阅了某个公众号,那么当公众号里面的内容发生变化时,该微信用户里面的update更新方法就会被自动调用,所以现在大家能理解观察者模式的概念了吧!
观察者模式的优缺点以及使用场景
接下来,我们来看一下观察者模式的优缺点以及使用场景。
优缺点
优点
关于观察者模式的优点,这里我总结出来了下面两点。
-
降低了目标与观察者之间的耦合关系,注意,两者之间是抽象耦合关系。
怎么看待这一点呢?大家要知道一点,就是这儿所说的目标指的就是被观察者对象(或者主题对象),有些人可能就会说,在下面代码处,不是也进行了一个耦合吗?
但是,你要注意,这儿的耦合是属于抽象耦合,抽象耦合的扩展性相对而言还是比较高的。
-
被观察者发送通知,所有注册的观察者都会收到信息,因此通过该特性我们可以实现广播机制。
缺点
关于观察者模式的缺点,这里我也总结出来了下面两点。
-
如果观察者非常多的话,那么所有的观察者收到被观察者发送的通知会很耗时。
大家不妨来看一下下面notify方法中的代码,现在我们是去遍历List集合,然后一个一个的去调用集合里面观察者对象的update方法。
如果观察者特别多的话,那么可能最后一个观察者收到的更新消息就会比较延时。
-
如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,这将会导致系统崩溃。
也就是说,我们在使用观察者模式时,一定要搞清楚观察者模式里面的角色以及角色和角色之间的一个关系,这样才不至于让我们设计的整个系统出现问题。
使用场景
只要出现如下几个场景,我们就可以去考虑一下能不能使用观察者模式了。
- 对象间存在一对多关系,而且一个对象的状态发生改变会影响其他对象。
- 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时。
JDK中提供的观察者模式的实现
对于观察者模式,JDK源码里面也提供了实现,所以接下来我们就来看一看在JDK中提供的观察者模式的实现。
在Java中,通过java.util.Observable
类(该类属于抽象主题角色)和java.util.Observer
接口(该接口属于抽象观察者角色)定义了观察者模式,所以我们只要实现这俩类和接口就可以编写观察者模式实例了。
在编写观察者模式实例之前,我先为大家讲一下java.util.Observable
类和java.util.Observer
接口。
Observable类是抽象主题类(即被观察者),它有一个Vector集合成员变量,用于保存所有要通知的观察者对象。
该Vector集合成员变量声明出来之后,并没有立即为其赋值,而是在无参构造里面对其进行赋值的。
下面我来向大家介绍一下该类里面最重要的3个方法,它们分别是:
-
void addObserver(Observer o):用于将新的观察者对象添加到集合中。
可以看到,该方法会首先判断Vector集合里面是否有要新添加的观察者对象,若有则做任何操操作,若没有则将新的观察者对象添加到集合中。
-
void notifyObservers(Object arg):调用集合中的所有观察者对象的update方法,通知它们数据发生了改变。通常越晚加入集合的观察者越先得到通知。
可以看到,该方法首先做的第一件事就是将Vector集合转换成了数组,然后倒序去遍历该数组,那做了些什么事情呢?无非就是拿到每一个观察者对象,并调用它里面的update方法,所以你现在该明白为什么通常越晚加入集合的观察者越先得到通知了吧!
-
void setChange():用来设置一个boolean类型的内部标志,注明目标对象发生了变化。当它为true时,notifyObservers方法才会真正地去通知观察者。
咱们还是来看一下以上notifyObservers方法,在该方法里面,你会发现如果changed标识为false的话,那么该方法就会直接返回了,就不会再执行下面的代码了,反之,才会继续执行下面的代码。
然后,我们再来看看setChange方法,如下图所示,可以看到在该方法里面是直接给changed标识赋了一个true的值,所以只有调用了该方法,notifyObservers方法才会真正地去通知观察者,让它们去调用自个的update方法。
那什么时候changed标识才会被设置为false呢?大家注意看以上notifyObservers方法,你会发现它里面在将Vector集合转换成了数组之后,又调用了一个clearChanged方法,而clearChanged方法就会将changed标识设置为false。
所以,只要主题内容发生了更新,那么notifyObservers方法就会真正地去通知观察者,并且它还得做一件事,那就是把changed标识改为false。
以上就是Observable类提供的3个重要的方法,大家一定要好好地理解一下哟!
接下来,我们来看一下Observer接口。
Observer接口是属于抽象观察者角色,它监视目标对象的变化(当然,你也可以理解成是主题对象的一个变化),当目标对象发生变化时,观察者得到通知,并调用update方法,进行相应的工作。
知道了以上Observable类和Observer接口之后,我们编写观察者模式实例就会很简单了,因为只需让具体主题类去继承Observable类,让具体观察者类去实现Observer接口就可以了。
下面我们就使用观察者模式来实现一个警察抓小偷的案例。
很明显,在该案例中,警察是观察者对象,而小偷是被观察者对象。既然该案例要使用到观察者模式,那么我们就可以使用JDK给我们提供的Observable类和Observer接口了。
首先,创建小偷类,这里我们就命名为Thief了。注意,由于小偷是一个被观察者,所以该类需要去继承Observable类。
package com.meimeixia.pattern.observer.jdk;
import java.util.Observable;
/**
* 小偷类(被观察者对象)
* @author liayun
* @create 2021-09-29 19:43
*/
public class Thief extends Observable {
private String name;
public Thief(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
/**
* 小偷偷东西的方法
*/
public void steal() {
System.out.println("小偷:我偷东西了,有没有人来抓我啊!");
super.setChanged(); // 将changed标识改为true
super.notifyObservers(); // 再去通知观察者,此时,观察者里面的update方法才会被执行
}
}
然后,创建警察类,这里我们就命名为Policemen了。注意,由于警察是一个观察者,所以需要让其去实现Observer接口。
package com.meimeixia.pattern.observer.jdk;
import java.util.Observable;
import java.util.Observer;
/**
* 警察类(观察者对象)
* @author liayun
* @create 2021-09-29 19:51
*/
public class Policemen implements Observer {
private String name;
public Policemen(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public void update(Observable o, Object arg) {
System.out.println("警察:" + ((Thief) o).getName() + ",我已经盯你很久了,你可以保持沉默,但你所说的将成为呈堂证供!");
}
}
最后,创建一个客户端类用于测试。
package com.meimeixia.pattern.observer.jdk;
/**
* @author liayun
* @create 2021-09-29 19:55
*/
public class Client {
public static void main(String[] args) {
// 创建小偷对象
Thief thief = new Thief("隔壁老王");
// 创建警察对象
Policemen policemen = new Policemen("老李");
// 让警察盯着小偷
thief.addObserver(policemen);
// 小偷偷东西
thief.steal();
}
}
此时,运行以上客户端类,打印结果如下图所示,可以看到只要小偷一偷东西,就会调用通知方法去通知警察进行抓捕,然后警察类中的update方法就会被执行了。
对于JDK里面提供的观察者模式的实现,相信大家应该很清楚了,嘻嘻!