c++ - 观察者模式 - 进一步的考虑和通用的 C++ 实现

标签 c++ oop design-patterns observer-pattern

我正在编写的 C++ MVC 框架大量使用了观察者模式。我已经彻底阅读了设计模式(GoF,1995)中的相关章节,并查看了文章和现有库(包括 Boost)中的大量实现。

但是当我实现这个模式时,我不禁感到一定有更好的方法 - 我的客户端代码涉及我认为应该重构到模式本身的行和片段,如果我能找到克服的方法一些 C++ 限制。此外,我的语法从未像 ExtJs 库中使用的那样优雅:

// Subscribing
myGridPanel.on( 'render', this.onRender );

// Firing
this.fireEvent( 'render', null, node );

因此,我决定进行进一步的研究,试图得出一个通用的实现,同时优先考虑代码的优雅、可读性和性能。我相信我已经在第 5 次尝试中中了头奖。

名为 gxObserveractual implementation 可在 GitHub 上找到;它是一个很好的文档,自述文件说明了优点和缺点。它的语法是:
// Subscribing
mSubject->gxSubscribe( evAge, OnAgeChanged );

// Firing
Fire( evAge, 69 );

完成了一项繁重的工作后,我觉得这只是与 SO 社区分享我的发现而已。那么下面我来回答这个问题:

What additional considerations (to these presented in Design Patterns) should programmers account for when implementing the observer pattern?



虽然专注于 C++,但下面的许多内容都适用于任何语言。

请注意: 由于 SO 将答案限制为 30000 字,我的答案必须分两部分提供,但有时会先出现第二个答案(以“主题”开头的答案)。答案的第 1 部分是从设计模式中的类图开始的。

最佳答案

enter image description here

(第一部分开始)

先决条件

这不仅仅是关于状态

设计模式将观察者模式与对象“状态”联系起来。如上面的类图(来自设计模式)所示,可以使用 SetState() 方法设置主题的状态;在状态改变时,主题将通知其所有观察者;然后观察者可以使用 GetState() 方法查询新状态。

但是, GetState() 不是主题基类中的实际方法。相反,每个具体的主题都提供了自己的专门状态方法。实际代码可能如下所示:

SomeObserver::onUpdate( aScrollManager )
{
    // GetScrollPosition() is a specialised GetState();
    aScrollPosition = aScrollManager->GetScrollPosition();
}

什么是对象状态?我们将其定义为状态变量的集合——需要持久化的成员变量(用于以后恢复)。例如,BorderWidthFillColour 都可以是 Figure 类的状态变量。

我们可以拥有多个状态变量——因此一个对象的状态可以以不止一种方式改变——这一想法很重要。这意味着受试者可能会触发不止一种类型的状态变化事件。它还解释了为什么在主题基类中使用 GetState() 方法毫无意义。

但是只能处理状态变化的观察者模式是不完整的——观察者观察无状态通知是很常见的,即与状态无关的通知。例如,KeyPressMouseMove OS 事件;或者像 BeforeChildRemove 这样的事件,这显然并不表示实际的状态变化。这些无状态事件足以证明推送机制是合理的——如果观察者无法从主题中检索更改信息,则所有信息都必须与通知一起提供(稍后会详细介绍)。

会有很多事件

很容易看出在“现实生活”中一个主题如何触发多种类型的事件;快速浏览 ExtJs 库会发现一些类提供超过 30 个事件。因此,通用的主体-观察者协议(protocol)必须集成设计模式所说的“兴趣”——允许观察者订阅特定事件,并且主体仅向感兴趣的观察者触发该事件。
// A subscription with no interest.
aScrollManager->Subscribe( this );

// A subscription with an interest.
aScrollManager->Subscribe( this, "ScrollPositionChange" );

它可能是多对多的

