您遇到了什么问题?
提出一个经典的需求:假设有一个报价系统。 当产品价格发生变化时,会通过短信发送给相关客户,价格变化也会通过电子邮件发送给客户。
用最经典的方法来实现这个需求。 这只是一个非常简化的实现。
创建两项服务,一项用于发送短信,一项用于发送电子邮件:
创建一个服务来处理价格变化的逻辑:
基于此设计的使用代码:
这种设计方法非常简单,也是一种非常非常常见的实现方案。 然而,这个解决方案显然存在一些问题:
· 两个感知价格变化的服务之间没有抽象接口,尽管感知价格的方法是相同的;
· 如果要添加新的价格变化感知服务,比如我现在要求日志系统中包含每次变化的价格,就需要添加一个实体类; 并且每次添加新的价格变化感知服务时,都必须将其添加为成员,然后手动依次调用;
简单来说,这种实现方案扩展性很差!
应用观察者模式
首先,我们简单解释一下什么是观察者模式。 举一个经典案例:公众号的关注度。 当用户关注某个公众号时,相当于该公众号将该用户添加到用户列表中。 这样只要公众号有新闻更新,就可以按照用户列表中的用户顺序推送消息。 如果用户不想接收来自公众号的信息,只需取消关注即可。
在《Head First Java》一书中,观察者模式被定义为:
观察者模式定义了对象之间的一对多依赖关系,以便当对象更改状态时,其所有依赖项都会得到通知并更新。
· 有状态对象的状态更新;
· 通知所有注册观察员进行更新; 其实就是这两个步骤。 我们首先使用观察者模式重写上面的案例。
首先为主体对象(有状态对象)定义一个接口:
· 注意第一个方法用于注册价格感知对象,即前一个,或者其他需要监听价格变化的对象;
第二步,为所有需要感知价格变化的业务提取一个接口:
该接口中只有一种方法可以进行价格变化感知; 那么该方法传入的价格就是变动后的价格;
然后重新实现发送电子邮件和短信的服务:
在之前的版本之上,只实现了接口;
最后,重新修改我们的价格变化对象:
在之前版本的基础上,实现了该接口。 因为是一对多,所以直接用List来存储即可。 方法中,分别调用每个注册价格传感器的业务方法。
该版本的测试代码:
益处
· 完全面向接口编程;
· 易于扩展。 如果我现在想添加日志功能,我只需要:
代码本身不需要修改,只需要在执行前注册一下即可:
就是这样。
标准观察者模式
上面的例子只是应用观察者模式的一个例子。 现在让我们抽象出标准的观察者模式并看一下类图:
有几点需要注意:
·:抽象主题(界面)是包含状态变化的类的抽象; 该接口中定义了三个方法:添加观察者、删除观察者、通知观察者;
·:主题界面的实现;
·:抽象观察者(接口)是对上述感知变化的对象的抽象,定义了感知变化后的业务逻辑方法。 这个方法中定义了两个参数,(本次变更的主体对象),(本次变更的数据),这两个参数涉及到变更数据的推送和拉取两种模式,后面会介绍,这里可以简单的写一下理解为利用这两个参数来获取变化后的数据(第二个可以看成是上面的)。
下面编写标准观察者模式的示例代码。
定义接口:
定义观察者接口:
定义具体的主题类实现,仍然使用简单的List来存储注册的观察者;
最后,制作一个观察者实现:
使用起来也非常简单。 您只需要像上面的例子一样创建一个特定的主题对象和一个特定的观察者,并将观察者注册到主题对象中。
Java中的观察者模式
由于观察者模式使用非常频繁,甚至被称为设计模式女王(不评价),所以Java实际上已经提供了观察者模式的实现。
在java.util包中,有两种类型:
· java.util.:可观察对象,我们的;
· java.util.:观察对象就是我们上面的;
我们看一下这两个类的结构,有一个直观的认识:
首先,接口的实现和我们上面的标准观察者接口定义是一样的。 仍然支持push和pull的方式来获取变化的数据,但无需过多解释;
让我们重点关注定义:
注意几点: 1. 首先,它是一个抽象类。 我们知道,不建议将这种抽象定义为抽象类,这会让我们的类组织关系变得非常僵化。 这是一个设计问题,我们先跳过(这个类从JDK1.0开始就存在,兼容问题一直没有更新,后面会介绍更好的方法)。 2. 我们看到了 和 方法。 显然,这两种方法都是用来维护观察者的; 3.方法,顾名思义,用于移除该主题的所有观察者; 4.方法和()两个方法,显然,它们都是通知观察者的方法。 它们只是一个可以主动传递的值(push),和一个不需要传递的值(这个值可以由主体对象通过方法提供,即pull)。 4、、、、这三个方法比较奇怪,肯定属于一组内容。 我们稍后会介绍它们。 这里只有一个重要的点需要注意,那就是在我们调用该方法之前,我们必须调用该方法来通知我们的更改已经建立(这样设计的原因将在后面讨论)
如果还是有点模糊的话,我们直接使用Java提供的观察者模式支持类来完成我们的价格监控示例:
从最简单的发送电子邮件服务开始:
我们只需要实现接口并完成方法中的逻辑即可。 这里需要注意的是,我们使用的是push方法,即我们直接从第二个参数中获取价格值,这意味着需要在主题对象中调用()方法来推送数据(注意以下代码);
日志服务:
接下来,主题对象的实现:
注意代码中的注释。 我们必须在调用方法之前调用该方法。
基于Java的观察者模式的测试代码:
测试代码很简单。
总结一下,那么Java提供的观察者模式到底为我们做了什么呢? 显然,主体对观察者的维护工作以及主体对观察者的通知动作都放在了类中。 那么我们简单分析一下该类的源码,主要解决两个问题:
· 如何维护观察者;
· 该方法的用途是什么?
类源码分析
· 维护两个成员变量,obs,代表数据是否确实发生变化。 obs 就是其中之一。 虽然性能较差,但确实是线程安全的。 这就是选择的原因。
· 构造方法中的初始化;
· 这里列出了与观察者相关的所有方法。 首先注意方法都是安全的,保证线程安全;
· 这是通知观察者并调用观察者更新方法的代码。 两种方法的区别已经在代码注释中说明了;
· 在方法中,我们注意到声明了一个局部变量[]; 并使用 = obs.(); 将当前观察者复制到 ,最后执行通知时,遍历 中的观察者引用; 这样做的目的是在当前执行通知时将其用作观察者快照。 那么,当它真的只是一个方法时,就不会引起正常的增删改查; 对并发下的性能会有帮助; 但可能有两个问题需要注意:
· 可能存在刚刚加入的观察者,无法及时获取变化数据;
· 可能有新删除的观察者仍然受到最近更改数据的影响;
· 在代码中,使用if(!)来判断数据是否真的发生了变化。 我们来看看相关的方法:
使用了三个方法来操作变量,代码也很简单。 但是通过这两段代码,我们也可以清楚的看出为什么调用方法之前一定要先调用方法。
有的小朋友可能觉得这样的设计有点没有必要,但是考虑到如果一个数据变化过于频繁,那么在某些情况下,我们也可以使用变量来控制实际发送数据变化消息的时机。 这也是设计的初衷。 酒吧。
Java的系统出了什么问题?
我们看了观察者模式的具体使用以及Java本身提供的相关代码实现。 那么,这样的设计有没有问题呢?
答案是肯定的。 有问题。 所以实际上我们发现真正使用Java的系统的代码并不多。 我觉得主要有两个原因:1.它是一个类,这本身就很烦人。 我的主题类需要继承它,代码结构就会有问题; 2.方法等重要方法导致除了继承之外,无法在外部使用第三方代码来推动主题的某些变化。 在某些情况下,存在很大的限制。
一些相关主题
上面已经对观察者模式进行了比较完整的概述。 你做完了吗? 还没有。 从观察者的角度,我们至少可以延伸出以下两个问题:
· 观察者模式和事件监听模式之间有什么关系?
· 观察者模式和发布订阅模式之间有什么关系?
这里再介绍两种模型:事件监听模型和发布订阅模型。 这两种模型可能会导致很多问题。 我们将在单独的文章中分别阐述。