概述
在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” ——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。
一个软件系统常常要求在某一个对象的状态发生变化的时候,某些其它的对象做出相应的改变。做到这一点的设计方案有很多,但是为了使系统能够易于复用,应该选择低耦合度的设计方案。减少对象之间的耦合有利于系统的复用,但是同时设计师需要使这些低耦合度的对象之间能够维持行动的协调一致,保证高度的协作(Collaboration)。观察者模式是满足这一要求的各种设计方案中最重要的一种。
意图
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时, 所有依赖于它的对象都得到通知并被自动更新。[GOF 《设计模式》]
<Design Pattern>结构图
图1 Observer模式结构图
角色说明:
Subject(被观察的对象接口)
规定ConcreteSubject的统一接口;
每个Subject可以有多个Observer;
ConcreteSubject(具体被观察对象)
维护对所有具体观察者的引用的列表;
状态发生变化时会发送通知给所有注册的观察者。
Observer(观察者接口)
规定ConcreteObserver的统一接口;
定义了一个update()方法,在被观察对象状态改变时会被调用。
ConcreteObserver(具体观察者)
维护一个对ConcreteSubject的引用;
特定状态与ConcreteSubject同步;
实现Observer接口,通过update()方法接收ConcreteSubject的通知。
生活中的例子
观察者定义了对象间一对多的关系,当一个对象的状态变化时,所有依赖它的对象都得到通知并且自动地更新。拍卖演示了这种模式。每个投标人都有一个标有数字的牌子用于出价。拍卖师开始拍卖时,他观察是否有牌子举起出价。每次接受一个新的出价都改变了拍卖的当前价格,并且广播给所有的投标人进行新的出价。
图2 使用拍卖例子的观察者模式
示例用例图:
十字路口的红绿灯,行人和司机都看红绿灯的变化来行动,司机看到向左转的指示灯和行人看到绿灯过马路这一情景,正好符合我们的观察者模式,司机(Drive)和行人(Pedestrian)是具体观察者而指示灯(PilotLamp)是观察者接口和红绿灯(TrafficLight)具体被观察对象,先看用例图:
代码设计:
先创建PilotLamp.cs:
01 |
public interface PilotLamp |
再创建DelegateEvent.cs:
1 |
public delegate void EventHandler(); |
再创建TrafficLight.cs:
01 |
public class TrafficLight : PilotLamp |
03 |
public event EventHandler Notices; |
04 |
private string notice; |
再创建Driver.cs:
04 |
private PilotLamp greenLight; |
06 |
public Driver( string name, PilotLamp greenLight) |
09 |
this .greenLight = greenLight; |
14 |
Console.WriteLine( string .Format( "{1}司机,{0},请向左开车." , greenLight.Notice, Name)); |
再创建Pedestrian.cs:
01 |
public class Pedestrian |
04 |
private PilotLamp greenLight; |
05 |
public Pedestrian( string name, PilotLamp greenLight) |
08 |
this .greenLight = greenLight; |
10 |
public void GoThrough() |
12 |
Console.WriteLine( string .Format( "{0}同志,{1},请向前走." , Name, greenLight.Notice)); |
最后再调用:
01 |
public partial class Run : Form |
05 |
InitializeComponent(); |
08 |
private void btnRun_Click( object sender, EventArgs e) |
12 |
TrafficLight trafficLight = new TrafficLight(); |
14 |
Driver driverOne = new Driver( "张三" , trafficLight); |
15 |
Driver driverTwo = new Driver( "李四" , trafficLight); |
17 |
Pedestrian pedestrianOne = new Pedestrian( "王五" , trafficLight); |
18 |
Pedestrian pedestrianTwo = new Pedestrian( "麻六" , trafficLight); |
20 |
trafficLight.Notices += new Observer.EventHandler(driverOne.GoLeft); |
21 |
trafficLight.Notices += new Observer.EventHandler(driverTwo.GoLeft); |
22 |
trafficLight.Notices += new Observer.EventHandler(pedestrianOne.GoThrough); |
23 |
trafficLight.Notices += new Observer.EventHandler(pedestrianTwo.GoThrough); |
24 |
trafficLight.Notice = "绿灯亮了." ; |
25 |
trafficLight.TurnOn(); |
输出时选控制台应用程序如图:
结果如下图:
效果及实现要点
1.使用面向对象的抽象,Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达到松耦合。
2.目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播。观察者自己决定是否需要订阅通知。目标对象对此一无所知。
3.在C#中的Event。委托充当了抽象的Observer接口,而提供事件的对象充当了目标对象,委托是比抽象Observer接口更为松耦合的设计。
4.抽象主体角色公开了自身的事件,可以给任意观察者订阅。
5. 抽象观察者角色定义了统一的处理行为,在C#中使用事件-代理模式的话,统一的处理行为并不这么重要,有的时候甚至还会限制灵活性。由于本例的特殊原因,并没有从这个接口中得益。
6.响应方法订阅代理事件的操作可以在观察者中定义也可以在外部定义,根据自己的需求决定,放在外部定义灵活性更高。
7. 具体观察者往往只需要实现响应方法即可。
8.可以有多个主体角色、多个观察者角色交错,也可以一个类型是两个角色,主体也可以提供多个事件。从应用上来说观察者模式变化是非常多的。
适用性
1.当一个抽象模型有两个方面, 其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
2.当对一个对象的改变需要同时改变其它对象, 而不知道具体有多少对象有待改变。
3.当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之, 你不希望这些对象是紧密耦合的。
4.一个对象的行为引发其它多个对象的行为。前者成为主体,后者称为观察者。
5.为了降低耦合,不希望主体直接调用观察者的方法,而是采用动态订阅主体事件的方式来进行自动的连锁响应行为。
6.为了增加灵活性,希望动态调整订阅主体事件的观察者,或者希望动态调整观察者订阅主体的事件。
优点
1.观察者模式在被观察者和观察者之间建立一个抽象的耦合。被观察者角色所知道的只是一个具体现察者聚集,每一个具体现察者都符合一个抽象观察者的接口。被观察者并不认识任何一个具体观察者,它只知道它们都有一个共同的接口。由于被观察者和观察者没有紧密地耦合在一起,因此它们可以属于不同的抽象化层次。
2.观察者模式支持广播通信。被观察者会向所有的登记过的观察者发出通知。
缺点
1.如果一个被观察者对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
2.如果在被观察者之间有循环依赖的话,被观察者会触发它们之间进行循环调用,导致系统崩溃。在使用观察考模式时要特别注意这一点。
3.如果对观察者的通知是通过另外的线程进行异步投递的话,系统必须保证投递是以自恰的方式进行的。
4.虽然观察者模式可以随时使观察者知道所观察的对象发生了变化,但是观察者模式没有相应的机制使观察者知道所观察的对象是怎么发生变化的。
总结
1.通过Observer模式,把一对多对象之间的通知依赖关系的变得更为松散,大大地提高了程序的可维护性和可扩展性,也很好的符合了开放-封闭原则。
2.由于这种灵活性,在观察者订阅事件的时候需要考虑是否会出现破坏行为?是否会出现无限循环或死锁等问题?观察者响应的时候是否会影响其它观察者?
3.对于观察者数量很多的时候使用观察者模式并不适合,可能会造成性能问题。
4.在不能采用事件-代理方式完成观察者模式的情况下(比如跨网络应用等)可以考虑采用传统的观察者模式。