单个观察者可以从多个主体观察同一事件(使观察者-主体关系成为多对多)。例如,属性检查器可能会监听许多选定对象的相同属性的变化。如果观察者对哪个主题发送通知感兴趣,则通知必须包含发送者:
SomeSubject::AdjustBounds( aNewBounds )
{
    ...
    // The subject also sends a pointer to itself.
    Fire( "BoundsChanged", this, aNewBounds );
}

// And the observer receives it.
SomeObserver::OnBoundsChanged( aSender, aNewBounds )
{
}

然而,值得注意的是,在许多情况下,观察者并不关心发送者的身份。例如,当主体是单例时,或者当观察者对事件的处理不依赖于主体时。因此,与其强制发送方成为协议(protocol)的一部分,我们应该允许它成为协议(protocol)的一部分,将是否拼写发送方的问题留给程序员。

观察员

事件处理程序

处理事件的观察者方法(即事件处理程序)可以有两种形式:覆盖或任意。在观察者的实现中提供了一个关键和复杂的部分,这两个部分将在本节中讨论。

覆盖的处理程序

覆盖处理程序是设计模式提出的解决方案。基类 Subject 定义了一个虚拟的 OnEvent() 方法,子类覆盖它:
class Observer
{
public:
    virtual void OnEvent( EventType aEventType, Subject* aSubject ) = 0;
};

class ConcreteObserver
{
    virtual void OnEvent( EventType aEventType, Subject* aSubject )
    {
    }
};

请注意,我们已经考虑了主体通常会触发不止一种类型的事件的想法。但是在 OnEvent 方法中处理所有事件(特别是如果有几十个)是笨拙的——如果每个事件都在自己的处理程序中处理,我们可以编写更好的代码;实际上,这使 OnEvent 成为其他处理程序的事件路由器:
void ConcreteObserver::OnEvent( EventType aEventType, Subject* aSubject )
{
    switch( aEventType )
    {
        case evSizeChanged:
            OnSizeChanged( aSubject );
            break;
        case evPositionChanged:
            OnPositionChanged( aSubject );
            break;
    }
}

void ConcreteObserver::OnSizeChanged( Subject* aSubject )
{
}

void ConcreteObserver::OnPositionChanged( Subject* aSubject )
{
}

具有覆盖(基类)处理程序的优点是它非常容易实现。订阅主题的观察者可以通过提供对自身的引用来实现:
void ConcreteObserver::Hook()
{
    aSubject->Subscribe( evSizeChanged, this );
}

然后主体只保留一个 Observer 对象的列表,触发代码可能如下所示:
void Subject::Fire( aEventType )
{
    for ( /* each observer as aObserver */)
    {
        aObserver->OnEvent( aEventType, this );
    }
}

覆盖处理程序的缺点是它的签名是固定的,这使得额外参数的传递(在推送模型中)变得棘手。此外,对于每个事件,程序员必须维护两位代码:路由器( OnEvent )和实际处理程序( OnSizeChanged )。

任意处理程序

克服被覆盖的 OnEvent 处理程序的不足之处的第一步是……没有全部!如果我们能告诉主体用哪种方法来处理每个事件,那就太好了。像这样:
void SomeClass::Hook()
{
    // A readable Subscribe( evSizeChanged, OnSizeChanged ) has to be written like this:
    aSubject->Subscribe( evSizeChanged, this, &ConcreteObserver::OnSizeChanged );
}

void SomeClass::OnSizeChanged( Subject* aSubject )
{
}

请注意,通过此实现,我们不再需要从 Observer 类继承我们的类;事实上,我们根本不需要 Observer 类。这个想法不是一个新想法,它在 Herb Sutter’s 2003 Dr Dobbs article called ‘Generalizing Observer’ 中有详细描述。但是,在 C++ 中实现任意回调并不是一件简单的事情。 Herb 在他的文章中使用了 function 工具,但不幸的是,他的提案中的一个关键问题没有完全解决。该问题及其解决方案如下所述。

