前言:设计模式,一个老掉牙的话题,一个永恒的话题。所有的java开发,都必须经过的话题,都必须学习的话题。但是其实很多时候自己只是看了,了解了,然后忘了。所以还是想从头梳理一遍重要的设计模式,给自己看。不求逻辑多清晰,内容多完善,UML图画的多严谨,理解、会意就好。

遇到什么问题

提出一个经典的需求:假设现在有一个报价系统,当某个产品的价格发生变化时,就会通过短信发送给相关客户,也会通过邮件给该客户发送价格变动情况。

用最经典的方法来实现该需求。这里只是做了一个非常简化的实现。

创建两个服务,一个用于发送短信,一个用于发送邮件:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(1)

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(2)

创建一个服务,用于处理价格变动的逻辑:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(3)

基于该设计的使用代码:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(4)

该设计方式非常简单,也是非常非常常见的实现方案。但是该方案很明显存在一些问题:

· 两个感知价格变动的服务SmsSender和MailSender,没有抽象出接口,虽然感知价格的方法一致;

· 如果要新添加感知价格变动的服务,比如我现在要求把每次变动的价格计入日志系统,又需要增加一个实体类 PriceLogger;并且,每次增加新的价格变动感知服务,都要添加为PublicPriceService的成员,然后由PublicPriceService手动去依次调用;

简单来说,就是,这种实现方案,扩展性太差!

应用观察者模式

先简单说明一下什么是观察者模式。举个经典的案例:公众号的关注。当一个用户关注了某个公众号,就相当于该公众号把用户添加到了用户列表,这样一来,只要该公众号有消息更新,即可按照用户列表中的用户依次推送消息。如果用户不愿意收到该公众号的信息,就取消关注即可。

在Head First Java Pattern一书中,把观察者模式定义为:

观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,他所有依赖者都会收到通知并执行更新。

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(5)

· 有状态对象的状态更新;

· 通知所有注册的观察者进行更新; 其实就这两步。下面先用观察者模式重写上面的案例。

首先为主题对象(有状态对象)定义一个接口:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(6)

· 注意,第一个方法用于注册价格感知对象,即之前的SmsSender,MailSender,或者其他的需要监控价格变化的对象;

第二步,为所有需要感知价格变化的业务统一抽取一个接口:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(7)

该接口中就一个方法,执行价格的改变感应;那么方法传入的price就是变化之后的price;

然后重新实现发送邮件和短信的服务:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(8)

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(9)

在之前的版本之上,只是实现了PriceChangeNotifier接口;

最后,重新修改我们的价格改变对象:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(10)

在之前的版本基础上,实现了PriceChanger接口,因为一对多,先简单的使用List来存放。在notifyChangers方法中,分别调用注册的每一个价格感知器的业务方法。

这个版本的测试代码:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(11)

好处

· 完全面向接口编程;

· 扩展方便,如果我现在要增加记录日志的功能,只需要:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(12)

不需要修改PublicPriceService本身的代码,只需要在执行之前,把PriceLogger注册到PublicPriceService中:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(13)

即可。

标准的观察者模式

上面的例子,仅仅只是应用观察者模式的案例,现在来抽象出标准的观察者模式,看类图:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(14)

有几个点注意:

· Subject:抽象主题(接口),就是包含状态发生改变的那个类的抽象;在该接口中,定义了添加观察者,删除观察者和通知观察者三个方法;

· ConcreateSubject:主题接口的实现;

· observer:抽象的观察者(接口),就是上面说到的感知改变的对象的抽象,定义了一个感知改变之后的业务逻辑方法。这个方法中定义了两个参数,Subject(本次产生改变的主题对象),Object(本次变化的数据),关于这两个参数,涉及到改变数据的推送和拉取两种模式,后面介绍,这里就简单理解为用这两个参数来获取改变的数据就可以了(第二个Object看成上面的BigDecimal即可)。

下面写一段标准观察者模式示例代码。

定义Subject接口:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(15)

定义观察者接口:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(16)

定义具体的主题类实现,仍然先使用简单的List来存储注册的观察者;

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(17)

最后做一个观察者的实现:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(18)

使用起来也很简单,只需要和上面示例中一样,创建好具体的主题对象和具体的观察者,将观察者注册到主题对象中即可。

Java中的观察者模式

因为观察者模式使用非常频繁,甚至被誉为设计模式的皇后(不做评价),所以Java其实已经提供了观察者模式的实现。

在java.util包中,有两个类型:

· java.util.Observable:可被观察对象,即我们的Subject;

· java.util.Observer:观察对象,就是我们上面的Observer;

我们来看看这两个类的结构,有一个直观了解:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(19)

首先,Observer接口的实现和我们上面标准的观察者接口定义相同,仍然支持推送和拉取两种获取变化数据的方式,不过多说明;

