前言:
观察者模式又叫做 发布订阅模式,这个设计模式无论在工作还是生活的应用都是非常常见的,但是在我们的代码里面应用场景并不是很多,一般这种设计模式更多的是由 消息中间件进行替代,但是在swing
等GUI
框架里面可以看到大量的实际使用案例。
什么是观察者模式?
监听某一个对象的变化,同时可以根据对象的变化执行对应的不同方法。为了更好的解耦,监听者和发布者之间互相实现独立的接口,与此同时,观察者模式定义了对象之间的一对多依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新,如果需要更好的设计,可以通过Flag设置是否通知。值得一提的是,观察者模式和线程安全问题息息相关。
观察者模式结构图:
观察者模式主要的是两个接口,同时一般需要在发布者对象内部维护一个订阅者的集合,这样是为了方便发布者对于订阅者的消息推送,而订阅者的接口通常为更新数据用的接口,供发布者调用推送更新数据。
观察者模式的结构图还是比较好辨认的,因为存在订阅接口和发布接口。可以看到这是一个明显的松耦合的设计,订阅者不知道发布者的具体细节,发布者也不需要关注订阅者的细节,只需要关注更新数据的接口。
观察者模式的特点:
- 主题接口和发布者接口互相独立,同时主题接口一般需要组合订阅者在对象内部。
- 一对多的关系,表示一个被观察者对象对应多个观察者对象的关系。
- 观察者模式是一种行为型模式,因为他涉及到观察的行为和发布的行为,发布的行为是抽象的,而且订阅的行为也是抽象的。
什么情况下使用观察者模式?
观察者模式是一个无处不在的模式,关于消息订阅,异步通信等,基本都是对于观察者模式的翻版或者直接实现。当出现一对多的情况,比如多个对象需要监听一个对象的数据改变,或者一个接口的数据需要通知多个订阅者,就可以考虑使用观察者模式实现。
实际案例:
观察者模式在自己的构造层面用的比较少,但是在许多框架里面有用到,观察者模式更重要的是思想,所以大致看一下应用场景即可。
模拟场景:
这次的模拟场景参考基金的涨跌,我们都知道基金的涨跌是会实时告知订阅者的,所以我们将基金作为一个主题,然后人作为基金的订阅者,当基金在交易时间有涨跌的情况,就实时改变数据并且通知订阅者。
使用策略模式和工厂模式?
我们回顾之前学到的两个设计模式,策略模式和工厂模式:
策略模式的结构图如下所示:
很明显,策略模式虽然是行为型模式但是无法解决一个基金通知多个订阅者的需求,因为我们之前讲到订阅的行为和发布的行为都是需要抽象的,因为我们的基金虽然是作为主题并且可以由多个人实现,但是无法反映发布者和观察者直接松耦合这一个概念。
下面是工厂模式的结构图:
工厂模式是创建型模式,他所关注的是对象的创建而不是关注对象的行为,这里直接否决。
使用设计模式:
这里直接给出使用设计模式的形式,因为这种情况下使用观察者的设计模式的策略是最好的,他可以通过主题发布者通知所有的订阅者进行通知,我们通过设计一个基金的通用接口,提供供基金观察者注册和取消关注的接口。同时用具体的发布机构进行实现,设计一个基金的观察者模拟股民,提供对外的更新接口给基金的发布机构进行数据的推送,而基金观察者也就是股民只需要实现自己的具体业务即可。
我们同样按照观察者模式的设计结构图模仿做出一个基金的结构图设计:
我们根据上面的结构以及之前的说明设计出以下几个对应的类:
+ FundSubject.java 基金的相关接口 + FundConcreteSubject.java 基金的具体实现,充当发布者 + FundObserver.java - 股民 - 订阅者 + StockholderObserver.java 具体的股民订阅者实现
我们先来看一下基金的发布者,基金发布者需要维护一个基金订阅者的列表,同时需要提供对外的接口供基金订阅者进行注册:
/** * 基金的相关接口 * * @author zxd * @version 1.0 * @date 2021/1/31 20:19 */ public interface FundSubject { /** * 注册订阅者 * @param fundObserver 订阅者 */ void registerObserver(FundObserver fundObserver); /** * 移除指定订阅者 * @param fundObserver 订阅者 */ void removeObserver(FundObserver fundObserver); /** * 通知所有的订阅者 */ void notifyAllObserver(); }
发布者的具体实现类如下:
/** * 基金具体的实现方 * * @author zxd * @version 1.0 * @date 2021/1/31 20:26 */ public class FundConcreteSubject implements FundSubject { /** * 当前的单位净值 */ private double nownum; /** * 绑定所有的订阅者 */ private List<FundObserver> list; public FundConcreteSubject() { this.list = new ArrayList<>(); } @Override public void registerObserver(FundObserver fundObserver) { list.add(fundObserver); } @Override public void removeObserver(FundObserver fundObserver) { list.remove(fundObserver); } @Override public void notifyAllObserver() { list.forEach(item -> item.change(nownum)); } private void change() { double v = new Random(1000).nextDouble(); // 单位净值改变,通知所有的股民 nownum = v; notifyAllObserver(); } }
接着我们看一下订阅者,订阅者提供一个数据变动的接口,供发布者进行调用并且进行数据的通知推送和更新。
/** * 股民 - 订阅者 * * @author zxd * @version 1.0 * @date 2021/1/31 20:18 */ public interface FundObserver { /** * 基金涨跌接口 */ void change(double num); }
下面根据订阅者接口进行具体的实现:
/** * 具体的订阅者实现 * * @author zxd * @version 1.0 * @date 2021/1/31 20:23 */ public class StockholderObserver implements FundObserver { /** * 当前订阅者关注的单位净值数据 */ private double num; @Override public void change(double num) { this.num = num; display(); } public void display() { System.out.println("当前股票的净值为:" + num); } }
特点:
- 当我们需要扩展发布者的接口,直接实现发布的接口即可
- 当我们需要扩展订阅者,也可以直接通过直接实现接口即可进行处理
- 通常会在发布者里面维护一个订阅者的列表进行通知。
缺点:
- 上面的接口反应了一个基金每次更新数据都往订阅者推送数据。但是订阅者不一定想要收到。
- 订阅者不能主动的获取数据,只能够等待发布者推送数据。
- 如何实现订阅者主动获取到发布者的数据?
- java内置的观察者和订阅者实现了既可以由发布者推送数据给订阅者,也可以实现订阅者主动获取发布者的数据
JDK实现观察者模式:
JAVA官方是有实现观察者模式
的,下面说一下JDK自带的观察者模式如何实现:
+ JdkFundObserver.java JDK实现基金的订阅者接口 + JdkFundObserverConstruct.java JDK订阅者实现 + JdkFundSubject JDK发布者实现子类 + Observable JDK 发布者父类 + Main.java 单元测试
JDK实现基金的订阅者接口:
/** * JDK实现基金的订阅者 * * @author zxd * @version 1.0 * @date 2021/2/1 22:18 */ public interface JdkFundObserver extends Observer { }
JDK订阅者实现:
/** * JDK订阅者实现 * * @author zxd * @version 1.0 * @date 2021/2/1 22:47 */ public class JdkFundObserverConstruct implements JdkFundObserver { private double price; @Override public void update(Observable o, Object arg) { if (o instanceof JdkFundSubject) { if (arg == null) price += 1d; else price = (double) arg; System.err.println(price); price = ((JdkFundSubject) o).getPrice(); System.err.println(price); } } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } }
JDK发布者的实现类:
/** * JDK发布者 * * @author zxd * @version 1.0 * @date 2021/2/1 22:21 */ public class JdkFundSubject extends Observable { private List<JdkFundObserver> fundObservers; private double price; public JdkFundSubject() { this.fundObservers = new ArrayList<>(); } public void NotifyAll(){ price = new Random(1000).nextDouble(); setChanged(); notifyObservers(price); } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } }
JDK的发布者父类请查看:java.util.Observable
单元测试类:
/** * 单元测试 * * @author zxd * @version 1.0 * @date 2021/2/1 22:44 */ public class Main { public static void main(String[] args) { JdkFundObserver jdkFundObserver = new JdkFundObserverConstruct(); JdkFundSubject jdkFundSubject = new JdkFundSubject(); jdkFundSubject.addObserver(jdkFundObserver); jdkFundSubject.NotifyAll(); } }
变成观察者和发布者
发布者需要继承java.util.Observable
类。调用addObserver()
添加对应的观察者
观察者需要实现java.util.Observable
接口。
如何发送通知:
- 调用
setChanged()
(最重要的一步),标记状态更改了状态。这一步非常重要 - 调用
notifyObservers()
方法,发送通知给所有的观察者。
为什么需要更改状态订阅者才会接受通知?
JDK的flag标识是为了实现发布者可以手动进行通知。
注意:JDK的代码里面对于通知的方法没有进行方法同步(synchronized),而是使用synchronized
锁锁住整个Flag
的标识的改动代码,这里会遇到(可能)最坏的竞态条件是:
- 新加入的观察者收不到通知
- 已经取消监听的观察者收到了通知。
观察者如何接受通知:
实现更新update
的方法,参数略微不同,签名如下void update(Observable o, Object arg)
- 第一个参数是主题,在订阅者的
update()
方法,可以使用instanceof
判断对应的主题进行对应的操作 - 第二个参数是主题携带的参数,主题通过主动传递给观察者,观察者选择是否需要更新携带参数。
JDK实现的观察模式特点:
- 设置一个flag,可以控制发布者的通知开关,选择通知的时机
- 发布者通知参数的同时
- JDK的发布订阅是线程安全的,使用
synchronized
对于方法加锁,同时使用线程安全容器维护所有的订阅者。同时按照订阅者添加顺序进行通知。
JDK实现的观察模式缺点:
- 通知状态变更被实现为一个被保护的方法,通知标志被保护,依赖继承。
- 大量的同步方法以及使用线程安全的旧集合存储观察者,效率低
- 最大的问题在于JDK的观察者是一个类而不是一个接口。
总结观察者要点:
- 观察者模式定义了对象之间一对多的关系。
- 主题(也就是可观察者)用一个共同的接口来更新观察者的数据。
- 观察者和发布者之间用松耦合方式结合(loosecoupl-ing),发布者不知道观察者的细节,只知道观察者实现了观察者接口。
- 使用此模式时,你可从发布者处
推(push)
或拉(pull)
数据(然而,推的方式更加“正确”)。 - 有多个观察者时,不可以依赖特定的通知次序。
- Java有多种观察者模式的实现,包括了通用的
java.util.Observable
。 - 要注意
java.util.Observable
实现上所带来的一些问题。 - 如果有必要的话,可以实现自己的
Observable
。 Swing
大量使用观察者模式,许多GUI
框架也是如此。- 此模式也被应用在许多地方,例如:
JavaBeans
、RMI
。
总结:
从上面的实例可以看到,观察者模式在代码层面的应用其实真不算特别多,这个模式也如前文所说的更多的是应用在框架或者一些消息队列的模式里面。同时JDK的观察者模式也确实是一个不太好的设计。如果需要自己动手实现观察者模式,还是更加推荐自己实现。