观察者模式与信号槽机制的探讨

观察者模式与信号槽机制的探讨

观察者模式与信号槽机制之间的关系,可以套用哲学里的世界观与方法论之间的关系。说人话? 观察者模式是设计模式中的一种,是理论基础,而信号槽机制是观察者模式的一种实现,是工程实践

观察者模式

维基百科对观察者模式的解释:观察者模式是软件设计模式的一种,在该种模式中,一个目标对象管理所有相依于它的观察者对象,并在它本身的状态改变时主动发出通知,通知所有的观察者。这通常通过调用各观察者所提供的方法来实现。该模式通常被用在实时事件处理系统中。(下图图片来源:维基百科)

Observer-pattern-class-diagram.png

观察者模式中的对象有:被观察者(或称为目标对象)观察者。一个被观察者可以被多个观察者所观察,观察者向被观察者注册观察,当被观察者的状态发生改变时,将通知所有注册了观察的观察者,观察者也可以断开对被观察者的观察,不再接受被观察者状态改变的通知。

观察者模式的形象解释是:一个用来展示价格数据(状态)的平台(被观察者),我(观察者)在这个平台上留下了手机号并开启了通知(注册观察),当价格发生改变时我会收到短信通知,同时其他在平台上留下了手机号并开启了通知的人(其他观察者)也能收到短信通知(被观察者状态改变,通知所有注册观察的观察者),当我退订了短信通知(断开观察),价格再发生改变时我将不再收到短信通知,而其他没有退订通知的人仍然还能收到通知。

信号槽机制

信号槽机制可以理解为一种遵循观察者模式的对象间通信手段,当某个对象的状态发生变化时,触发该对象的一个信号,另外一些对象的槽函数绑定了该信号,信号的触发将导致执行这些槽函数,完成对象之间的通信。

传统观察者模式实现方式

传统的观察者模式的实现需要开发人员/既关注观察者/还需要关注被观察者,通过/在/被观察者中/注册/观察者的状态改变回调函数/来实现/被观察者状态改变时通知观察者/的目的。(加入了分隔符方便阅读理解)

这种实现方式下,观察者模式中的观察者和被观察者是糅合在一起的,开发人员在编码时需要分散精力理顺概念和逻辑,然后才能写出符合要求的代码,即整个项目的架构通过要求开发者来保持,如果开发者的功力不够,很容易把项目的大架构写搓。

同时,传统的实现方式在观察者销毁时,会在被观察者中留下一个已经失效的函数指针,为了移除这个指针,开发人员往往需要手动断开观察或是在观察者的析构函数中断开观察(此步骤需要调用被观察者的函数),这一过程要求开发人员参与,在实际工程中有可能会出现开发人员忘记收尾的情景。

信号槽给我们带来了什么

信号槽实现了新的一套观察者模式,由信号槽框架维护关联性、处理收尾工作,整体呈现一个松耦合的架构,使得代码层级分明及模块独立,使用灵活方便,易于拓展及维护,开发人员能够聚焦于具体的业务而无需再关心观察者模式的框架,被观察者只关注状态变化,观察者只关注观察对象状态变化后的处理逻辑。这样在应用开发前的设计阶段,设计好架构并确定好信号和槽的对接后,项目将变成可分解的小需求,这时候就能投入人力并行开发(解除阻塞大大缩短开发周期)

信号槽也有弊端,信号槽框架维系着信号和槽之间的联系,响应速度不及回调函数指针,在极端的性能场景会受影响。同时槽函数内是允许继续发出信号的,如果使用不当,会造成信号槽死循环。另外,信号槽在运行时的调试具有一定的挑战性,也正是因为信号槽机制的松耦合特性,如果没有好好设计需要扔的信号和接收的槽,会导致“信号满天飞”和“一地鸡篮槽”,如果没有一个好的设计文档罗列出信号和槽的关联,不去深入读代码很难知道整个系统中某一个信号有什么槽函数在什么时候和它相连接了,这些对于问题的定位、新人加入项目组都有挑战。

