from: http://www.windameister.org/blog/2008/12/21/ogre-message-exchange-with-low-coupling/
关于低耦合的消息传递,实现的方式有很多,哪种方法更好与具体的使用环境有关,本文使用试错的方法,逐步探索达成这一目的具体方式,并理解实现方式背后的原因。
面向对象的系统当中,不可避免的有大量的类间消息传递的需求:一个类需要通知另一个或几个类做些什么。
这种类间消息传递,简单的说,就是调用其他类的方法。
如下:
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的代码逻辑变得更加清晰,并且减少未来逻辑需求变更的改动所需要付出的代价(逻辑变更可能需要更改接口,需要增加状态判断,无论是调试时间还是编译时间都是不可忽视的代价)。
No comments:
Post a Comment