由于 C++ 不提供原生委托(delegate),我们需要使用成员函数指针(MFP)。 C++ 中的 MFP 是类函数指针而不是对象函数指针,因此我们必须为 Subscribe 方法提供 &ConcreteObserver::OnSizeChanged(MFP)和 this(对象实例)。我们将这种组合称为委托(delegate)。

Member Function Pointer + Object Instance = Delegate


Subject 类的实现可能依赖于比较委托(delegate)的能力。例如,在我们希望向特定委托(delegate)触发事件的情况下,或者当我们想要取消订阅特定委托(delegate)时。如果处理程序不是虚拟的并且属于订阅类(而不是在基类中声明的处理程序),则委托(delegate)可能具有可比性。但在大多数其他情况下,编译器或继承树的复杂性(虚拟或多重继承)将使它们无法比拟。 Don Clugston has written a fantastic in-depth article关于这个问题,他还提供了一个C++库来克服这个问题;虽然不符合标准,但该库几乎适用于所有编译器。

值得一问的是,虚拟事件处理程序是否是我们真正需要的东西;也就是说,我们是否可能有一个观察者子类想要覆盖(或扩展)其(具体观察者)基类的事件处理行为的场景。可悲的是,答案是这很有可能。所以一个通用的观察者实现应该允许虚拟处理程序,我们很快就会看到一个例子。

更新协议(protocol)

设计模式的实现点 7 描述了拉与推模型。本节扩展了讨论。



使用拉模型,主体发送最少的通知数据,然后观察者需要从主体检索更多信息。

我们已经确定拉模型不适用于无状态事件,例如 BeforeChildRemove 。也许还值得一提的是,对于拉模型,程序员需要向每个事件处理程序添加代码行,而推模型不存在这些代码:
// Pull model
void SomeClass::OnSizeChanged( Subject* aSubject )
{
    // Annoying - I wish I didn't had to write this line.
    Size iSize = aSubject->GetSize();
}

// Push model
void SomeClass::OnSizeChanged( Subject* aSubject, Size aSize )
{
    // Nice! We already have the size.
}

另一件值得记住的事情是,我们可以使用推模型来实现拉模型,但不能反过来。尽管推送模型为观察者提供其需要的所有信息,但程序员可能希望不发送特定事件的信息,而让观察者向主题查询更多信息。

固定数量推送

使用固定数量推送模型,通知携带的信息通过约定数量和类型的参数传递给处理程序。这很容易实现,但由于不同的事件将具有不同数量的参数,因此必须找到一些解决方法。在这种情况下,唯一的解决方法是将事件信息打包到一个结构(或类)中,然后将其传递给处理程序:
// The event base class
struct evEvent
{
};

// A concrete event
struct evSizeChanged : public evEvent
{
    // A constructor with all parameters specified.
    evSizeChanged( Figure *aSender, Size &aSize )
      : mSender( aSender ), mSize( aSize ) {}

    // A shorter constructor with only sender specified.
    evSizeChanged( Figure *aSender )
      : mSender( aSender )
    {
        mSize = aSender->GetSize();
    }

    Figure *mSender;
    Size    mSize;
};

// The observer's event handler, it uses the event base class.
void SomeObserver::OnSizeChanged( evEvent *aEvent )
{
    // We need to cast the event parameter to our derived event type.
    evSizeChanged *iEvent = static_cast<evSizeChanged*>(aEvent);

    // Now we can get the size.
    Size iSize  = iEvent->mSize;
}

现在,尽管主体与其观察者之间的协议(protocol)很简单,但实际实现却相当冗长。有几个缺点需要考虑:

首先,我们需要为每个事件编写相当多的代码(见 evSizeChanged)。很多代码都不好。

其次,涉及一些不容易回答的设计问题:我们应该在 evSizeChanged 类旁边还是在触发它的主题旁边声明 Size?仔细想想,两者都不是理想的。那么,大小更改通知是否总是带有相同的参数,还是取决于主题? (答案:后者是可能的。)

