观察者模式 - 进一步的考虑和广义的C ++实现

时间:2013-01-31 19:40:03

标签: 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次尝试中获得了累积奖金。

actual implementation,名为gxObserver,可在GitHub上找到;它是完整的文档和自述文件拼写出来的优点和缺点。它的语法是:

// Subscribing
mSubject->gxSubscribe( evAge, OnAgeChanged );

// Firing
Fire( evAge, 69 );

做了过多的工作后,我觉得只是与SO社区分享我的发现。所以下面我将回答这个问题:

  

在实现观察者模式时,程序员应该考虑哪些额外的考虑因素(设计模式中提供的那些)?

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

请注意:由于SO限制了30000个单词的答案,我的答案必须分为两部分,但有时候第二个答案(首先是“主题”出现的答案)。答案的第1部分是从设计模式的类图开始的。

1 个答案:

答案 0 :(得分:11)

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个事件。因此,广义的主题 - 观察者协议必须集成设计模式所谓的“兴趣”和“兴趣”。 - 允许观察者订阅特定事件,并且只对感兴趣的观察者发起该事件。

// 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 )
{
}

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

观察

事件处理程序

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

重写处理程序

重写的处理程序是Design Patterns提供的解决方案。基本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 ++中实现任意回调并不是一件简单的事情。赫伯在他的文章中使用function设施,但不幸的是,他提案中的一个关键问题尚未完全解决。该问题及其解决方案如下所述。

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

  

成员函数指针+对象实例=委托

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

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

更新协议

Design Patterns的实现第7点描述了pull vs push模型。本节扩展了讨论范围。

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

我们已经确定拉模型不适用于BeforeChildRemove等无状态事件。或许还值得一提的是,使用pull模型,程序员需要为每个事件处理程序添加代码行,这些代码不会与推送模型一起存在:

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

现在虽然主题及其观察者之间的协议很简单,但实际实现相当漫长。有一些缺点需要考虑:

首先,我们需要为每个事件编写大量代码(请参阅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()方法中完成。但是这将涉及动态转换(性能代价高昂),或者我们进行静态转换,如果事件被触发并且处理程序期望的事件不匹配,则可能导致灾难。

第五,处理程序arity几乎没有可读性:

// 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()方法和观察者处理程序每​​个事件都有不同的arity。代码是可读的,并且可以达到我们希望的那么短。

此实现涉及非常干净的客户端代码,但会带来相当复杂的Subject代码(具有大量功能模板和可能的其他好东西)。这是大多数程序员将要采取的权衡 - 最好在一个地方(Subject类)拥有复杂的代码,而不是许多(客户端代码);如果主题类完美无缺,那么程序员可能只会把它视为一个黑盒子,对它的实现方式一无所知。

值得考虑的是如何以及何时确保Fire arity和处理程序arity匹配。我们可以在运行时完成它,如果两者不匹配,我们会提出一个断言。但是如果我们在编译期间遇到错误会非常好,为此我们必须明确地声明每个事件的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 )
};

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

(第一部分结束)