(PS:Qt有一个非官方的调试工具gammaray,解决了信号槽松耦合特性下信号与槽割裂的问题,能够运行时实时观测信号槽的联系,事实上gammaray还实现了许多针对Qt的硬核调试工具)

信号槽实现

在这篇文章中,我不打算写手撸一个信号槽的过程,当然造一个轮子对理解和领悟能更上一层楼(也很有意思)。本着做工程的思想,如果有现有的成熟好用的轮子且经过时间和各种项目的检验,开源协议又友好,用这样的轮子自然比手撸一个还要后期投入去维护更能节省成本和专注于工程本身。

调研一下现有的成熟信号槽实现(事实上有不少的信号槽实现项目,但以下几个的名声和使用度最高):

信号槽实现 Qt boost libsigc++ sigslot
返回值 直联模式支持
信号实体 成员函数 对象 对象 对象
线程安全 有(指定参数) 有(独立实现) 有(指定参数)
类型安全 是,运行期检查 是,编译期检查 是,编译期检查 是,编译期检查
参数列表 允许 信号≥槽 要求一致 要求一致 要求一致
实现方式 moc预编译 C++ C++ C++

Qt的核心思想是它最引以为豪的信号槽机制,Qt的信号槽功能强大容错性极好,有内省机制,有Qt完善友好的界面工具支持,但是Qt的信号槽机制需要依赖于它自己的moc预编译,需要有Qt的一套编译工具链才能工作。boost的信号槽boost::signals依赖于其他的boost模块,需要引入整个boost库,线程安全的信号槽独立实现在boost::signals2中,太过厚重,多线程替换不够友好。libsigc++的开源协议是LGPL,需要做动态链接库隔离或进程隔离,对商用不够友好。sigslot库是head-only的,而且遵循公共开放开源协议(无任何限制的协议),对任何形式的使用都十分的友好,OK就是它了!

SignalSlot——封装一下sigslot

修改sigslot通过编译

如果直接集成sigslot编译,会出现类似如下的错误。

1
2
3
4
5
6
7
8
1>------ 已启动生成: 项目: SignalSlot, 配置: Debug Win32 ------
1> SignalSlotTest.cpp
1> signalslot\sigslot-1-0-0\sigslot\sigslot.h(419): warning C4346: “const_iterator”: 依赖名称不是类型
1> signalslot\sigslot-1-0-0\sigslot\sigslot.h(419): note: 用“typename”为前缀来表示类型
1> signalslot\sigslot-1-0-0\sigslot\sigslot.h(476): note: 参见对正在编译的类 模板 实例化“sigslot::has_slots<mt_policy>”的引用
1> signalslot\sigslot-1-0-0\sigslot\sigslot.h(419): error C2061: 语法错误: 标识符“const_iterator”
1> signalslot\sigslot-1-0-0\sigslot\sigslot.h(419): error C2238: 意外的标记位于“;”之前
========== 生成: 成功 0 个,失败 1 个,最新 0 个,跳过 0 个 ==========

问题发生在第419行。

1
typedef sender_set::const_iterator const_iterator;

只是一个简单的语法错误,意义是准确的,对于非模板是能通过编译的。

但是此处应用于模板中,sender_set是一个模板类类型。

1
typedef std::set<_signal_base<mt_policy> *> sender_set;

根据错误提示,我们需要使用typename前缀来表示模板类型,修改该句即可。

1
typedef typename sender_set::const_iterator const_iterator;

更通用的信号模板类

sigslot的信号模板类型是signalx,x是槽函数参数的个数,同时我们还会在模板参数列表中将所有的参数类型依次列出。事实上,我们在将参数类型依次列出时,就能够得知槽函数的参数个数了,没有必要再多此一举在信号模板类型中加一个x。因此在封装时把它封装起来吧。