第三,有人需要在触发之前创建一个事件实例,然后删除它。因此,主题代码将如下所示:
// Argh! 3 lines of code to fire an event.
evSizeChanged *iEvent = new evSizeChanged( this );
Fire( iEvent );
delete iEvent;

或者我们这样做:
// If you are a programmer looking at this line than just relax!
// Although you can't see it, the Fire method will delete this 
// event when it exits, so no memory leak!
// Yes, yes... I know, it's a bad programming practice, but it works.
// Oh.. and I'm not going to put such comment on every call to Fire(),
// I just hope this is the first Fire() you'll look at and just 
// remember.
Fire( new evSizeChanged( this ) );

第四,有一个类型转换业务正在进行中。我们已经在处理程序中完成了转换,但也可以在主题的 Fire() 方法中进行转换。但这要么涉及动态转换(性能成本高),要么我们进行静态转换,如果触发的事件与处理程序期望的事件不匹配,则可能导致灾难。

第五,处理程序数量很少可读:
// What's in aEvent? A programmer will have to look at the event class 
// itself to work this one out.
void SomeObserver::OnSizeChanged( evSizeChanged *aEvent )
{
}

与此相反:
void SomeObserver::OnSizeChanged( ZoomManager* aManager, Size aSize )
{
}

这使我们进入下一节。

多样性推送

就看代码而言,很多程序员都希望看到这个主题代码:
void Figure::AdjustBounds( Size &aSize )
{
     // Do something here.

     // Now fire
     Fire( evSizeChanged, this, aSize );
}

void Figure::Hide()
{
     // Do something here.

     // Now fire
     Fire( evVisibilityChanged, false );
}

这个观察者代码:
void SomeObserver::OnSizeChanged( Figure* aFigure, Size aSize )
{
}

void SomeObserver::OnVisibilityChanged( aIsVisible )
{
}

主体的 Fire() 方法和观察者处理程序对每个事件具有不同的数量。代码是可读的,而且和我们所希望的一样短。

这个实现涉及一个非常干净的客户端代码,但会带来一个相当复杂的 Subject 代码(带有大量的函数模板和其他可能的好东西)。这是大多数程序员会采取的权衡——在一个地方(Subject 类)拥有复杂的代码比在许多地方(客户端代码)更好;并且考虑到主题类完美无缺地工作,程序员可能只是将其视为一个黑匣子,很少关心它是如何实现的。

值得考虑的是如何以及何时确保 Fire 数量与处理程序数量匹配。我们可以在运行时完成,如果两者不匹配,我们就提出断言。但是,如果我们在编译时遇到错误,那就太好了,为此我们必须显式声明每个事件的 arity,如下所示:
class Figure : public Composite, 
               public virtual Subject
{
public:
    // The DeclareEvent macro will store the arity somehow, which will
    // then be used by Subscribe() and Fire() to ensure arity match 
    // during compile time.
    DeclareEvent( evSizeChanged, Figure*, Size )
    DeclareEvent( evVisibilityChanged, bool )
};

稍后我们将看到这些事件声明如何发挥另一个重要作用。

(第一部分结束)

关于c++ - 观察者模式 - 进一步的考虑和通用的 C++ 实现,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/14633808/

相关文章:

c# - 工厂模式中使用的策略模式?

c++ - CRT 的 C++ 等价物是什么?

c++ - 类之间的双向引用——如何避免它们相互认识?

java - 在类中使用内部类是一个好的设计吗?

c++ - 我们可以在 C++ 的类中定义 hashcode 方法吗

Javascript私有(private)方法专用调用者返回未定义

c++ - 谁应该拥有迭代器、我的数据类或该类中的实际列表?

c++ - QtSerialPort 示例失败

c++ - 尽管遵循正常初始化,但我的数组拒绝成为正确的大小

c++ - shared_ptr 如何破坏对齐