重温设计模式之观察者
之前看过一些设计模式的书和文章,像head first,大话设计模式,不过一直都是囫囵吞枣,一知半解,
时间一长,大多忘了个十之八九,和没看过竟然相差无几,这实在是件很让人伤感的事。
这情形就好像你看过一部很经典的电影,有一天和女神聊天,女神说:我看过一部很经典的电影………,
然后神情的忘着你,想和你交流下那部电影的观后感,而此时,你的心里确是一片空白,你甚至会狠咬自己一口看能否从
疼痛中挤出点印象来,因为你知道,和女神聊电影的机会可是不多的,
可是很可惜,你还是错过了,于是你镇痛过后,决定再次重温,将其记录下来!
闲话太多了,下面切入正题,此处涉及如下几个问题:
1 观察者模式的定义?
2 它主要应用在何处?
3 示例?
4 剩下的问题?
1 观察者模式的定义:
此模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时, 所有依赖于它的对象都得到通知并被自动更新。[GOF 《设计模式》]
说白点意思就是:假如有对象a,b,c,当a发生变化时,需要对象b,c得到通知,并做相应的变化,那么此情形就是一个观察者模式。
2 应用在何处?
举几个例子:
(1)以去银行柜台取钱为例,步骤应该是你先去拿号然后等着显示屏上显示你拿的号码了,就过去柜台取钱,这是一个很经典的观察者模式。
柜台办理完业务后通知显示屏,然后显示屏更新信息,这里类图关系如下:
柜台类a,显示屏b,c,d等,类a变化时通知b,c,d,然后b,c,d等收到变更通知后更新信息
(2)以天气预报为例,气象台取得最新天气信息后,通知所有的电视台播报员,然后播报员预播最新的天气信息,
类图关系如:气象台a,电视台b,c,d等,气象台a观察到天气信息有变化,通知b,c,d天气有变,然后b,c,d播报最新天气信息
3 此时似乎还是一知半解,来看示例吧!
以银行柜台显示屏为例来看,我们先从最简单的实现代码开始,逐渐修改成观察者模式实现方式:
(1)先从实现如上需求最简单的代码开始,如下:
/// <summary> /// 柜台 /// </summary> public class Counter { private List<Screen> lstScreen = new List<Screen>();//显示屏列表 private string counterName;//柜台名称 private string bussinessNo;//当前业务号 public Counter(string name,string no) { this.counterName = name; this.bussinessNo = no; } //增加需要通知的屏幕 public void AddScreen(Screen Screen) { lstScreen.Add(Screen); } //删除需要通知的屏幕 public void RevScreen(Screen Screen) { lstScreen.Remove(Screen); } //柜台业务发生变化,通知显示屏更新信息 public void NotifyChange() { foreach(Screen Screen in lstScreen ) { Screen.Display(); } } //通知显示屏要更新的信息 public string GetInformation() { return "请"+bussinessNo+"号到"+ counterName+"号柜台办理业务!"; } } /// <summary> /// 显示屏 /// </summary> public class Screen { private string name; private Counter counter; public Screen(string name,Counter counter) { this.name = name; this.counter = counter; } public void Display() { Console.WriteLine(this.name+":"+counter.GetInformation()); } }
上面有2个类,柜台类和显示屏类,业务类调用代码如下:
static void Main(string[] args) { Counter counter = new Counter("1号柜台","业务号9"); Screen scr1 = new Screen("1号显示屏", counter); Screen scr2 = new Screen("2号显示屏", counter); counter.AddScreen(scr1); counter.AddScreen(scr2); counter.NotifyChange(); }
执行过程:先实例化柜台类,然后将要通知显示的显示屏加到通知列表,然后调用通知的方法,
显示屏更新的过程在同志的 方法里实现。上述过程有个显而易见的问题就是,双向耦合调用。
public void NotifyChange() { foreach(Screen Screen in lstScreen ) { Screen.Display(); } } public void Display() { Console.WriteLine(this.name+":"+counter.GetInformation()); }
柜台类的通知方法
NotifyChange()
里调用了显示屏更新信息的方法,而显示屏更新信息
Display()
的方法又调用了柜台类获取通知信息的方法。
(2)若此时需求出现变更,除了显示屏,还需要用广播通知,即柜台办理完业务后,需要用广播更新信息,
业务你想到了,再新建一个广播类(和显示屏类类似),然后在柜台类最如下修改
//柜台业务发生变化,通知显示屏更新信息 public void NotifyChange() { foreach(Screen Screen in lstScreen ) { Screen.Display(); }
遍历广播类,通知其更新信息 }
我们增加了一个广播类Speaker,还修改了类Counter,,当前提出的需求算是解决了,可是代码的耦合度却加大了,已经有3个类纠缠在一起了,那么以后再追加接收信息的终端怎么办?每一次追加观察者,都不得不改一遍类Counter的代码,如果继续做下去,出错的几率将不断加大,可维护性不断降低,这段代码明显违背开闭原则,所以,我们需要重新审视之前的设计了。
此时也许你想到了抽象出一个类作为显示屏,和广播类的基类。面向对象的设计中最重要的思维就是抽象。显然,无论是小屏、大屏还是音箱,它们只是外观和更新信息的手段有所不同,更新信息的功能却是一致的,所以,我们完全可以把这些观察者抽象出来一个基类Observer,更新信息的手段由各个观察者自己负责实现。
abstract public class Observer { protected String name; protected Counter counter; //构造函数 public Observer(String name, Counter counter) { this.name = name; this.counter = counter; } //更新信息 public abstract void update(); }
然后柜台类作如下修改:
private List<Observer> lstScreen = new List<Observer>();
经过我们这么一番改造,应对观察者的加入是绝对没有问题了,再也不用去修改柜台类Counter了,把柜台类对具体终端的依赖关系给去除了。不管是小屏还是音箱,柜台类一视同仁,进行同样的处理,它根本不需要知道需要通知的对象是什么。比如:银行又提出来加入大屏的显示。这样的需求,对于我们来说已经是很easy的事情了,可以从Observer类再派成出来一个大屏类LargeScreen。有人问:“大屏和小屏还不一样,为什么不用一个类来表示呢”。其实,做过排队项目的人可能很清楚,大屏幕的显示驱动和显示方式可能与小屏完全不同,甚至于供货厂商都不一样,在这个案例中,我们只是剥离出来它的其中一项显示功能而已。又比如:银行为了提升服务质量,准备加入短信提醒用户的功能。现在,我们是不是很容易对付了?
(3)银行又提出了新的需求:“除了柜台上可以发布呼号信息以外,银行内部的管理部门在某些情况下,也能利用大小屏发布一些紧急的信息”。 我们按照处理观察者方法,可以把这些通知者抽象出来,形成一个基类Subject,银行柜台和管理部门作为两个通知者,都可以从基类Subject派生出来,以后再增加通知者,就能够以此类推。我们按照这个思路形成最终一个版本:
抽象出柜台类的基类
public abstract class Subject { //观察者列表 private List<Screen> lstScreen = new List<Screen>(); //增加需要通知的屏幕 public void AddScreen(Screen Screen) { lstScreen.Add(Screen); } //删除需要通知的屏幕 public void RevScreen(Screen Screen) { lstScreen.Remove(Screen); } //柜台业务发生变化,通知显示屏更新信息 public void NotifyChange() { foreach(Screen Screen in lstScreen ) { Screen.Display(); } } //通知显示屏要更新的信息 public string GetInformation() { return "请"+bussinessNo+"号到"+ counterName+"号柜台办理业务!"; } }
柜台类等通知类继承此基类,其它地方无需更改,调用如下:
Counter counter = new Counter("1号柜台","业务号9"); Screen scr1 = new Screen("1号显示屏", counter); Screen scr2 = new Screen("2号显示屏", counter); counter.AddScreen(scr1); counter.AddScreen(scr2); counter.NotifyChange(); Speaker speaker = new Speaker("1号广播", "业务号9"); Screen scr1 = new Screen("1号显示屏", speaker); Screen scr2 = new Screen("2号显示屏", speaker); speaker.AddScreen(scr1); speaker.AddScreen(scr2); speaker.NotifyChange();
又经过我们的一番改造,应对主题和观察者的需求变化都没有什么问题了,以后针对类似这样的需求,我们已经非常有经验了,完全可以把这段代码稍加修改套用一下就可以了。换句话说,我们把观察者模式的实现方式做成了一个很小的框架。
参考博客:http://blog.csdn.net/wanghao72214/article/details/4017507