sigslot仅支持最多8个槽函数参数,我们可以拓展sigslot的代码来支持更多的槽函数参数个数,但过多的参数是一种代码坏味道,会降低代码的质量,影响可测试性等等。我们一般限定一个函数的参数个数不超过5个,8个完全足够我们使用了。我们定义SignalTemplate模板类,并采用模板特例化的方法,特例化有0~8个槽函数参数的情景,并用静态断言禁止槽函数参数个数多于8个。由于8种特例化的过程十分类似,我们用宏来简化重复的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
template<const unsigned int argsCount, class ...Args>
class SignalTemplate {
static_assert(sizeof...(Args) <= 8,
"SignalTemplate: only <= 8 args are supported.");
};

#define _SIGNAL_TEMPLATE_FUNCTION_IMPLEMENT_ \
inline void Emit(const Args& ...args) \
{ \
signal.emit(args...); \
} \
template<typename DestObj> \
inline void Connect(DestObj* object, void(DestObj::*slot)(Args...)) \
{ \
signal.connect(object, slot); \
} \
template<typename DestObj> \
inline void Disconnect(DestObj* object) \
{ \
signal.disconnect(object); \
}
#define _SIGNAL_TEMPLATE_VARIABLE_IMPLEMENT_(argsCount) \
sigslot::signal##argsCount<Args...> signal;

#define _SIGNAL_TEMPLATE_IMPLEMENT_(n) \
template<class ...Args> \
class SignalTemplate<n, Args...> { \
public: \
_SIGNAL_TEMPLATE_FUNCTION_IMPLEMENT_ \
private: \
_SIGNAL_TEMPLATE_VARIABLE_IMPLEMENT_(n) \
};

_SIGNAL_TEMPLATE_IMPLEMENT_(0)
_SIGNAL_TEMPLATE_IMPLEMENT_(1)
_SIGNAL_TEMPLATE_IMPLEMENT_(2)
_SIGNAL_TEMPLATE_IMPLEMENT_(3)
_SIGNAL_TEMPLATE_IMPLEMENT_(4)
_SIGNAL_TEMPLATE_IMPLEMENT_(5)
_SIGNAL_TEMPLATE_IMPLEMENT_(6)
_SIGNAL_TEMPLATE_IMPLEMENT_(7)
_SIGNAL_TEMPLATE_IMPLEMENT_(8)

Signal类含有SignalTemplate类,我们用sizeof…(Args)获取模板类型列表个数,来对应特例化的SignalTemplate类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
template<class ...Args>
class Signal {
public:
Signal() = default;
virtual ~Signal() = default;
// COPY & MOVE construction are not allowed.
Signal(Signal const&) = delete;
Signal(Signal &&) = delete;
Signal& operator=(Signal const&) = delete;
Signal& operator=(Signal &&) = delete;

inline void Emit(const Args& ...args)
{
st.Emit(args...);
}

template<typename DestObj>
inline void Connect(DestObj* object, void(DestObj::*slot)(Args...))
{
st.Connect(object, slot);
}

template<typename DestObj>
inline void Disconnect(DestObj* object)
{
st.Disconnect(object);
}

private:
SignalTemplate<sizeof...(Args), Args...> st;
};

这样最终对外暴露的Signal类就只有模板参数列表了,不论x是多少,都采用通用的Signal类。

保持风格统一

为了保持风格的统一,我们再来包装一下sigslot::has_slots基类。有槽函数的类需要继承自sigslot的has_slots基类,在析构时,has_slots会自动断开所有的连接槽,这个过程由has_slots的析构函数来完成。

1
2
3
4
5
6
7
8
9
10
class Object : public sigslot::has_slots<> {
public:
Object() = default;
~Object() override = default;
// COPY & MOVE construction are not allowed.
Object(Object const&) = delete;
Object(Object &&) = delete;
Object& operator=(Object const&) = delete;
Object& operator=(Object &&) = delete;
};

如上,我们包装了一个Object类继承自sigslot::has_slots<>,事实上这个类并没有做什么:-)

我们在Connect和Disconnect内使用静态断言来确保模板类型继承自Object类,它帮助我们在静态编译期确保逻辑正确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template<class ...Args>
class Signal {
...

template<typename DestObj>
inline void Connect(DestObj* object, void(DestObj::*slot)(Args...))
{
static_assert(std::is_base_of<Object, DestObj>(),
"Template is not base of Object");
st.Connect(object, slot);
}

template<typename DestObj>
inline void Disconnect(DestObj* object)
{
static_assert(std::is_base_of<Object, DestObj>(),
"Template is not base of Object");
st.Disconnect(object);
}

...
}

