Ogre源码剖析:如何实现低耦合的类间消息传递机制

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

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

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

如下:

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的代码逻辑变得更加清晰,并且减少未来逻辑需求变更的改动所需要付出的代价(逻辑变更可能需要更改接口,需要增加状态判断,无论是调试时间还是编译时间都是不可忽视的代价)。

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

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

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

如下:

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的代码逻辑变得更加清晰,并且减少未来逻辑需求变更的改动所需要付出的代价(逻辑变更可能需要更改接口,需要增加状态判断,无论是调试时间还是编译时间都是不可忽视的代价)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值