随笔-4  评论-40  文章-117  trackbacks-0

 
  (转)

关于低耦合的消息传递,实现的方式有很多,哪种方法更好与具体的使用环境有关,本文使用试错的方法,逐步探索达成这一目的具体方式,并理解实现方式背后的原因。

 

面向对象的系统当中,不可避免的有大量的类间消息传递的需求:一个类需要通知另一个或几个类做些什么。

 

这种类间消息传递,简单的说,就是调用其他类的方法。

如下:

void A::OnMessageXX()

{

         B::GetInstance()->DoSomething();

}

在这里,类A需要通知类B做些事情。这种调用在所有的面向对象程序中都是极其常见的。

 

但是如果类A需要调用类B,就不可避免的产生了耦合性。虽然耦合性终归是不可能完全避免的,但是在一定程度上降低耦合性是完全可能的。

(至于为什么在设计中应该尽可能降低耦合性,不在本文的探讨范围之内)

 

上面的例子,我们使用了Singleton的模式,从全局作用域中获取了B的实例,并调用了B的相关方法。使用Singleton的一个缺点是,假若我们希望对类A编写测试代码,我们需要做一些额外的解耦合工作。(关于编写测试与解耦合,可以参考Robert C. Martin Series的Working Effectively with Legacy Code一书,该书的中译版在这

我们也可以通过将B参数化的方法降低A与B间的耦合程度,像下面这样:

void A::OnMessageXX(B* pBInstance)

{

         pBInstance->DoSomething();

}

现在的写法要比之前的做法耦合性低,通过使用多态的方法,现在传入函数的类B指针可能是另一个实现了B的相应接口的派生类,A并不关心B接口背后的具体实现。

 

但是等等,你说,现在对类B的耦合性虽然在A中被降低了,但是依旧存在于调用A::OnMessageXX的地方。在那里我们还是需要取得B的实例,然后传递给A。

 

没错,是这样。

 

通过参数化类A的方法,我们把类A与类B间的耦合转移了一部分到A的调用者那里。实际上总的耦合并没有消除,只是被分解了。但是程序设计中不可能完全不存在耦合,我们需要做的是”正确”,而不是”完美”。类A的耦合性降低了,使得我们在未来需求变更的时候,类A有更大的可能性不需要被修改,并且对功能的扩展更加友好,这就达成了我们的目标了。

基于上述做法,如果我们在未来扩展是派生出一个B的子类,override相关的方法,那么类A的代码基本是不需要修改的。

 

不过,问题是,假若A::OnMessageXX中,并不仅仅需要对类B发出消息,还需要对一系列相关的类B1,B2,B3等等发出消息呢?

 

哦,或许我们可以这样做:

 

void A::OnMessageXX(const std::list<B*>& lstBInstances)

{

         for (std::list<B*>::const_iterator itr = lstBInstances.begin();

                   itr != lstBInstances.end();

                   ++itr)

         {

                   (*itr)->DoSomething();

}

}

 

是的,上面这是一种做法,有一系列B的对象需要被通知到,所以我们可以用一个列表把他们串起来,然后在循环中通知他们去干活。不过这样做的前提是,这一系列B对象都是派生自一个公共基类B,有共通的接口;此外,我们需要在A的OnMessageXX被调用之前构造一个需要接受通知的B对象列表。

 

当A需要通知B,C,D等一系列没有公共接口的对象的时候,上面的这种做法就无法处理了。

 

对于B、C、D等需要由A来调用的类来说,它们需要在A通知它们的时候,做一些特定的事情。而又A则是在某些特定的时刻需要通知B、C、D。这样,我们可以把问题看成一个消息响应机制。

 

B、C、D可以在A的某些事件上注册一些回调函数,当事件发生时,A确保注册该事件的函数被调用到。

 

如下:

typedef void(callback*)();

class A {

public:

         enum EventIds {

         EVENT_MSG1,

         EVENT_MSG2,

};

void RegisterEvent(int nEventId, callback pfn);

private:

callback m_pfnCallback;

};

 

现在,B可以调用A::RegisterEvent注册一个事件,并传递一个函数指针给A。

当A中发生了注册的事件时,这个函数指针会被回调到。

不过这种简单的做法适应性很差:

1、  不能支持单个事件的多个callback (可能有很多类都需要注册该事件,并在事件发生时依次被回调)

2、  不能支持多个事件的同时存在

3、  回调函数没有参数’

 

针对问题1,2,我们可以使用一个事件映射解决问题,做法如下:

typedef int EventId;

typedef void (callback*)();

typedef std::list<callback> CallbackList;

typedef std::map<EventId, CallbackList> CallbackMap;

现在这个数据结构就能够支持多个event同时存在,且每个event都可以支持多个回调函数了。

 

但是这种用法依旧很不方便,如果类B想要注册A上的一个事件,他需要定义一个 callback类型的函数,并把这个函数的地址传递给A。问题是,往往我们希望类B的回调函数在被调用到的时候,对类B中的数据和状态进行修改,而一个单独的函数,若想获得/修改B中的状态,则必须要与类B紧密耦合。(通过获取全局对象,或者Singleton的方式)

这种紧密耦合引发我们的思考,能否在Callback中同时包含类B的指针与类B的成员函数。

 

答案是肯定的:泛型回调就可以做到这一点。关于泛型回调(Generic callback)的信息,在Herb Sutter的Exceptional C++ Style的35条中有详细介绍。

 

一下比较简单的泛型回调的定义如下:

class callbackbase {

public:

virtual void operator()() const {};

virtual ~callbackbase() = 0 {};

};

 

template <class T>

class callback : public callbackbase {

public:

typedef void (T::*Func)();

callback(T& t, Func func) : object(t), f(func) {}     // 绑定到实际对象

void operator() () const { (object->*f)(); }              // 调用回调函数

private:

T* object;

Func f;

};

 

有了这种泛型回调类,我们就可以将类B的实例与B的成员回调函数绑定在一起注册到容器当中了,而不必再被如何在普通函数中修改B对象状态的问题所困扰了。不过回调函数的参数问题依旧。如果想支持参数,我们不得不对每一种参数类型做一个不同的typedef,像上面定义的这样 typedef void (T::*Func)();(如:typedef void (T::*Func)(int);)

一种解决方案是借助于Any(一种任意类型类)进行参数传递。

 

但是还有更完善的解决方案,不需要id号,也不需要泛型回调,Ogre采用Listener的方式实现的类间消息传递不仅可以支持单个类B对类A中某个事件的单次/多次注册,也可以支持类B、C、D对同一个事件的注册。而且可以完美的解决参数传递问题。

 

具体的方案如下:

class A {

public:

         class Listener {

         public:

                   virtual void OnMessageXX(int param1, float param2) = 0;

                   virtual void OnMessageYY(int param1, const std::string& param2) = 0;

};

 

void registerListener(Listener* obj) { m_lstListener.push_back(obj); }

void removeListener(Listener* obj)

{

         ListenerList::iterator itr = std::find(m_lstListener.begin(), m_lstListener.end(), obj);

         if (itr != m_lstListener.end())

                   m_lstListener.erase(itr);

}

 

private:

         typedef std::list<Listener*> ListenerList;

         ListenerList m_lstListeners;

};

 

有了以上定义,当类A收到某个消息XX之后,只需遍历m_lstListeners列表,调用所有列表成员的OnMessageXX即可。

 

而所有注册A的消息的类,都必须从A::Listener派生一个类,在它感兴趣的消息处理函数中做出相应处理,而对不感兴趣的消息,只需设为空函数即可。

 

一个简单的类B的定义如下:

 

 

class B {

public:

         friend class BListener;

         class BListener : public A::Listener {

         public:

                   BListener(B* pBInstance) : m_pBInstance(pBInstance) {}

                   virtual void OnMessageXX(int param1, float param2)

{ m_pBInstance->DoSomething(); }

                   virtual void OnMessageYY(int param1, const std::string& param2) {}

         private:

                   B* m_pBInstance;

};

 

explicit B(A* pAInstance) : m_pAInstance(pAInstance)

{

m_pListener(new BListener(this));

m_pAInstance->registerListener(m_pListener);

}

         ~B() { m_pAInstance->removeListener(m_pListener); delete m_pListener; }

 

void DoSomething();

 

private:

         BListener* m_pListener;

}

 

类B在创建自身实例时,接受一个A的指针(这是合理的,因为类B需要监听类A的消息,理应知道A的存在),并创建一个派生自A::Listener的监听者对象,并把自身的指针传递给该对象,以使得该监听者改变类B的状态,而后类B将创建好的监听者对象加入到A的监听者列表中。

在B进行析构的时候,需要从A中删除自己注册的监听者。而后将该对象释放。

 

这种做法的好处:

1、  类B(以及类C等)对类A实现了信息隐藏,类A不再关注任何需要监听它自身消息的其他类,只需关注其自身的状态。从而减低了类A与其他与之关联的类之间的耦合。(类A不必再费尽心机的去获取B的指针,不管是通过全局变量,还是Singleton,还是参数,还是类成员变量,都不再需要了,A只关心在Listener中定义好的一组接口即可)而且,如果有必要类B可以对同一个消息注册多次,且可以对同一消息有不同的反应(通过定义不同的BListener实现达到这一目的),只需在B不再需要监听相关消息时将所注册过的对象注销掉即可。

2、  由于1中所述,类A的实现无需关心类B的实现,因此类A的逻辑中不需要包含任何类B的方法调用,从而,类A的cpp文件中,无需包含类B的头文件,(可能还包括类C,D等等,此处类B指代需要根据类A状态而做出动作的类)从而降低编译时间,这是解耦合所带来的附加好处。

3、  同样是解耦合带来的好处:因为无需关注类B等等其他类的实现,类A的代码逻辑变得更加清晰,并且减少未来逻辑需求变更的改动所需要付出的代价(逻辑变更可能需要更改接口,需要增加状态判断,无论是调试时间还是编译时间都是不可忽视的代价)。



posted on 2009-06-03 17:36 李阳 阅读(385) 评论(0)  编辑 收藏 引用

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理