易于使用的观察者模式实现(无需继承)






4.95/5 (19投票s)
2004年1月27日
7分钟阅读

95408

1365
观察者模式在一个漂亮的模板模型中实现,易于使用,因为它不需要经典的继承,并且可以轻松解耦主题和观察者。
引言
观察者模式(Gamma)是许多实现的一部分,并且是最常用的模式之一。然而,实现灵活的观察者模式并非易事,而且大多数时候会在主题和观察者之间产生紧密耦合。
最流行的实现方式是
- 使用一个需要被观察者继承的接口
- 使用从主题到观察者的函数指针
实现这两种模型中的任何一种都既复杂又容易出错,因此耗时。更重要的是,主题和观察者之间通常出现的耦合在大多数时候是不必要的,而实现接口的要求有时会破坏设计,增加多重继承的成本,或者需要您在主题和观察者之间创建 `Adaptor` 类。
提出的实现试图通过利用模板的强大功能来克服这些问题,并创建一种在现有或新代码中实现观察者模式的简便方法,无需继承或函数指针,并且易于将主题与观察者解耦。
背景
观察者模式(也称为“发布-订阅”机制),如 Gamma 所述,定义了一种“一对多”关系,即当一个对象改变其状态时,依赖对象将被告知。
观察者模式广泛用于 GUI 应用程序(模型-视图-控制器模型)和其他需要特定对象(观察者)在主题更改状态时被告知的系统(例如,按下按钮时调用的函数处理程序)。
C# 以事件的形式实现了这种模型。
- `Button` 类将事件定义为事件和事件的委托。
public event ClickEventHandler Click(object sender, System.EventArgs e) public delegate void ClickEventHandler(object sender, System.EventArgs e)
- `Observer` 类(您的窗体)订阅事件。
this.btnMyButton.Click += new System.EventHandler(this.btnMyButton_Click);
- `Observer` 类期望在按下按钮时接收事件,并且 `Click` 事件在函数中生成。
private void btnMyButton_Click(object sender, System.EventArgs e) {...}
现在,这种模型对于 C# 来说很简单且基础,没有太多依赖。此 `Observer` 实现试图为 C++ 带来这种便捷性,并且可能更具灵活性。
Using the Code
首先:我们有一个主题和多个观察者。主题是定义事件的一方,而观察者是必须订阅这些事件并在事件触发时接收它们的一方。任何观察者实现唯一真正的要求是,主题定义的事件在观察者中以相同的方式定义。示例
class CSubject1{
public:
void Event1( int param1, int param2, bool param3 );
// event definition
};
暗示以下观察者实现之一
class CObserver1{
public:
void OnEvent1( int param1, int param2, bool param3 );
// event definition
};
class CObserver2{
public:
void OnEvent1WithSubject( CSubject1 *pObject,
int param1, int param2, bool param3 );
// event definition with reference to the
// Subject that generated the event
};
这两种事件定义都很有用。在第一种定义中,我们有不了解事件生成类的优势,因此它可以是任何类,而不仅仅是 `CSubject1`。在第二种定义中,我们必须了解 `CSubject1` 类,因此我们可以根据 `CSubject1` 的详细信息进行特定操作。
现在,如何访问主题,如何连接这两个类,并使 `CSubject1::Event1` 函数表现得像一个 `Event` 并调用 `CObserver1::OnEvent1` 或 `CObserver2::OnEvent1WithSubject`?答案很简单。在 `Subject` 中添加一个 `SubjectEvents` 的成员,并将观察者与主题连接起来。
class CSubject1{
public:
CSubject1();
// event definition
void Event1( int param1, int param2, bool param3 );
// the Events manager
SubjectEvents<CSubject1> Events;
};
CSubject1::CSubject1()
// required to initialize the Events with the "this" pointer
: Events(this)
{ }
void main()
{
CSubject1 s1;
CObserver1 o1;
CObserver2 o2;
// Connect s1 event to o1, o2
// First Connect the Event1 to CObserver1 o1. Type checking
// is automatically done on both ends
// thus the event implementation in the Observer has
// to be correct or the code will not compile
// ClientObserver is used when the event is implemented
// without requiring the CSubject1 as parameter
s1.Events ( CSubject1::Event1 ) +=
ClientObserver ( &o1, CObserver1::OnEvent1 );
// connect o2. SubjectObserver is used when the event
// implementation has the CSubject1 as the first parameter
s1.Events ( CSubject1::Event1 ) +=
SubjectObserver ( &o1, CObserver1::OnEvent1WithSubject );
// both event handlers are not connected to the event generator.
// Now, generate an event:
s1.Events ( CSubject1::Event1 ).Notify ( 1, 2, true );
// type checking is done again assuring
// the validity of the parameters !!!
// if you like more explicit/shorter function
// calls you can call the event like:
s1.Events ( CSubject1::Event1 ) ( 1, 2, true );
// or
s1.Events.Event ( CSubject1::Event1 ).Notify ( 1, 2, true );
}
然而,大多数时候,您不想从定义事件的类外部“生成”事件,也不想经常编写类似以下的代码:
s1.Events ( CSubject1::Event1 ) ( 1, 2, true );
来生成事件,更重要的是,我们还有 `CSubject1::Event1(...)` 函数未实现且未使用,因此一个小小的改动将使我们的生活更轻松。
void CSubject1::Event1( int param1, int param2, bool param3 )
{
Events ( CSubject1::Event1 )( param1, param2, param3 );
// type checking is done again assuring
// the validity of the parameters !!!
}
现在,我们拥有一个完整、类型安全且相当灵活的事件生成和处理机制。要触发事件,我们只需调用 `Event1 ( ... )`,事件就会自动生成并分发给所有已订阅的 `Observer`。主要优点是:
- 无需定义接口即可让 `Observer` 接收由 `Subject` 生成的事件。
- 无需 `observer` 了解 `subject`(可能但非强制)。
- 无需复杂的函数指针转换或观察者注册模型。
- 易于使用:只需两行代码,您就可以拥有一个发布-订阅机制。
- 安全:所有函数定义和调用都经过类型检查。
- 安全:您无法连接参数不同的两个函数。
- 解耦主题和观察者。
实现的主要“缺点”是:
- 返回类型必须是 `void`。
- 当前实现最多支持 5 个参数。
- 事件生成的开销是:
- 在列表中查找事件定义的成本 +
- 一次 `virtual` 函数调用(代码被编译器优化后)。
实现细节
该实现使用函数指针和模板自动检测参数的能力。当一个函数注册为事件 `s1.Events ( CSubject1::Event1 )` 时,会存储函数的参数类型,并且任何注册为 `Observer` 函数的函数都必须符合 `Subject` 函数定义的参数。
`SubjectEvents` 类维护一个已注册事件的列表。每次调用 `Event(...)` 或 `operator() ( ... )` 时,都会查找并注册作为 `Event` 发送的函数。
`Event(...)` 和 `operator() ( ... )` 具有相同的原型,并为可以注册的不同类型的函数重载,从
template<typename _Subject2>
Blue::TSubjectEvent<_Subject2>& Event ( void (_Subject2::*_FuncPoint)() )
对于没有任何参数的函数,到
template<typename _Subject2, typename _Param,
typename _Param2, typename _Param3, typename _Param4, typename _Param5>
Blue::TSubjectEvent<_Subject2,_Param, _Param2,_Param3,_Param4,_Param5>& Event
( void (_Subject2::*_FuncPoint)(_Param,_Param2,_Param3,_Param4,_Param5) )
对于带有五个参数的函数。因此,每次调用都会自动检测传递的确切参数,并返回用指定参数构造的 `TSubjectEvent` 类。`TSubjectEvent` 重载了 `operator+=`,并接受将其内部的观察者列表 - `TClientObserver` 类添加到其中,这些类以相同的方式创建并具有相同的参数。`TClientObserver` 是通过调用 `ClientObserver` 或 `SubjectObserver` 函数返回的,其构建方式与 `Event(...)` 函数相同,通过检测函数的参数。
template <typename _Observer, typename _Subject>
TClientObserver<_Observer,_Subject>
SubjectObserver( _Observer* pObserver,
void (_Observer::*_FuncPoint)(_Subject) )
{ // SubjectObserver requires function
// that contain the "_Subject" as the first parameter
return TClientObserver<_Observer,_Subject> ( pObserver, _FuncPoint );
}
template <typename _Observer>
TClientObserver<_Observer,NullType>
ClientObserver( _Observer* pObserver, void (_Observer::*_FuncPoint)() )
{ // ClientObserver requires functions
// that do not _Subject as the first parameter
return TClientObserver<_Observer> ( pObserver, _FuncPoint );
}
如果您尝试使用参数与 `TSubjectEvent` 中定义的参数不同的 `SubjectObserver` / `CodeObserver` 来添加 `Observer` 函数,编译器将不会编译代码,因为参数不同。
在尝试从主题触发事件时,参数类型检测机制与函数注册时使用的相同。唯一值得注意的实现细节是最终执行的函数调用,该调用基于 `Observer` 和 `Observer` 的函数等详细信息,从基于模板的函数执行。
_Call ( param, param2, param3, param4, param5,
ParamCount<ArgCount>(), Type2Type<_Subject>() );
如果观察者注册为不接收主题作为参数,则编译器会自动选择以下函数之一:
template<typename T> void _Call( _Param param,
_Param2 param2, _Param3 param3, _Param4 param4, _Param5 param5,
ParamCount<0>, Type2Type<NullType> )
{
( ((_Observer*)m_pObserver)->*m_pFunction) ( );
}
[...]
template<typename T> void _Call( _Param param,
_Param2 param2, _Param3 param3, _Param4 param4,
_Param5 param5, ParamCount<5>, Type2Type<NullType> )
{
( ((_Observer*)m_pObserver)->*m_pFunction)
( param, param2, param3, param4, param5 );
}
或者,如果观察者想要主题指针,则选择以下函数之一:
template<typename T> void _Call( _Param param,
_Param2 param2, _Param3 param3, _Param4 param4,
_Param5 param5, ParamCount<0>, ... )
{
( ((_Observer*)m_pObserver)->*m_pFunction) ( (_Subject)m_pSubject );
}
[...]
template<typename T> void _Call( _Param param,
_Param2 param2, _Param3 param3, _Param4 param4,
_Param5 param5, ParamCount<5>, ... )
{
( ((_Observer*)m_pObserver)->*m_pFunction)
( (_Subject)m_pSubject, param, param2, param3, param4, param5 );
}
最后两个参数帮助编译器根据以下内容检测要编译的确切函数:
- 根据 `ParamCount<0>` 确定 `Observer` 函数的有效参数数量。
- 根据 `Type2Type
` 确定是否传递主题指针。
如果 `Observer` 注册为不接收 `Subject`,则 `Type2Type<_Subject>()` 将解析为 `Type2Type
如果 `Observer` 注册为接收 `Subject`,则编译器将选择带有最后一个参数 `...` 的函数。
同样,`ParamCount` 用于确定要发送到最终函数的参数数量。
结论
观察者模式可以通过多种方式实现,大多数都需要依赖定义一个由另一个类(以某种方式)实现的接口。
此实现使用非侵入式方法来实现此模式,允许您完全解耦 `Observer` 和 `Subject`。
历史
- 2004 年 1 月 25 日:版本 1.0:首次发布版本
Copyright
您可以完全免费使用这些源代码。
相关文章
- 封装观察者模式
一个基于模板的观察者模式实现,使用了继承。 - 在C++应用程序中应用观察者模式
对观察者模式及其用法的绝佳解释。 - Intercom:一个基于模板的通知库。
一个观察者模式实现,具有使用三个组件模式的优势:消息、通知者、观察者。 - 使用模板实现主题/观察者模式。
另一个不错的基于模板的模式实现,基于继承。
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。