10.13 观察者模式
观察者(Observer)模式用于解决一个相当常见的问题:当某些其他对象改变状态时,如果一组对象需要进行相应的更新,那么应该如何处理呢?这可以在Smalltalk的MVC(model-view-controller,模型-视图-控制器)的“模型-视图”或是几乎完全等价的“文档-视图设计模式”中见到。假定有一些数据(即“文档”)和两个视图:一个图形视图和一个文本视图。在更改“文档”数据时,必须通知这些视图更新它们自身,这就是观察者模式所要完成的任务。
在下面的代码中使用两种对象的类型以实现观察者模式。类Observable跟踪那些当一类对象发生某种变化时需要被通知的对象。类Observable为列表上的每个观察者调用成员函数notifyObservers()。成员函数notifyObservers()是基类Observable的一部分。
在观察者模式中有两个“变化的事件”:正在进行观察的对象的数量和更新发生的方式。这就是说,观察者模式允许修改这二者而不影响周围的其他代码。
可以用很多方法来实现观察者模式,下面的代码将创建一个程序框架,读者可根据这个框架构建自己的观察者模式代码。首先,这个接口描述了什么是观察者模式,如下所示:
因为在这种方法中Observer与Observable交互作用,所以必须首先声明Observable。另外,类Argument是空的,在更新过程中它只担任一个基类的角色,用于传递需要的任何参数类型。如果需要,也可以仅传递额外的像void*类型这样的参数。在这两种情况下无论哪种情况都有向下类型转换的操作。
类Observer是只有一个成员函数update()的“接口”类。当正在被观察的对象认为到了更新其所有观察者的时机时,它将调用此函数。函数的参数是可选的;可以调用一个没有参数的update(),这仍然符合观察者模式的要求。然而,更常见的是—它允许被观察的对象传递引起更新操作的对象(因为一个Observer可以注册到多个被观察对象)和任何额外的有用信息,而不必强迫Observer对象自己去搜寻正在被更新的对象并取得任何其他所需的信息。
“被观察对象”的类型是Observable的类型:
再次说明,这里的设计比实际必需的更精细些。只要有方法用Observable注册一个Observer并有方法为Observable更新其Observer,不必太在意其成员函数的设定。然而,这个设计的意图是可重用的。(它是从用于Java标准库的设计中摘取出来的。)[1]
Observable对象有一个用于指示是否已被修改的标志。在一个简单的设计中可能没有
标志;在出现变化时所有对象都将得到通知。然而需要注意的是,标志状态的控制是protected,所以只有继承者才能决定是什么造成了这个变化,而不是产生派生Observer类的末端用户。
收集到的Observer对象被保存在一个set<Observer*>中以防止复制;集合中的setinsert()、erase()、clear()和size()函数是开放的,允许在任何时候添加和删除Observer对象,因此提供了运行时的灵活性。
大部分工作是在函数notifyObservers()中做的。如果标志changed没有设置,它不做任何动作。否则,它将首先清除标志changed以防重复调用notifyObservers()所造成的时间浪费。这些是在通知观察者之前做的,以防被调用的update()会执行某些能够引起变化的操作,进而把这个变化反馈给Observable对象。然后它遍历集合set并回调每个Observer对象的成员函数update()。
起初似乎可以使用一个普通的Observable对象来管理更新操作,但这不会起作用;为了使之生效,必须从Observable派生出子类并且在派生类的代码中某处调用函数setChanged()。这就是设置标志“changed”的成员函数,这就意味着当调用notifyObservers()时,事实上所有观测者将得到通知。在哪里调用setChanged()取决于程序的逻辑设计。
现在我们进入了一个进退两难的窘境。被观察的对象将有不止一个类似的可选择项。例如,假设有一个用于处理GUI的可选择项—比如按钮—类似的可选择项有鼠标击发按钮、鼠标在按钮上方移过以及(由于某些原因)改变按钮颜色等。所以我们希望能够向不同的观察者报告所有这些事件,而每个观察者只对一种不同类型的事件感兴趣。
问题是,在这种情况下要达到以上目的采用多重继承:“为了处理鼠标击发按钮从Observable中继承,为了处理鼠标在按钮上方移动从Observable中继承,如此等等,好啦,……哦!这不可能实现。”
10.13.1 “内部类”方法
在某些情况下,必须(有效地)向上类型转换(upcast)成为多个不同的类型,但是在这种情况下,需要为同一个基类型提供几个不同的实现。从Java中引进了这种解决方法,这种方法比C++的嵌套类更优越。Java有一个被称为内部类的内建特征,它很像C++中的嵌套类,但是它能够通过隐式使用内部类创建的对象的“this”指针来访问其包含(外围)类的非静态数据成员。[2]
为了在C++中实现内部类(inner class)方法,必须显式获得和使用指向包含对象的指针。举例如下:
这个例子(有意以最简单的语法形式来说明这种方法;在后面很快就可以看到其实际的用法)以接口Poingable和Bingable开始,每个接口包含一个成员函数。由callPoing()和callBing()提供的服务要求它们接收的对象分别实现相应的Poingable和Bingable接口,除此之外它们对对象没有别的请求,这就使得使用callPoing()和callBing()具有最大限度的灵活性。注意,这两个接口中都缺少virtual析构函数—这就意味着不能通过接口来完成析构对象。
类Outer的构造函数包含一些私有数据(如name),它希望同时提供Poingable和Bingable两个接口,这样它就能同callPoing()和callBing()一起使用。(在这种情形下也可以仅使用多重继承,但在这里为清晰起见而保持简单的程序结构。)为了能够在Outer不派生自Poingable的前提下提供Poingable对象,这里使用了内部类方法。首先,class Inner的声明说明这是一个名为Inner的嵌套类。这就允许在后面能够将其声明为外部类Outer的友元类。其次,现在嵌套类能够访问外部类Outer的所有私有成员,现在就可以定义嵌套类了。注意,嵌套类有一个指向用于创建Outer对象的指针,这个指针必须在嵌套类的构造函数中进行初始化。最后,来自Poingable的poing()函数得以实现。另外一个用来实现Bingable的内部类采用同样的过程实现。每个内部类只有一个private实例被创建,该实例在Outer的构造函数中被初始化。通过创建成员对象并返回对它们的引用,排除了对象生存期可能产生的问题。
注意,两个内部类都是private的,事实上客户代码都不能访问其任何实现细节,因为两个访问函数operator Poingable&()和operator Bingable&()只返回一个用来向上类型转换为接口的引用,而不是实现它的对象。事实上,因为两个内部类是private的,客户代码甚至不能向下类型转换为实现类,这样就在接口和实现之间提供了完全的隔离。
这里获得了定义自动类型转换函数operator Poingable&()和operator Bingable&()的额外特权。在main()函数中,可以看到这些允许提供一种使得Outer看起来像是从Poingable和Bingable多重继承来的语法形式。不同之处在于这种“类型转换”在此情况下是单向的。只可以得到向上类型转换为Poingable或Bingable的效果,但是不能向下类型转换回Outer。在下面observer的例子中,可以看到更典型的方法:通过提供使用普通的成员函数而不是自动类型转换函数来访问内部类对象。
10.13.2 观察者模式举例
具备了Observer和Observable头文件和内部类方法的知识,现在请看一个观察者模式的程序例子:
在这里,令人感兴趣的事件是Flower的打开或关闭。由于内部类方法的使用,这两个事件成为可以独立进行观察的现象。类OpenNotifier和CloseNotifier都派生自Observable,因此它们能够访问setChanged(),并且能够处理需要Observable的任何事件。请注意,与InnerClassIdiom.cpp相反,Observable的派生是public的。这是因为它们的一些成员函数要求必须能够被客户程序员访问。没有任何规定要求内部类必须为private;在InnerClassIdiom.cpp中只是遵从“尽可能声明为私有”的设计原则。可以将这些类声明为private,并在Flower中设定代理来开放那些成员函数,但这并不会有多大好处。
在Bee和Hummingbird中内部类方法也很便利地定义了多种Observer,因为这两个类都需要独立观察Flower的打开与关闭。请注意,内部类方法是如何提供了许多和继承一样有益的特性(例如,能够访问外部类中的私有数据)。
在main()中,可以看到观察者模式的主要有益之处:以Observable动态地注册和注销Observer获得在程序运行时改变行为的能力。这个灵活性是以显著增加代码的代价而达到的—读者可能经常能看到在设计模式中的这种折中:增加某处的复杂性以换取另一处的灵活性的提升和(或)复杂性的降低。
如果仔细研究前面的例子,就会发现OpenNotifier和CloseNotifier使用了基本的Observable接口。这意味着,可以从其他完全不同的Observer类派生;Observer与Flower之间惟一的联系是Observer接口。
另外一种完成这种细微粒度的可观察现象的方法是对该现象使用某种形式的标记,例如空类、字符串或枚举等表示不同类型的可观察行为。这种方法可以使用聚合而不是继承来实现,不同之处主要在于时间与空间效率间的折中。而对于客户来说,这种差异是可以忽略的。
[1]它与Java不同,因为在通知所有观察者之前java.util.Observable.notifyObservers()不会调用clearChanged()。
[2]内部类和子程序闭包(subroutine closure)有些相似,子程序闭包用于引用一个函数调用的环境以便稍后复制。