线程安全

sigslot原生支持多线程和线程安全,通过在包含头文件前定义宏SIGSLOT_DEFAULT_MT_POLICY来使用不同的线程模式。定义该宏需要知道不同的线程模式,我们简单包装一下它。

默认情况下,我们使用单线程模式,信号槽不保证线程安全,但不需要加锁,速度快。 如果定义了SIGNAL_SLOT_MULTI_THREADS_SINGLE_MUTEX宏,我们使用多线程单一锁模式,线程安全,在全局只使用一个锁,所有信号共用同一个锁。 如果定义了SIGNAL_SLOT_MULTI_THREADS_MULTI_MUTEXS宏,我们使用多线程多锁模式,线程安全,针对每个信号使用独立的锁。

1
2
3
4
5
6
7
#if defined SIGNAL_SLOT_MULTI_THREADS_SINGLE_MUTEX
#define SIGSLOT_DEFAULT_MT_POLICY multi_threaded_global
#elif defined SIGNAL_SLOT_MULTI_THREADS_MULTI_MUTEXS
#define SIGSLOT_DEFAULT_MT_POLICY multi_threaded_local
#else
#define SIGSLOT_DEFAULT_MT_POLICY single_threaded
#endif

槽函数内的执行过程是不保证线程安全的,比如一个槽函数连接了两个在不同线程的信号,两个信号被先后触发,几乎同时运行了该槽函数,如果槽函数对类内数据有操作,此时有线程安全问题。

我们可以使用SIGNAL_SLOT_LOCKSIGNAL_SLOT_UNLOCK宏来对访问加解锁确保对类内数据操作是安全有序的。 我们也可以使用SIGNAL_SLOT_LOCK_BLOCK宏,该宏替换后的lock_block采用RAII特性实现了一个生命周期锁,实例化(构造)时加锁,生命周期结束(析构)时解锁。

1
2
3
4
5
6
7
8
9
// --------------- !!!! NB !!!! ---------------
// Thread safety is not guaranteed between slots,
// but you can use:
// "SIGNAL_SLOT_LOCK;/SIGNAL_SLOT_UNLOCK;" or
// "SIGNAL_SLOT_LOCK_BLOCK;"(RAII)
// in slot function to ensure multi-thread safety.
#define SIGNAL_SLOT_LOCK lock()
#define SIGNAL_SLOT_UNLOCK unlock()
#define SIGNAL_SLOT_LOCK_BLOCK lock_block<SIGSLOT_DEFAULT_MT_POLICY> lock(this)

全局的Emit&Connect&Disconnect函数

sigslot的connect、disconnect、emit函数都是写在信号类(signalx)里的,我们在使用时是调用信号对象实例的函数来进行操作。如果使用过Qt,我们会发现Qt的这些函数是写在QObject基类里的,我们在使用时是直接在类中调用connect&disconnect函数和使用emit宏来完成操作。

这两种方式很难说哪种用法更好。sigslot的信号实体是一个对象,因此写在了信号类中;Qt的信号实体是一个成员函数,需要通过moc预编译器来生成真正的函数定义,因此写在了QObject基类中。

而从我的观点出发,我希望将这些函数直接放在全局空间(或直接放在SignalSlot命名空间)内,而不是在某一个类中。

正向论证: 信号槽机制的很大一个功能点是解耦,信号和槽是隔离的,对于使用者而言,信号不需要关注有什么槽连接了自己,槽也只关注自己的实现,而Connect、Disconnect、Emit函数是信号与槽间的桥梁,割裂的关系通过这三个函数联系了起来,因此无论把它们放在哪方的类中都不太合适。

