对象之间的关系:依赖,关联,聚合,组合和继承,耦合关系依次加强。
12.2 消息总线关键技术
12.2.1 通用的消息定义
消息类型定义:主题+泛型函数的签名
主题可以是字符串或整型等其他类型;这里将泛型函数定为std::function<R(Args...)>,R表示函数返回值,Args表示函数可变入参。泛型函数能表示所有的可调用对象,表示通用的消息格式。
如下将可调用对象转换为std::function,用到特性萃取function_traits:
#include<functional>
#include<tuple>
template<typename T>
struct function_traits;
// 普通函数
template<typename Ret,typename... Args>
struct function_traits<Ret(Args...)>
{
public:
enum { arity = sizeof...(Args) };
typedef Ret function_type(Args...);
typedef Ret return_type;
using stl_function_type = std::function<function_type>;
typedef Ret(*pointer)(Args...);
template<size_t I>
struct args {
static_assert(I < arity, "index is out of range");
using type = typename std::tuple_element < I, std::tuple<Args...>>::type;
};
};
// 函数指针
template<typename Ret, typename... Args>
struct function_traits<Ret(*)(Args...)> : function_traits<Ret(Args...)>{};
// std::function
template<typename Ret, typename... Args>
struct function_traits<std::function<Ret(Args...)>> : function_traits<Ret(Args...)>{};
// member function
#define FUNCTION_TRAITS(...)\
template<typename ReturnType, typename ClassType, typename ... Args>\
struct function_traits<ReturnType(ClassType::*)(Args...) __VA_ARGS__> : function_traits<ReturnType(Args...)>{}; \
FUNCTION_TRAITS()
FUNCTION_TRAITS(const)
FUNCTION_TRAITS(volatile)
FUNCTION_TRAITS(const volatile)
// 函数对象
template<typename Callable>
struct function_traits :function_traits<decltype(&(Callable::operator())>{};
template<typename Function>
typename function_traits<Function>::stl_function_type to_function(const Function & lambda)
{
return static_cast<function_traits<Function>::stl_function_type>(lambda);
}
template<typename Function>
typename function_traits<Function>::stl_function_type to_function(Function && lambda)
{
return static_cast<function_traits<Function>::stl_function_type(std::forward<Function>(lambda));
}
template<typename Function>
typename function_traits<Function>::pointer to_function_pointer(const Function & lambda)
{
return static_cast<typename function_traits<Function>::pointer>(lambda);
}
测试代码如下:
auto f = to_function([](int i){return i;});
std::function<int(int)> f1 = [](int i){return i;};
if (std::is_same<deltype(f), deltype(1)>::value) {
cout << "same" << endl;
}
输出same,可以看到,to_function会将lambda表达式(可调用对象)转换为std::function。
12.2.2 消息的注册
消息的注册是告诉总线该对象对某种消息感兴趣,希望收到某种主题和类型的消息。
消息类型:topic + std::function<R(Args...)>。
对于不同的Args和R,消息类型是不同的。为了保存不同的消息对象,需要用Any做类型擦除。
最后,消息总线内部使用std::unordered_multimap<string, Any> m_map来保存消息,键为topic + typeid的字符串,值为消息对象。
消息注册如下:
// 注册可调用对象
template<typename T>
void Attach(const string& strTopic, const F& f)
{
auto func = to_function(f);
Add(strTopic, std::move(func));
}
template<typename T>
void Add(const string& strTopic, F&& f)
{
// typeid 运算符用来获取一个表达式的类型信息
string strKey = strTopic + typeid(F).name();
// Any类型擦除
m_map.emplace(std::move(strKey), f);
}
先将可调用对象转换为了std::function,后将std::function通过类型擦除转换为Any,最后将消息保存在map里。
12.2.3 消息分发
看下消息总线如何发送消息的:
template<typename R, typename... Args>
void sendReq(Args&&... args, const string& strTopic = "")
{
using function_type = std::function<R(Args...)>; // 1
string strMsgType = strTopic + typeid(function_type).name(); // 2
auto range = m_map.equal_range(strMsgType); // 3
for (Iterator it = range.first(); it != range.second; it++) // 4
{
auto f = it->second.AnyCast<function_type>(); // 5
f(std::forward<Args>(args)...); // 6
}
}
函数的第1行根据形参生成具体的消息std::function<R(Args...)>;第2行获取主题+表达式类型的字符串,用来查找注册了该消息的对象;第3行查找哪些对象注册了该消息;第5行将被擦除类型恢复;第6行通知接收者处理消息。
12.2.4 消息总线的设计思想
消息总线融合了观察者模式和中介者模式。观察者模式是用来维护主题目标,在适当的时候向观察者广播消息;中介者模式用来降低主题目标与各个观察者之间的耦合性,把观察者中主题目标与观察者的1对多关系转移为中介者与观察者的1对多关系,使得主题目标与观察者不再依赖,耦合性降低。另外,消息是所有类型的可调用对象,没有对象之间的直接调用,没有继承,也会降低耦合性。
先看下观察者模式和中介者模式:
(1)观察者模式
观察者(Observer)模式的定义:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式,它是对象行为型模式。
观察者模式是一种对象行为型模式,其主要优点如下。
1 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。符合依赖倒置原则。
2 目标与观察者之间建立了一套触发机制。
它的主要缺点如下。
1 目标与观察者之间的依赖关系并没有完全解除。
2 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率。
类图如下:
(2)中介者模式
中介者(Mediator)模式的定义:定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。
中介者模式是一种对象行为型模式,其主要优点如下。
1 类之间各司其职,符合迪米特法则。
2 降低了对象之间的耦合性,使得对象易于独立地被复用。
3 将对象间的一对多关联转变为一对一的关联,提高系统的灵活性,使得系统易于维护和扩展。
其主要缺点是:中介者模式将原本多个对象直接的相互依赖变成了中介者和多个同事类的依赖关系。当同事类越多时,中介者就会越臃肿,变得复杂且难以维护。
中介者模式包含以下主要角色。
1 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
2 具体中介者(Concrete Mediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
3 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
4 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。
类图如下:
消息总线的时序图如下:
图12-1 消息总线时序图
从图12-1可以看到,Subject和Observer对象之间没有任何联系,它们是通过消息总线发生联系的。
(1)观察者向消息总线注册消息,消息类型定义:主题+泛型函数的签名。
topic + std::function<R(Args...)>
(2)消息总线保存观察者注册的消息。
(3)主题对象向消息总线发送消息,消息类型定义:主题+泛型函数的签名。
topic + std::function<R(Args...)>
(4)消息总线根据主题对象发送的消息查找对该消息感兴趣的观察者。
(5)消息总线向观察者广播消息。
(6)观察者处理消息。
消息总线的类图如下:
图12-2 消息总线类图
类图中的几个对象:
(1)NonCopyable:防止类被复制。
(2)MessageBus:消息总线,维护系统之所有的消息,具备注册消息,分发消息和移除消息的功能。
(3)Observer:观察者对象,接收并处理来自消息总线的消息。
(4)Obsever_Function:消息,本质是可调用对象,实际是观察者对象的某个函数。
(5)Subject:主题目标对象,向消息总线发送消息。
分析类图,如何应用了观察者和中介者模式:
(1)主题目标(Subject)与观察者(Observer),观察者注册感兴趣的消息类型,当主题目标发送一个消息时,会通过消息总线告知所有感兴趣的观察者,观察者收到消息后处理消息。==>观察者模式
(2)主题目标(Subject)里不再依赖多个观察者(Observer),而是消息总线里依赖多个观察者,从而使得主题目标与观察者没有关系,降低了耦合度。==>中介者模式
最后来看下消息总线与QT中信号与槽的区别:
(1)消息总线是观察者模式与中介者模式的融合;信号槽本质上是观察者模式。
(2)消息总线可以接收所有类型的函数注册(因为类型擦除);信号槽,一种信号只能接收特定函数注册。