65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (19投票s)

2004年1月27日

7分钟阅读

viewsIcon

95408

downloadIcon

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 );
}

最后两个参数帮助编译器根据以下内容检测要编译的确切函数:

  1. 根据 `ParamCount<0>` 确定 `Observer` 函数的有效参数数量。
  2. 根据 `Type2Type` 确定是否传递主题指针。

如果 `Observer` 注册为不接收 `Subject`,则 `Type2Type<_Subject>()` 将解析为 `Type2Type`(因为 `_Subject` 已设置为 `NullType`)。

如果 `Observer` 注册为接收 `Subject`,则编译器将选择带有最后一个参数 `...` 的函数。

同样,`ParamCount` 用于确定要发送到最终函数的参数数量。

结论

观察者模式可以通过多种方式实现,大多数都需要依赖定义一个由另一个类(以某种方式)实现的接口。

此实现使用非侵入式方法来实现此模式,允许您完全解耦 `Observer` 和 `Subject`。

历史

  • 2004 年 1 月 25 日:版本 1.0:首次发布版本

Copyright

您可以完全免费使用这些源代码。

相关文章

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.