本篇博客主要对设计模式中行为模式里的“观察者模式”进行简要介绍,以应付学校布置的《软件体系结构》作业。
观察者模式最早出现在”GOF“撰写的设计模式一书《Design Patterns Elements of Reusable Object-Oriented Software》,一说它最早是在一个以MVC框架为基础的SmallTalk项目中实现的。
事实上,观察者模式作为一种最佳实践,早在面向对象编程出现之前就已经存在了,甚至在一些汇编程序中也出现过它的影子。
它当初被称作”回调“,通常用各种语言的函数指针实现或用函数/子线程的标志来实现,代表了模块之间抽象通信的最早形式之一。
(上面这段对观察者模式来源的介绍出自谷歌的安卓开发工程师兼讲师Scott Stanchfield在stackoverflow上对一位询问观察者模式故事的网友的回答。)
首先借用一个例子来对观察者模式进行介绍,介绍的例子引用于加拿大西蒙菲莎大学Simon Fraser University在开放课程中对观察者模式的讲解示例
示例是一个网络威胁分析的应用程序,采用传统的MV模型,Model层用于监控网络故障(可以直观地理解为一些数据),View层用于展示这些威胁(可以直观地理解为数据的展示)
为了实现上述的应用程序,我们需要为Model数据层和View渲染层之间的通信选择合适的方式。
注意该方式下,Model层显式地、静态地、强耦合地知悉View层各个组件的类型和情况
当Model层发现了网络威胁时,Model层的监控指标(数据)会发生变化,从而使得View层的渲染组件显示的数据也要发生变化,这时Model层显式地通知View层的各个组件进行渲染更新。
/* Model层 */
class Model{
// 威胁数据
ThreatData threadData;
// 当威胁数据发送变化时,更新View层的各UI组件
if (threatData.changed()){
// 获取View层的各UI组件
UIStatics uiStatics = View.getUI("uiStatics");
UILocations uiLocations = View.getUI("uiLocations");
UILastThreat uiLastThreat = View.getUI("uiLastThreat");
// 更新View层各UI组件的数据致使View层重新渲染
uiStatics.changeData(threadData);
uiLocations.changeData(threadData);
uiLastThreat.changeData(threadData);
}
}
可以明显地看出这种方式的优缺点,优点即是编写非常简单。但缺点更为突出,即Model层与View层的耦合度太高,Model层必须显式地知悉View层各组件的类型和情况,导致可扩展性太差:一旦View层增删组件将必须重写代码!
这里给出伪代码
/* UI组件父类 */
class ViewUI {
// 这里编写一个开启单独线程的方法
public run(){
// 线程一直监听
while(true){
// 当Model层的数据变化时,UI组件修改绑定数据重写渲染
if (Model.threatData.changed()){
this.changeData(Model.threatData);
}
}
}
}
// 各UI组件开启单独线程进行监听
ViewUI viewUI1,viewUI2,...run();
该方式下,一定程度解决了方式一的问题,当View层的UI组件增加或删除时,新增时只需编写新UI组件询问Model层数据是否变化的代码,无需修改原有的代码;删除时则无需做任何代码的改动。但也引入了新的问题:由于Model层数据的改变时机是不确定的,因而每个UI组件需要开启单独的线程去监听Model层数据的变化,当变化发生时改变自己绑定的数据进行渲染
当UI组件过多时则需要开启许多单独的线程监听Model层数据的变化,这无疑增加了很大的开销,使得GUI程序非常不流畅,当UI组件较多时将导致程序开启了太多的无休止的线程导致崩溃!
在上述两种方法分别在程序的可扩展性&耦合性与性能上具有很大的问题上,观察者模式提供了一种非常优雅的解决方法。
在观察者模式中,Model层作为被观察的对象,它其中的threatData数据即作为它的状态,它还保存了一个View层各个UI组件抽象对象组成的列表(列表中的每个具体UI组件即是观察者,被动观察Model层状态的变化)当有新的UI组件加入时,仅需将它注册到被观察对象Model的观察者列表中即可。
每当被观察者Model层的状态threatData发生变化时,它就通知给抽象UI组件对列中的每一个具体UI实例,修改数据并重新渲染。
通过这种方式可以解决方式二中给每一个UI组件开辟监听线程的问题;相比于方式一而言,它只是将方式一中每个具体的UI组件变成UI组件的抽象集合(这也意味着每个UI组件需要继承抽象类或实现抽象方法)这样就降低了Model层和View层的耦合度,通过注册的方式解决了可扩展性问题。
观察者模式的代码和UML图将在第4节给出,使用上述例子并没有使用诸如新闻发布订阅
这种现实中的例子更好理解,更加直观。但观察者模式在GUI程序(如UI组件渲染,组件事件回调)的使用是非常广泛与真实的。
通过第3节中对观察者模式的用例我们对观察者模式进行总结。
1)观察者模式中的角色
/* 抽象被观察者 */
public interface AbstractSubject{
// 注册观察者
public void registerObserver(AbstractObserver abstractObserver);
// 解绑观察者
public void unRegisterObserver(AbstractObserver abstractObserver);
// 当被观察状态改变时,通知所有观察者进行修改
public void notifyObserver();
}
public interface AbstractObserver{
// 在被观察状态改变时更新
public void update(AbstractSubject abstractSubject);
}
/* 具体被观察者:Model */
public class Model implement AbstractSubject{
// 被观察状态:网络威胁
ThreatData threatData;
// 抽象观察者列表
ArrayList<AbstractObserver> abstractObserverList = new ArrayList();
// 注册观察者
@override
public void registerObserver(AbstractObserver abstractObserver){
this.abstractObserverList.add(abstractObserver);
}
// 解绑观察者
@override
public void unRegisterObserver(AbstractObserver abstractObserver){
this.abstractObserverList.remove(abstractObserver);
}
// 当被观察状态改变时,通知所有观察者进行修改
@override
public void notifyObserver(){
if(threatData.changed()){
abstractObserverList.foreach((item)->{item.update(this)});
threatData.setChanged(false);
}
}
}
public class Statics implement AbstractObserver{
// 更新分析数据
@override
public void update(AbstractSubject abstractSubject){
this.changeData(abstractSubject.statics);
}
}
public class Locations implement AbstractObserver{
// 更新分析数据
@override
public void update(AbstractSubject abstractSubject){
this.changeData(abstractSubject.locations);
}
}
3)适用场合
两类对象间存在一对多的关系,且观察方会根据被观察方的状态改变而执行相应的操作。
4)注意事项
5)观察者模式的优缺点
6)观察者模式在设计原则方面带给我们的启示
java.util包中包含了Observable
基类作为被观察者的父类,以及Observer
接口作为观察者的抽象接口
在观察者与被观察者交互的过程中,可以通过被观察者到观察者方向的通信(推),也可以通过观察者到被观察者的主动询问(拉)
JDK原生的抽象主题是通过基类而非接口实现的,这对于Java这种不支持多继承的语言显然不太优雅,因为它违背了合成复用原则(尽量使用合成/聚合的方式,而不是使用继承),但客观上它也避免了使用者重写被观察者的方法,提供了便利。
需要注意的是,JDK原生观察者模式中,被观察者在通知观察者更新时,并非按照观察者注册到被观察者的顺序,这意味我们不能依赖于注册顺序进行编程。
这是找工作面试中一个比较高频的问题,当然也是一个有意思的话题。
说它有意思的原因在于这里具有一个冲突:即在最初的一些讲解设计模式的书籍中,如《Design Patterns Elements of Reusable Object-Oriented Software》、《Head First 设计模式》认为观察者模式与发布订阅是等同的。
《Design Patterns Elements of Reusable Object-Oriented Software》
《Head First 设计模式》
但是如今国内的IT工业领域在面试时经常问的一个问题是:说说观察者模式与发布订阅模式的区别?
事实上,造成这一冲突的原因是如今国内IT工业界所谓的发布订阅模式与经典的设计模式教材中所说的发布订阅是不同的。
如今国内IT工业界所说的发布订阅模式是在观察者与被观察者之间增加了一层事件通道,原本的被观察者向观察者传递信息时不直接传递于观察者,而是发布到事件通道中。在传递的消息维度又增加了消息的类型,不同类型的发布者(被观察者)向事件通道推送不同的消息,订阅者(观察者)订阅不同类型的消息。由事件通道统一、间接地向订阅者(观察者)推送对应类型的消息。
实际上,观察者模式和发布订阅模式的本质上是一样的,发布订阅模式引入了事件通道这一中介,为发布者(被观察者)和订阅者(观察者)进行解耦,又引入了消息类型作为增强。
但是也可以看出,从使用上而言,观察者模式更注重被观察者状态改变引起观察者的操作;而发布订阅模式更注重发布者和订阅者之间消息的传递。
由于本篇博客的总结重心在于对观察者模式进行介绍,想要更加详细地了解发布订阅模式以及两者的异同还请参考其他文章。
观察者模式在工业界中的应用非常重要而广泛,作为经典的最佳实践受人称道。随着学术界软件体系结构的发展,观察者模式作为经典的设计模式也倍受关注。本节的目标则是粗略调研一下观察者模式在学术研究中的情况。
由于笔者时间和资源匮乏,仅以”观察者模式“为keyword在知网的中文数据库中进行统计,截止于当前2022年09月22日,共有252篇文献,以下是统计结果
1)以”主要主题“维度统计,以观察者模式
为”主要主题“的论文有30篇,占比约为:17.34%
,其中以设计模式
为”主要主题“的论文有69篇,占比约为:39.88%
2)在这252篇文献中,学术期刊
有122篇,学位论文
有120篇,会议
有5篇,图书
有4篇,特色期刊
有1篇
3)通过下面的以”观察者模式“为keyword的论文发表数量年份折线图可以看出,在2005年-2016年
间,”观察者模式“(设计模式)是一个比较火热的研究主题。
4)通过对论文摘要和内容的观察,可以发现文献中”观察者模式“主要是作为某一领域的应用,一些文献分析了”观察者模式”在一些API源码中的应用,较少文献对“观察者模式”进行扩展和改进。