反向论证: 如果我们直接采用sigslot的方式,在使用时总是需要拎着信号到处调用函数,虽然本质上是解耦的,但编码上看起来像是总由信号在主导一切,直观的解耦感受不够强烈。 如果我们学习Qt放在QObject里,假设有三个类A、B、C,A中有一个信号,C中有一个槽,我们能在B类中去将C类的槽连接到A类中的信号上,调用的是B类中的connect函数,虽然是一个static函数,但是这个过程和B类有什么关系呢,放在QObject里就会导致这种奇怪的逻辑,如果把connect脱离出来放在信号槽之外的全局空间,逻辑就不奇怪了,实际架构中B类有可能是一个中间人的角色,是有实际意义的。

因此,放过它们吧,直接放在全局空间里。实在有冲突,整个SignalSlot命名空间包装一下。

为了实现全局的Emit&Connect&Disconnect函数,我们在Signal类中定义友元函数,并将原先在Signal类中对sigslot包装的Emit、Connect、Disconnect函数调整为private属性,然后定义全局的Emit、Connect、Disconnect函数,在全局的函数中转调Signal内的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
template<class ...Args>
class Signal {
...

private:
template<class ...Args> friend void Emit(
Signal<Args...>& signal, const Args& ...args);
template<typename DestObj, class ...Args> friend void Connect(
Signal<Args...>& signal, DestObj* object, void(DestObj::*slot)(Args...));
template<typename DestObj, class ...Args> friend void Disconnect(
Signal<Args...>& signal, DestObj* object);

...
}

template<class ...Args> void Emit(
Signal<Args...>& signal, const Args& ...args)
{
signal.Emit(args...);
}

template<typename DestObj, class ...Args> void Connect(
Signal<Args...>& signal, DestObj* object, void(DestObj::*slot)(Args...))
{
signal.Connect(object, slot);
}

template<typename DestObj, class ...Args> void Disconnect(
Signal<Args...>& signal, DestObj* object)
{
signal.Disconnect(object);
}

性能损耗

我们对sigslot封装了一层,内部的复杂度提升了,但是对外接口的逻辑清晰了些,也符合了我项目中的编程风格。我们把性能优化任务留给了编译器,让编译器帮我们“擦屁股”。在Release模式下,这层封装的转调基本都会被优化掉,几乎没有性能损失。

SignalSlot——完整代码

SignalSlot.hpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
////////////////////////////////////////////////////////////////////////////////
//
// MIT License
//
// Copyright (c) 2021 kongdeyou(https://tis.ac.cn/blog/author/kongdeyou/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
////////////////////////////////////////////////////////////////////////////////

#ifndef _SIGNAL_SLOT_HPP_
#define _SIGNAL_SLOT_HPP_

#if defined SIGNAL_SLOT_MULTI_THREADS_SINGLE_MUTEX
#define SIGSLOT_DEFAULT_MT_POLICY multi_threaded_global
#elif defined SIGNAL_SLOT_MULTI_THREADS_MULTI_MUTEXS
#define SIGSLOT_DEFAULT_MT_POLICY multi_threaded_local
#else
#define SIGSLOT_DEFAULT_MT_POLICY single_threaded
#endif
#include "sigslot-1-0-0/sigslot/sigslot.h"
// --------------- !!!! NB !!!! ---------------
// Thread safety is not guaranteed between slots,
// but you can use:
// "SIGNAL_SLOT_LOCK;/SIGNAL_SLOT_UNLOCK;" or
// "SIGNAL_SLOT_LOCK_BLOCK;"(RAII)
// in slot function to ensure multi-thread safety.
#define SIGNAL_SLOT_LOCK lock()
#define SIGNAL_SLOT_UNLOCK unlock()
#define SIGNAL_SLOT_LOCK_BLOCK lock_block<SIGSLOT_DEFAULT_MT_POLICY> lock(this)

class Object : public sigslot::has_slots<> {
public:
Object() = default;
~Object() override = default;
// COPY & MOVE construction are not allowed.
Object(Object const&) = delete;
Object(Object &&) = delete;
Object& operator=(Object const&) = delete;
Object& operator=(Object &&) = delete;
};

template<const unsigned int argsCount, class ...Args>
class SignalTemplate {
static_assert(sizeof...(Args) <= 8,
"SignalTemplate: only <= 8 args are supported.");
};

