设计模式之观察者(Observer)模式与其C++通用实现分上、中、下三篇。上篇详细讲解何为观察者模式以及其特点,并给出一个应用实例与其实现。中篇研究如何运用C++各种技术实现一个通用/万能的观察者模式。下篇讨论中篇所给出的实现可能遇到的问题及解决方案。
设计模式之观察者(Observer)模式与其C++通用实现(中)
——林石
2008-10-03通过上篇的介绍我们知道了观察者模式的基本特点、使用场合以及如何以C++语言实现。有过多次 编写 观察者模式代码经验的你 也许会发现,几乎所有的案例存在为数相当可观的重复性代码:定义一个观察者接口;定义一个主题并实现其诸如注册一/多个观察者,移除一/多个观察者,广播至所注册的观察者等 基本行为 。既然如此,我们有没有可能为所有观察者模式抽象出共有的接口与行为,以便日后复用呢?
此篇文章便是探讨如何实现一个通用或称为万能的观察者模式库。
我们为所有的观察者/订阅者抽象出一个共有的接口IObserver:
- struct IObserver {
- virtual void update() = 0;
- virtual ~Observer() {}
- };
- 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() {}
- };
- 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<Observer*> observers_;
- };
现在让我们通过以上三个基类(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中的窗口 设计 ,需要监视的窗口事件可能有:
倘若代码全由你一人设计,你大可将以上7个事件合并为一个粗事件并通过窗口(也就是这里的Subject了)提供一个标志表明目前发生的是这7个中的哪一个事件,这没什么问题。但是,我相信并不是所有代码都由你一人包办,设想你的同事或是客户将WindowEventListener(也就是这里的Observer)设计成几个独立的更新方法的情况吧(java便是如此)。糟糕,我们目前定义的IObserver接口只支持单一更新方法。
- windowOpened
- windowClosing
- windowIconified
- windowDeiconified
- windowActivated
- windowActivated
- windowDeactivated
是时候将我们的设计改进了。
事实上,在我们定义的三个基类当中最没有意义的便是IObserver接口,它什么也没帮我们实现,仅是个Tag标记,以便我们能为BasicSubject类 指明 addObserver及removeObserver方法的参数。通过模板技术,我们不必定义IObserver接口:
- template <
- class ObserverT,
- class ContainerT = std::list<ObserverT*>
- >
- class BasicSubject
- {
- public:
- inline void addObserver(ObserverT &observer);
- inline void removeObserver(ObserverT &observer);
- protected:
- ContainerT observers_;
- };
- class MyBasicSubject : public BasicSubject<MyObserver, std::vector<MyObserver*> { ...};
- 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);
- }
- }
现在连Event基类都不需要了,其角色完全由模板参数类型Arg1T所取代。
问题远没有结束。
仔细想想Arg1T参数类型的推导,编译器既可选择从 pfn 函数所声明的形参类型中推导也可选择从arg1实参推导,当实参(arg1)类型可唯一推导且与 pfn 函数声明的形参类型完全匹配时没问题。当实参类型与形参类型不匹配时编译器报错。如:
- struct MyObserver {
- void increment(int &val) {++val;}
- };
- struct MySubject : public BasicSubject<MyObserver> {
- void trigger() {
- int i = 10;
- notifyAll(&MyObserver::increment, i);
- }
- };
此问题的根源是模板参数 多渠道 推导的不匹配性所致。为避免多渠道推导,聪明的你可能想到这样定义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<MyObserver> {
- void trigger() {
- notifyAll(&MyObserver::increment, 10);
- }
- };
那能否将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) ;
- struct MyObserver {
- void increment(int val) {}
- };
- struct MySubject : public BasicSubject<MyObserver> {
- void trigger() {
- notifyAll(&MyObserver::increment, 10); // OK
- }
- };
- struct MyObserver {
- void increment(int &val) {++val;}
- };
- struct MySubject : public BasicSubject<MyObserver> {
- 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<T> ByRef(T& t) {
- return ref_holder<T>(t);
- }
- struct MyObserver {
- void increment(int &val) {++val;}
- };
- struct MySubject : public BasicSubject<MyObserver> {
- void trigger() {
- int i = 10;
- notifyAll(&MyObserver::increment, ByRef(i));
- cout << i << endl; // 输出11,OK
- }
- };
我会给出另外一种更完美的方案。
实际上,此处的notfiyAll方法是个转发函数,对其的调用会转发给已向BasicSubject注册了的所有观察者对象的相应更新方法(我称之为目的函数)。为了具有正确的转发行为以及较高的效率,转发函数的形参类型声明与目的函数的形参类型声明必须遵循一定的对应规则。篇幅所限,这里直接给出结论(以下将“转发函数形参”简称为“转发形参 ”,将“目的调用函数形参”简称为“目的形参”。):
我们通过模板traits技术可实现上面所提的转发——目的函数形参类型对应规则:
- 目的形参类型为const引用类型时,转发形参类型也是const引用类型;
- 目的形参类型为non-const引用类型时,转发形参类型也是non-const引用类型;
- 目的形参类型为其它类型时,转发形参类型是const引用类型。
- template <typename T>
- struct arg_type_traits {
- typedef const T& result;
- };
- template <typename T>
- struct arg_type_traits<T&> {
- typedef T& result;
- };
- template <typename T>
- struct arg_type_traits<const T&> {
- typedef const T& result;
- };
- template <typename ReturnT, typename Arg1T>
- inline void BasicSubject::notifyAll(ReturnT (ObserverT::*pfn)(Arg1T),
- typename arg_type_traits<Arg1T>::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<Arg1T>::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<Arg1T>::result arg1,
- typename arg_type_traits<Arg2T>::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<Arg1T>::result arg1,
- typename arg_type_traits<Arg2T>::result arg2,
- typename arg_type_traits<Arg3T>::result arg3,
- typename arg_type_traits<Arg4T>::result arg4,
- typename arg_type_traits<Arg5T>::result arg5) {
- for (ContainerT::iterator it = observers_.begin(), itEnd = observers_.end(); it != itEnd; ++it)
- ((*it)->*pfn)(arg1, arg2, arg3, arg4, arg5);
- }
代码看起来有点复杂,但你的客户却很方便:
- struct MyObserver {
- void copy(int src, int &dest) {dest = src;}
- };
- struct MySubject : public BasicSubject<MyObserver> {
- 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();
- }
<未完,待续>