设计模式之观察者(Observer)模式与其C++通用实现分上、中、下三篇。上篇详细讲解何为观察者模式以及其特点,并给出一个应用实例与其实现。中篇研究如何运用C++各种技术实现一个通用/万能的观察者模式。下篇讨论中篇所给出的实现可能遇到的问题及解决方案。
通过上篇的介绍我们知道了观察者模式的基本特点、使用场合以及如何以C++语言实现。有过多次编写观察者模式代码经验的你也许会发现,几乎所有的案例存在为数相当可观的重复性代码:定义一个观察者接口;定义一个主题并实现其诸如注册一/多个观察者,移除一/多个观察者,广播至所注册的观察者等基本行为。既然如此,我们有没有可能为所有观察者模式抽象出共有的接口与行为,以便日后复用呢?
此篇文章便是探讨如何实现一个通用或称为万能的观察者模式库。
我们为所有的观察者/订阅者抽象出一个共有的接口IObserver:
- struct IObserver {
- virtual void update() = 0;
- virtual ~Observer() {}
- };
当主题状态发生改变时IObserver对象的update方法会被自动调用。IObserver的子类会实现update方法,以便具有其特定的行为。考虑到update方法的具体实现,大部分情况下我们需要查询主题的状态,从而做出反应。这有多种实现方案:一是生成全局或类似全局性(如Singleton技术)的主题对象:
- Subject g_subject;
- ...
- struct ConcreteObserver : public IObjserver {
- virtual void update() {
- if (g_subject.getStatus() == xxx) {
- ...
- }
- };
因为“尽可能的不要使用全局对象”缘故,这种方式不常用。二是为update方法增加一个参数,以便告知update某些必要的信息,为具有普遍性,我以Event代表此类,定义如下:
- struct Event {
- Event(Subject &subject);
- BasicSubject *getSubject();
- virtual ~Event() {}
- };
很明显,这应该是个基类,所以具有需析构方法,此外,Event还提供一个获取主题的方法。BasicSubject类是我们随后要说到的主题基类。这样,IObserver接口的定义看起来应该是这样:
- struct IObserver {
- virtual void update(Event &event) = 0;
- virtual ~IObserver() {}
- };
接下来处理我们的主题,根据前面所提到的它应该具有的行为,它的定义应该大致像这样:
- class BasicSubject{
- public:
- virtual ~BasicSubject() {}
- void addObserver(IObserver &observer);
- void removeObserver(IObserver &observer);
- protected:
- void notifyAll(Event &event);
- protected:
- std::list observers_;
- };
BasicSubject基类有三个方法,分别是增加一个观察者,移除一个观察者以及通知已注册观察者。至于其实现,我留给读者,当作练习。
现在让我们通过以上三个基类(Event、IObserver及BasicSubject)来重新实现在上篇中所给出的例子:
- struct MMEvent : public Event {
- MMEvent(MMInteligenceAgent &sub) : Event(sub) {}
- };
- struct MMInteligenceAgent : public BasicSubject{
- MMStatus getStatus() const {return status_;}
- void trace() {notifyAll(MMEvent(this));} // for demonstrating how to use nofifyAll method.
- private:
- MMStatus status_;
- };
- struct Larcener : public IObserver {
- virtual void update(MMStatus status) {
- if (status == Sleeping) {
- ...
- }
- }
- };
现在是不是简单了许多?
不要停止你的脚步,更不要高兴的过早。
我们事先定义了三个接口让我们的客户遵循,约束太多了。
主题Subject与观察者Observer之间虽然已是抽象耦合(相互认识对方的接口基类),但仍可改进,使两者间的耦合度更低。
考虑到UI中的窗口设计,需要监视的窗口事件可能有:
- windowOpened
- windowClosing
- windowIconified
- windowDeiconified
- windowActivated
- windowActivated
- windowDeactivated
倘若代码全由你一人设计,你大可将以上7个事件合并为一个粗事件并通过窗口(也就是这里的Subject了)提供一个标志表明目前发生的是这7个中的哪一个事件,这没什么问题。但是,我相信并不是所有代码都由你一人包办,设想你的同事或是客户将WindowEventListener(也就是这里的Observer)设计成几个独立的更新方法的情况吧(java便是如此)。糟糕,我们目前定义的IObserver接口只支持单一更新方法。
是时候将我们的设计改进了。
事实上,在我们定义的三个基类当中最没有意义的便是IObserver接口,它什么也没帮我们实现,仅是个Tag标记,以便我们能为BasicSubject类指明addObserver及removeObserver方法的参数。通过模板技术,我们不必定义IObserver接口:
- template <
- class ObserverT,
- class ContainerT = std::list
- >
- class BasicSubject
- {
- public:
- inline void addObserver(ObserverT &observer);
- inline void removeObserver(ObserverT &observer);
- protected:
- ContainerT observers_;
- };
BasicSubject不需要虚析构函数,因为客户不需要知道BasicSubject类的存在。类模板参数ContainerT的存在是为了让客户可以选择容器类型,默认容器类型是std::list,也许你的客户更喜欢std::vector,于是他便可这样使用:
- class MyBasicSubject : public BasicSubject { ...};
当BasicSubject状态改变时需要通知观察者,所以notifyAll方法仍不可缺少。考虑到观察者可能具有多个更新方法,我们可以通过notifyAll方法的参数来指定要更新的方法。是的,就是函数指针了。所以nofifyAll方法可能是这样的:
- template<typename ReturnT,typename Arg1T>
- void BasicSubject::notifyAll(ReturnT (ObserverT::*pfn)(Arg1T), Arg1T arg1) {
- for (ContainerT::iterator it = observers_.begin(), itEnd = observers_.end(); it != itEnd; ++it) {
- ((*it)->*pfn)(arg1);
- }
- }
其中pfn是指向ObserverT类的、具有ReturnT返回类型的、接收一个类型为Arg1T参数的函数的指针。
现在连Event基类都不需要了,其角色完全由模板参数类型Arg1T所取代。
问题远没有结束。
仔细想想Arg1T参数类型的推导,编译器既可选择从pfn函数所声明的形参类型中推导也可选择从arg1实参推导,当实参(arg1)类型可唯一推导且与pfn函数声明的形参类型完全匹配时没问题。当实参类型与形参类型不匹配时编译器报错。如:
- struct MyObserver {
- void increment(int &val) {++val;}
- };
- struct MySubject : public BasicSubject {
- void trigger() {
- int i = 10;
- notifyAll(&MyObserver::increment, i);
- }
- };
我的编译器上的报错信息大致是:"template parameter 'Arg1T' is ambiguous" ... "could be 'int' or 'int &'"。编译器不知道Arg1T是int(从实参i推导)还是int&(从函数increment形参val推导)。编译器真傻。
此问题的根源是模板参数多渠道推导的不匹配性所致。为避免多渠道推导,聪明的你可能想到这样定义notifyAll方法:
- template <typename MemFunT,typename Arg1T>
- void BasicSubject::notifyAll(const MemFunT &pfn, Arg1T &arg1);
值得表扬。
设想pfn所声明的形参类型是const引用类型(如const int&)而用户把常量(如10)直接用作实参的情形吧:
- struct MyObserver {
- void increment(const int &val) {}
- };
- struct MySubject : public BasicSubject {
- void trigger() {
- notifyAll(&MyObserver::increment, 10);
- }
- };
编译器会抱怨不能把实参(10)类型(int)转换到形参(val)类型(const int&)。
那能否将arg1声明为const引用类型呢,即:
- template <typename MemFunT,typename Arg1T>
- void BasicSubject::notifyAll(const MemFunT &pfn, const Arg1T &arg1);
这会限制观察者更新方法对参数进行任何修改,不可接受。
按着你的思路,我可以给你一种解决方案,不过要将notifyAll方法声明为:
- template <typename MemFunT, typename Arg1T>
- inline void notifyAll(const MemFunT &pfn, Arg1T arg1) ;
是的,arg1前少个引用(&)符号。当观察者更新方法的形参类型为非引用类型时没任何问题,仅仅是多了一次拷贝而使效率稍微低下而已:
- struct MyObserver {
- void increment(int val) {}
- };
- struct MySubject : public BasicSubject {
- void trigger() {
- notifyAll(&MyObserver::increment, 10); // OK
- }
- };
但是当形参类型为引用类型时直接使用的结果与预期行为不符:
- struct MyObserver {
- void increment(int &val) {++val;}
- };
- struct MySubject : public BasicSubject {
- void trigger() {
- int i = 10;
- notifyAll(&MyObserver::increment, i);
- cout << i << endl; // 输出10,但我们期望是11
- }
- };
我们可以通过一个额外的辅助类将其解决:
- template <typename T>
- class ref_holder
- {
- T& ref_;
- public:
- inline ref_holder(T& ref) : ref_(ref) {}
- inline operator T& () const {return ref_;}
- };
- template <typename T>
- inline ref_holder ByRef(T& t) {
- return ref_holder(t);
- }
函数ByRef的存在仅仅是为了方便生成ref_holder对象(类似STL中的make_pair)。当需要引用传递时以ByRef函数作用到实参上:
- struct MyObserver {
- void increment(int &val) {++val;}
- };
- struct MySubject : public BasicSubject {
- void trigger() {
- int i = 10;
- notifyAll(&MyObserver::increment, ByRef(i));
- cout << i << endl; // 输出11,OK
- }
- };
现在没问题了,前提是能正确使用。但是,我敢打赌,你的客户会经常忘记ByRef函数的存在,以致最终放弃你所提供的解决方案。
我会给出另外一种更完美的方案。
实际上,此处的notfiyAll方法是个转发函数,对其的调用会转发给已向BasicSubject注册了的所有观察者对象的相应更新方法(我称之为目的函数)。为了具有正确的转发行为以及较高的效率,转发函数的形参类型声明与目的函数的形参类型声明必须遵循一定的对应规则。篇幅所限,这里直接给出结论(以下将“转发函数形参”简称为“转发形参 ”,将“目的调用函数形参”简称为“目的形参”。):
- 目的形参类型为const引用类型时,转发形参类型也是const引用类型;
- 目的形参类型为non-const引用类型时,转发形参类型也是non-const引用类型;
- 目的形参类型为其它类型时,转发形参类型是const引用类型。
我们通过模板traits技术可实现上面所提的转发——目的函数形参类型对应规则:
- template <typename T>
- struct arg_type_traits {
- typedef const T& result;
- };
- template <typename T>
- struct arg_type_traits {
- typedef T& result;
- };
- template <typename T>
- struct arg_type_traits<const T&> {
- typedef const T& result;
- };
最后一个traits的存在是必须的,因为引用引用类型(如int&&)在C++中是不合法的。现在我们可以定义我们的notifyAll方法了:
- template <typename ReturnT, typename Arg1T>
- inline void BasicSubject::notifyAll(ReturnT (ObserverT::*pfn)(Arg1T),
- typename arg_type_traits::result arg1) {
- for (ContainerT::iterator it = observers_.begin(),itEnd = observers_.end(); it != itEnd; ++it)
- ((*it)->*pfn)(arg1);
- }
聪明的你可能会问,万一观察者的更新方法参数不是一个呢?说真的,我也很想确定到底具有几个参数,令我悲伤的是我的客户经常这样回答:“我也不知道有几个。”
我使用了一种比较简单、笨拙却行之有效的手段解决了这一问题。我通过重载notifyAll方法,使其分别对应更新方法是0、1、2、3……个参数的情况。
- template <typename ReturnT>
- inline void BasicSubject::notifyAll(ReturnT (ObserverT::*pfn)()) {
- for (ContainerT::iterator it = observers_.begin(), itEnd = observers_.end(); it != itEnd; ++it)
- ((*it)->*pfn)();
- }
- template <typename ReturnT, typename Arg1T>
- inline void BasicSubject::notifyAll(ReturnT (ObserverT::*pfn)(Arg1T),
- typename arg_type_traits::result arg1) {
- for (ContainerT::iterator it = observers_.begin(),itEnd = observers_.end(); it != itEnd; ++it)
- ((*it)->*pfn)(arg1);
- }
- template <typename ReturnT, typename Arg1T, typename Arg2T>
- inline void BasicSubject::notifyAll(ReturnT (ObserverT::*pfn)(Arg1T, Arg2T),
- typename arg_type_traits::result arg1,
- typename arg_type_traits::result arg2 ) {
- for (ContainerT::iterator it = observers_.begin(), itEnd = observers_.end(); it != itEnd; ++it)
- ((*it)->*pfn)(arg1, arg2);
- }
- ...
- template <typename ReturnT, typename Arg1T, typename Arg2T, typename Arg3T, typename Arg4T, typename Arg5T>
- inline void BasicSubject::notifyAll(ReturnT (ObserverT::*pfn)(Arg1T, Arg2T, Arg3T, Arg4T, Arg5T),
- typename arg_type_traits::result arg1,
- typename arg_type_traits::result arg2,
- typename arg_type_traits::result arg3,
- typename arg_type_traits::result arg4,
- typename arg_type_traits::result arg5) {
- for (ContainerT::iterator it = observers_.begin(), itEnd = observers_.end(); it != itEnd; ++it)
- ((*it)->*pfn)(arg1, arg2, arg3, arg4, arg5);
- }
按我的经验,超过5个参数的类方法不常见,要是你真的有幸遇到了,你大可让实现作者与你共进晚餐,当然,账单由他付。你也大可再为notifyAll增加几个重载方法。
代码看起来有点复杂,但你的客户却很方便:
- struct MyObserver {
- void copy(int src, int &dest) {dest = src;}
- };
- struct MySubject : public BasicSubject {
- void trigger() { // demonstrate how to use notifyAll method.
- int i= 0;
- notifyAll(&MyObserver::copy, 100, i);
- assert(i == 100);
- }
- };
- int main(){
- MyObserver obs;
- MySubject sub;
- sub.addObserver(obs);
- sub.trigger();
- }
以上便是我所实现的通用观察者模式库的骨架。之所以称为骨架,是因为还有许多诸如多线程等现实问题没有考虑,我将在下篇中与读者一起探讨现实世界中可能遇到的问题。
<未完,待续>
阅读(857) | 评论(0) | 转发(0) |