下面重点看看Observable定义:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(20)

注意到几个点: 1,首先,Observable是一个抽象类。我们知道,不建议将这种抽象定义为抽象类,会让我们的类组织关系变得非常死板,这是设计上的一个问题,我们先跳过(这个类是JDK1.0就存在了,兼容问题没有更新,后面会介绍更好的方法)。 2,我们看到了addObserver和deleteObserver方法,很明显,这两个方法用于维护观察者; 3,deleteObservers方法,顾名思义,用于移除掉所有该主题上的观察者; 4,notifyObservers方法和notifyObservers(Object)两个方法,很明显都是告知观察者的方法,只是一个可以主动传递改变的值(推送),一个不需要传递改变的值(这个值可以主题对象通过getter方法提供,即拉取)。 4,setChanged,clearChanged和hasChanged,这三个方法比较奇怪,肯定是属于一组内容的,具体是什么,我们后面介绍。这里只需要注意一个重点,就是在我们调用notifyObservers方法之前,我们必须调用setChanged方法,告知我们的改变已经成立(为什么这么设计,后面讨论)

如果还有点模糊,我们直接使用java提供的观察者模式支持类,完成我们的价格监控的实例:

从最简单的发送邮件服务开始:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(21)

我们只需要实现Observer接口,然后在update方法中完成逻辑即可。这里需要注意,我们采用的是推送的方式,即我们直接从第二个参数中获取了price的数值,那么意味着主题对象中,需要调用notifyObservers(Object)方法来推送数据(注意观察后面的代码);

记录日志服务:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(22)

接下来,主题对象的实现:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(23)

注意代码中的注释,我们在notifyObservers方法调用之前,一定要调用setChanged方法。

基于Java的观察者模式的测试代码:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(24)

测试代码很简单。

小结,那么Java提供的观察者模式到底为我们做了些什么?很明显,主题对观察者的维护工作,主题对观察者的通知动作,都放到了Observable类中。那我们来简单分析一下Observable类的源码,主要解决两个问题:

· 怎么维护观察者;

· setChange方法到底用来干嘛;

Observable类源码分析

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(25)

· Observable维护两个成员变量,changed和obs,其中changed代表数据是否确定改变,obs是一个Observer的Vector,Vector虽然性能差,但确是线程安全的,这点是选用Vector的原因。

· 构造方法中初始化Vector;

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(26)

· 这里列出了和观察者相关的所有方法,首先注意,方法都是synchronized的,保证了线程安全;

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(27)

· 这是关于通知观察者,调用观察者更新方法的代码。在代码注释中已经说明两个方法的区别;

· 在notifyObservers方法中,我们注意到声明了一个局部变量Object[] arrLocal;并使用arrLocal = obs.toArray();将当前Vector中的观察者拷贝到了arrLocal中,最后在执行通知的时候,遍历的是arrLocal中的观察者引用;这样做的目的在于,使用arrLocal作为一个当前执行通知时的观察者快照,那么,在真正只是update方法的时候,不会造成Vector正常的添加和删除;对并发下的性能有一定帮助;但是可能会存在两个问题需要注意:

· 可能会有刚加入的观察者没法及时获取正在变更的数据;

· 可能会有刚删除的观察者仍然受到了最近正在变更的数据;

· 代码中,使用if(!changed)return 来判定数据是否真正改变。接下来看看changed相关的方法:

设计模式观察者怎么用(设计模式私人笔记-观察者模式)(28)

使用了三个方法来操作changed变量,代码很简单。但是我们通过这两段代码,也能清楚的看出,为什么我们在调用notifyObservers方法之前,一定要调用setChanged方法了。

可能有童鞋会觉得这样的设计有点多此一举,但是考虑到,如果一个数据变化过于频繁,那么我们在某些情况下,也可以通过changed变量来控制真正的发送数据变更消息的时机,这也是一个设计初衷吧。

Java的Observer体系有什么问题?

上面已经看了Java本身提供的观察者模式的具体使用和相关代码实现,那么,这样设计有问题么?

回答是肯定的。有问题。所以其实我们发现,真正使用Java的Observer体系的代码也不多。主要原因我认为有两个: 1,Observer是一个类,这个本身就是很烦的事情,我的主题类需要继承它,代码结构会有问题; 2,Observer中重要的方法,比如setChanged方法,导致除了继承,没法在外部通过第三方代码来促使主题确定改变,在一些场景中,有较大局限性。

一些相关话题

上面已经对观察者模式做了较完整的梳理,完了么?还没有。从观察者,我们至少能够延伸出下面两个问题:

· 观察者模式和事件监听模式有什么关系?

· 观察者模式和发布订阅模式有什么关系?

这里又引出了两个模式:事件监听模式和发布订阅模式,这两个模式又可以引申出一大堆问题。我们在分文章来独立阐述。

,