#define _SIGNAL_TEMPLATE_FUNCTION_IMPLEMENT_ \
inline void Emit(const Args& ...args) \
{ \
signal.emit(args...); \
} \
template<typename DestObj> \
inline void Connect(DestObj* object, void(DestObj::*slot)(Args...)) \
{ \
signal.connect(object, slot); \
} \
template<typename DestObj> \
inline void Disconnect(DestObj* object) \
{ \
signal.disconnect(object); \
}
#define _SIGNAL_TEMPLATE_VARIABLE_IMPLEMENT_(argsCount) \
sigslot::signal##argsCount<Args...> signal;

#define _SIGNAL_TEMPLATE_IMPLEMENT_(n) \
template<class ...Args> \
class SignalTemplate<n, Args...> { \
public: \
_SIGNAL_TEMPLATE_FUNCTION_IMPLEMENT_ \
private: \
_SIGNAL_TEMPLATE_VARIABLE_IMPLEMENT_(n) \
};

_SIGNAL_TEMPLATE_IMPLEMENT_(0)
_SIGNAL_TEMPLATE_IMPLEMENT_(1)
_SIGNAL_TEMPLATE_IMPLEMENT_(2)
_SIGNAL_TEMPLATE_IMPLEMENT_(3)
_SIGNAL_TEMPLATE_IMPLEMENT_(4)
_SIGNAL_TEMPLATE_IMPLEMENT_(5)
_SIGNAL_TEMPLATE_IMPLEMENT_(6)
_SIGNAL_TEMPLATE_IMPLEMENT_(7)
_SIGNAL_TEMPLATE_IMPLEMENT_(8)

template<class ...Args>
class Signal {
public:
Signal() = default;
virtual ~Signal() = default;
// COPY & MOVE construction are not allowed.
Signal(Signal const&) = delete;
Signal(Signal &&) = delete;
Signal& operator=(Signal const&) = delete;
Signal& operator=(Signal &&) = delete;

private:
template<class ...Args> friend void Emit(
Signal<Args...>& signal, const Args& ...args);
template<typename DestObj, class ...Args> friend void Connect(
Signal<Args...>& signal, DestObj* object, void(DestObj::*slot)(Args...));
template<typename DestObj, class ...Args> friend void Disconnect(
Signal<Args...>& signal, DestObj* object);

inline void Emit(const Args& ...args)
{
st.Emit(args...);
}

template<typename DestObj>
inline void Connect(DestObj* object, void(DestObj::*slot)(Args...))
{
static_assert(std::is_base_of<Object, DestObj>(),
"Template is not base of Object");
st.Connect(object, slot);
}

template<typename DestObj>
inline void Disconnect(DestObj* object)
{
static_assert(std::is_base_of<Object, DestObj>(),
"Template is not base of Object");
st.Disconnect(object);
}

SignalTemplate<sizeof...(Args), Args...> st;
};

template<class ...Args> void Emit(
Signal<Args...>& signal, const Args& ...args)
{
signal.Emit(args...);
}

template<typename DestObj, class ...Args> void Connect(
Signal<Args...>& signal, DestObj* object, void(DestObj::*slot)(Args...))
{
signal.Connect(object, slot);
}

template<typename DestObj, class ...Args> void Disconnect(
Signal<Args...>& signal, DestObj* object)
{
signal.Disconnect(object);
}

#endif

看个例子:智能家居场景

假设我们有一个智能灯,家里有一个智能家居家庭控制中心,控制中心能实时展示当前智能灯的状态(开/关),通过控制中心我们能够控制智能灯的状态,我们还有一个手机端应用,手机端应用上也能实时展示和控制智能灯的状态。我们来构建这样的智能家居场景的服务端。

