设计模式之观察者模式

来源:互联网 发布:c语言打印出杨辉三角形 编辑:程序博客网 时间:2024/03/29 09:35

目前的例子,暂时都是java语言书写,稍后,我会补上.net和php语言版。

•1 什么是观察者模式

我们在日常生活中有很多观察者模式应用的场景。比如,凡是去过银行营业大厅办理业务的人,大多会有这么一段经历:办理业务之前,先要在营业大厅的门口领取一个排队号,然后你就可以在休息区等待叫号,当轮到自己办理业务的时候,某个柜台上方悬挂的小显示屏就会出现“请XXX号到XX柜台办理业务”。有的时候,某个柜台可能暂时停止办理业务,那么柜台上方的小屏就显示跟其它柜台上方的小屏同样的内容,以便提醒当前用户办理。对于这样的一个需求,你会怎么去实现它呢?我经过一番思考之后,写出了这么一段代码:

Java代码:

Php代码:

//运行结果如下

1号小屏:请9号到1号柜台号柜台办理业务

2号小屏:请9号到1号柜台号柜台办理业务

 

观察者模式又叫做发布-订阅(Publish/Subscribe)模式。观察者模式定义了一种一对多地依赖模式,让多个观察者同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有的观察者对象,使它们能够自动更新自己。这里的主题对象就是指通知者,又叫做发布者。观察者又叫订阅者。
    在上面这段代码中,我们让多个小屏同时监听银行柜台信息的变化,每当发生变化时,就立即通知所有监听的小屏,这些小屏就会更新自己的显示内容。在这个案例中,小屏就是观察者,银行柜台就是通知者,当通知者自身发生变化后,就会通知观察者,观察者会根据自身情况进行相应的更新。这就是观察者模式的雏形。
如果我们不使用观察者模式,那么用什么办法才能实现这样的需求呢?最直接的想法就是让各个小屏每隔一段时间就查询一次银行柜台,并判断银行柜台是否发生了改变,如果发生了改变,就去更新自己的显示内容,显然这是一种轮询的实现方式。根据这个思路,小屏类SmallScreen的Update方法就应该这样实现:

Java代码:

Php代码:

这跟观察者模式到底有什么不同呢?很显然,这种实现方式是一种“拉模型”,观察者需要自己主动轮询通知者的状态。观察者模式与之相反,它是一种“推模型”,通知者把变化通知给观察者。
观察者模式又叫做发布-订阅(Publish/Subscribe)模式。观察者模式定义了一种一对多地依赖模式,让多个观察者同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有的观察者对象,使它们能够自动更新自己。这里的主题对象就是指通知者,又叫做发布者。观察者又叫订阅者。在这个例子中,小屏类SmallScreen就是观察者Observer,银行柜台类Counter就是主题对象Subject。

 

•2 需求变化后的尴尬

我们的代码已经解决了由“小屏往回拉信息”到“柜台推送信息”的模式改变,这种改变的好处是效率的得到了提高,至少不用小屏频繁的访问银行柜台了,但是系统的可扩展性和可维护性却变差了,为什么这么说呢?我们可以分析一下:
在“小屏往回拉信息”的模式下,银行柜台类Counter,只要提供两个方法就行了,一个是getSubjectChanged,小屏通过它就可以知道银行柜台是否发生变化,另一个是getSubject,小屏通过它就可以获取银行柜台发布的内容,甚至于这两个方法完全可以合并成一个。在这种模式下,Counter与SmallScreen耦合点就是这两个方法。类SmallScreen必须访问类Counter,但是类Counter不必访问类SmallScreen,两者是一个单向耦合的关系。
在“柜台推送信息”的模式下,我们可以看一下代码。银行柜台类Counter发生变化的时候,强制小屏类SmallScreen进行Update,所以类Counter必须用到类SmallScreen,这是第一个耦合点。小屏类SmallScreen更新自己内容的时候,需要调用银行柜台类Counter的getSubject方法,这是第二个耦合点。所以,两者互相调用对方的方法,这是一个双向耦合的关系。
双向耦合有什么危害呢?耦合越多越紧密,系统就越难以维护,无论是修改或者扩展其中一个类的功能,都可能影响到其它的类。如果类之间都是独立的,或者联系非常弱,那么修改任何一个类,都不会影响到其它类,这是非常理想的状态。
现在银行方面又追加了一个需求:除了小屏显示信息之外,还需要通过音箱进行呼叫。这个需求在排队系统中是非常普遍的。我们之前完成的代码怎样应对这种变化呢?首先,我们需要再创建一个音箱类Speaker,然后需要在类Counter里面再增加一个音箱列表,最后在类Counter通知变化的方法notifyChange里再遍历一遍音箱列表。

Java代码:

Php代码:

我们增加了一个类Speaker,还修改了类Counter,,当前提出的需求算是解决了,可是代码的耦合度却加大了,已经有3个类纠缠在一起了,那么以后再追加接收信息的终端怎么办?每一次追加观察者,都不得不改一遍类Counter的代码,如果继续做下去,出错的几率将不断加大,可维护性不断降低,这段代码明显违背开闭原则,所以,我们需要重新审视之前的设计了。

•3 继续改进观察者模式

面向对象的设计中最重要的思维就是抽象。显然,无论是小屏、大屏还是音箱,它们只是外观和更新信息的手段有所不同,更新信息的功能却是一致的,所以,我们完全可以把这些观察者抽象出来一个基类Observer,更新信息的手段由各个观察者自己负责实现。我们再修改一遍代码:

Java代码:

Php代码:

//运行结果如下

1号小屏:请9号到1号柜台号柜台办理业务

2号小屏:请9号到1号柜台号柜台办理业务

3号音箱:请9号到1号柜台号柜台办理业务

经过我们这么一番改造,应对观察者的加入是绝对没有问题了,再也不用去修改柜台类Counter了,把柜台类对具体终端的依赖关系给去除了。不管是小屏还是音箱,柜台类一视同仁,进行同样的处理,它根本不需要知道需要通知的对象是什么。比如:银行又提出来加入大屏的显示。这样的需求,对于我们来说已经是很easy的事情了,可以从Observer类再派成出来一个大屏类LargeScreen。有人问:“大屏和小屏还不一样,为什么不用一个类来表示呢”。其实,做过排队项目的人可能很清楚,大屏幕的显示驱动和显示方式可能与小屏完全不同,甚至于供货厂商都不一样,在这个案例中,我们只是剥离出来它的其中一项显示功能而已。又比如:银行为了提升服务质量,准备加入短信提醒用户的功能。现在,我们是不是很容易对付了?

•4 又有新需求提出来

我们在开发软件的时候,用户需求很难做到百分之百的稳定,只能做到尽量稳定。银行又提出了新的需求:“除了柜台上可以发布呼号信息以外,银行内部的管理部门在某些情况下,也能利用大小屏发布一些紧急的信息”。 我们按照处理观察者方法,可以把这些通知者抽象出来,形成一个基类Subject,银行柜台和管理部门作为两个通知者,都可以从基类Subject派生出来,以后再增加通知者,就能够以此类推。我们按照这个思路形成最终一个版本:

Java代码:

php代码:

又经过我们的一番改造,应对主题和观察者的需求变化都没有什么问题了,以后针对类似这样的需求,我们已经非常有经验了,完全可以把这段代码稍加修改套用一下就可以了。换句话说,我们把观察者模式的实现方式做成了一个很小的框架。

原创粉丝点击