在C++面向对象编程中,观察者模式是大家熟知的实现异步编程的一种模式。
观察者模式定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。如下图所示:
观察者模式提供了一种对象设计,让主题和观察者之间松耦合:
- 运行时我们用新的观察者取代现有的观察者,主题不会受到影响。
- 有新类型的观察者出现时,主题代码不需要更改。
- 可以轻易地独立使用或复用主题或者观察者。
- 改变主题或者观察者的任何一方都不会影响另一方。
UML 关系图:
传统的这种观察者模式虽然是松耦合的,但是observer和subject之间还是互相关联的关系。实现observer的代码文件,必须包含subject的定义头文件;实现subject的代码文件,必须包含observer的定义头文件。由于彼此有稀松的互相“包含”关系, 使得观察者模式的使用得到了很大限制。有的情况下,编译都难通过,浪费很多时间。这个问题是是典型的“两个类相互包含的问题”。
问题描述:
A类包含B类的实例,而B类也包含A类的实例
(你可以将A当成subject,而B当成observer)
错误的解法
A文件包含B,而B文件又包含A文件,这样就形成死循环 (在本问你中,实现observer的代码文件,必须包含subject的定义头文件;实现subject的代码文件,必须包含observer的定义头文件。subject和observer彼此有稀松的互相“包含”关系, 这使得观察者模式的使用得到了很大限制。)
#include "B.h"
class A
{
int i;
B b;
};
#include "A.h"
class B
{
int i;
A a;
};
l 常规的解决办法以及注意事项
1)主函数只需要包含b.h 就可以,因为b.h中包含了a.h
2)a.h中不需要包含b.h,但要声明class b,在避免死循环的同时也成功引用了b。
3)包含class b 而没有包含头文件 "b.h",这样只能声明 b类型的指针!!!!而不能实例化!!!!
有没有更好的办法呢?当然有。本文就是要解决这个问题。
从理论上将,subject对象是不需要过度关注observer对象的,也就是subject不需要去关联observer类型。只需要两步:
(1)observer对象向subject对象发起注册,可以只传递一个函数指针。
(2)当subject有事情需要通知时,可以调用上步的函数。
这种方式下,observer 和subject之间的耦合性被进一步降低。subject不再刻意关注observer对象,其组成也不再关联一个observer对象指针或一个observer对象指针链表。
我们用boost::bind这个神兵利器来解决这个问题。
第一步,在observer对象中初始化的时候定义一个boost::function函数对象,如 boost::function<void(NotifyParams)> State_Update_Func;
第二步,将该boost::function对象绑定到本observer对象对接收到subject通知后的处理函数:
State_Update_Func =boost::bind(&Observer::Notification_Process_Func,this,_1);
第三步,进行注册:
m_Subject.Subscribe ( State_Update_Func);
第四步,实现subject的注册函数:
inline void Subscribe( boost::function<void(NotifyParams)> Listener) { m_Status_Update_Func=Listener;};
第四步,实现subject的通知函数:
void CSubject::NotifySubscriber()
{
if ( m_Status_Update_Func )
m_Status_Update_Func(m_NotifyParams);
}
这样,当subject调用NotifySubscriber时,observer的处理函数就会被自动调用,实现了异步编程。
一个问题讨论:
(1)有人认为,通过boost::bind绑定函数对象时,如果被绑定的函数为成员函数,成员函数的参数最好都是传值,而不要传递引用或指针。
例如,假设在多线程环境下,我们要根据IP异步的查询一个设备
mp_context->executeInThread(boost::bind(&CMbxdHandler::queryDeviceAsync, this,strIP);
查询函数最好参数为const string ipaddress,而不是const string &ipaddress
void CQueryHandler::queryDeviceAsync(const string ipaddress)
这样的理由在于,在多线程调用环境下,当CQueryHandler被析构时,有可能strIP已经被析构,此时再去使用它的引用,将导致core dump.
其实是多虑了,boost::bind已经帮你考虑了这个问题。boost::bind会自动将每个传入的参数对象拷贝一份,只有当你显示将参数设置为boost:ref时,参数才可能是引用传递。下面写一段代码来验证这个问题:
#include "boost/ref.hpp"
#include "boost/function.hpp"
#include "boost/bind.hpp"
#include <string>
#include <iostream>
using namespace std;
class A
{
public:
A(const string& s)
{
_name = s;
}
A(const A& a)
{
_name = a._name;
cout << "copy function" << "\n";
}
void set(const string& s)
{
_name = s;
}
string get() const
{
return _name;
}
private:
string _name;
};
void show_me(const A& a)
{
cout << "show me: " << a.get() << "\n";
}
int main()
{
A* p_a = new A("Jack");
cout << "before bind" << "\n";
boost::function<void ()> f = boost::bind(&show_me, *p_a);
cout << "after bind" << "\n";
p_a->set("Tom");
delete p_a;
f();
f();
f();
}
可以看到,运行结果在bind之前和之后都是内部拷贝的 “Jack”。而不是通过传递引用,打印出“tom”