首先是智能家居场景中的设备,这里只有一个智能灯。我们定义一个House类,存储智能灯的状态lightStatus,智能灯状态发生改变时扔出信号LightStatus,同时有接口函数SetLightStatusGetLightStatus。调用SetLightStatus函数会改变智能灯的状态并扔出信号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class House {
public:
Signal<bool> LightStatus;

void SetLightStatus(bool on)
{
lightStatus = on;
... // 控制智能灯改变状态
Emit(LightStatus, lightStatus);
}

bool GetLightStatus() const
{
return lightStatus;
}

private:
bool lightStatus = false;
};

然后我们看看处理手机端的代码,手机端处理中要响应智能灯的状态改变,当智能灯状态改变后通知手机端去更新手机端上智能灯状态的显示。因此,我们在Mobile中写一个LightStatusChanged槽函数实现通知手机端更新智能灯状态的显示,在构造函数中将该槽函数连接到house.LightStatus信号上。此外,我们还写了一个SetLightStatus函数,我们在后面会在接收到移动端请求改变智能灯状态的数据后,调用该函数,该函数最终会调用House类中的SetLightStatus真正改变智能灯的状态并扔出信号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Mobile : public Object {
public:
explicit Mobile(House& house)
: house(house)
{
Connect(house.LightStatus, this, &Mobile::LightStatusChanged);
// 智能家居手机端初始化
...
}

void LightStatusChanged(bool on)
{
// 更新手机端上智能灯状态的显示
...
}

void SetLightStatus(bool on)
{
house.SetLightStatus(on);
}

private:
House& house;
};

控制中心的基本的操作框架也是类似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Center : public Object {
public:
explicit Center(House& house)
: house(house)
{
Connect(house.LightStatus, this, &Center::LightStatusChanged);
// 智能家居家庭控制中心初始化
...
}

void LightStatusChanged(bool on)
{
// 更新家庭控制中心显示面板上的智能灯状态的显示
...
}

void SetLightStatus(bool on)
{
house.SetLightStatus(on);
}

private:
House& house;
};

最后是消息循环,我们循环去获取手机端和控制中心是否有控制数据传输过来。如果有改变智能灯状态的控制数据,我们会调用相应的mobile或center设置智能灯的状态,然后house中的LightStatus信号会被发送出来,mobile和house都连接了该信号,此时会运行mobile和house中的LightStatusChanged槽函数,更新各自的状态显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int main()
{
House house;

Mobile mobile(house);
Center center(house);

...

while (true) {
//////////////////////////////////////////////////
bool mobileChangedLightStatus = false;
bool mobileLightStatus = false;

// 获取手机端数据
ReceiveMobileData(&mobileChangedLightStatus, &mobileLightStatus);

if (mobileChangedLightStatus) {
mobile.SetLightStatus(mobileLightStatus);
}

//////////////////////////////////////////////////
bool centerChangedLightStatus = false;
bool centerLightStatus = false;

// 获取家庭控制中心数据
ReceiveCenterData(&centerChangedLightStatus, &centerLightStatus);

if (centerChangedLightStatus) {
center.SetLightStatus(centerLightStatus);
}

//////////////////////////////////////////////////
...
}
}

协议

本文以上内容遵循CC BY-ND 4.0协议,署名-禁止演绎。

本文中所提供的源代码遵循MIT开源协议。 代码托管于:https://github.com/KondeU/SignalSlot
代码仓中含有演示示例代码。

转载请注明出处:https://tis.ac.cn/blog/kongdeyou/观察者模式与信号槽机制的探讨/
并署名:kongdeyou(https://tis.ac.cn/blog/author/kongdeyou/)

后记

2021年1月1日 周五 晴

本命年第一天去拔智齿:-(

完稿于2021年1月3日22:22

原始链接:https://blog.kdyx.net/blog/kongdeyou/%E8%A7%82%E5%AF%9F%E8%80%85%E6%A8%A1%E5%BC%8F%E4%B8%8E%E4%BF%A1%E5%8F%B7%E6%A7%BD%E6%9C%BA%E5%88%B6%E7%9A%84%E6%8E%A2%E8%AE%A8/

版权声明: "CC BY-NC-ND 4.0" 署名-不可商用-禁止演绎 转载请注明原文链接及作者信息,侵权必究。

×

喜欢或有帮助?赞赏下作者呗!