13

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

但是当我在实现该模式时,我不禁觉得必须有更好的方法——我的客户端代码包含我认为应该重构为模式本身的行和片段,只要我能找到一种方法来克服一些 C++ 限制。此外,我的语法从未像 ExtJs 库中使用的那样优雅:

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

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

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

实际实现,称为,gxObserver可在 GitHub 上找到;它的文档很好,自述文件说明了优点和缺点。它的语法是:

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

// Firing
Fire( evAge, 69 );

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

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

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

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

4

2 回答 2

15

在此处输入图像描述

(第一部分开始)

先决条件

这不仅仅是关于状态

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

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

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

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

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

但是只能处理状态变化的观察者模式是不完整的——观察者观察无状态通知是很常见的,即与状态无关的通知。例如,KeyPressMouseMove操作系统事件;或类似的事件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 )
{
}

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

观察员

事件处理程序

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

被覆盖的处理程序

重写处理程序是设计模式提供的解决方案。基 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 2003 年 Dobbs 博士的一篇名为“Generalizing Observer”的文章中进行了详细描述</a>。但是,在 C++ 中实现任意回调并不是一件简单的事情。Herbfunction在他的文章中使用了该工具,但不幸的是,他的提案中的一个关键问题没有完全解决。该问题及其解决方案如下所述。

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

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

Subject类的实现可能依赖于比较委托的能力。例如,如果我们希望向特定委托触发事件,或者当我们想要取消订阅特定委托时。如果处理程序不是虚拟的并且属于订阅类(与在基类中声明的处理程序相反),则委托可能是可比较的。但在大多数其他情况下,编译器或继承树的复杂性(虚拟或多重继承)将使它们无法比拟。Don Clugston 就这个问题写了一篇精彩的深入文章,其中他还提供了一个 C++ 库来解决这个问题;虽然不符合标准,但该库几乎可以与所有编译器一起使用。

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

更新协议

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

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

我们已经确定拉模型不适用于无状态事件,例如BeforeChildRemove. 或许还值得一提的是,对于 pull 模型,程序员需要向每个事件处理程序添加代码行,而 push 模型不存在这些代码行:

// 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首先,我们需要为每个事件编写大量代码(参见 参考资料)。很多代码都很糟糕。

其次,其中涉及一些不容易回答的设计问题:我们应该evSizeChangedSize类一起声明,还是与触发它的主题一起声明?如果您考虑一下,两者都不是理想的。那么,尺寸更改通知是否总是带有相同的参数,还是取决于主题?(答案:后者是可能的。)

第三,有人需要在触发之前创建事件的实例,然后将其删除。因此,主题代码将如下所示:

// 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代码(具有大量功能模板和可能的其他好东西)。这是大多数程序员都会采取的权衡取舍——在一个地方(主题类)拥有复杂的代码,而不是在许多地方(客户端代码);并且鉴于主题类完美无缺,程序员可能只是将其视为一个黑盒,很少关心它是如何实现的。

值得考虑的是如何以及何时确保Firearity 和 handler 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 )
};

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

(第一部分结束)

于 2013-01-31T19:42:33.957 回答
11

(第二部分开始)

科目

订阅过程

存储的是什么?

根据具体实现,在观察者订阅时,主体可能会存储以下数据:

  • 事件 ID – 兴趣或观察者订阅的事件。
  • 观察者实例——最常见的是对象指针的形式。
  • 成员函数指针——如果使用任意处理程序。

这些数据将形成 subscribe 方法的参数:

// Subscription with an overridden handler (where the observer class has a base class handler method).
aSubject->Subscribe( "SizeChanged", this );

// Subscription with an arbitrary handler.
aSubject->Subscribe( "SizeChanged", this, &ThisObserverClass::OnSizeChanged );

值得注意的是,如果使用任意处理程序,成员函数指针很可能与类或结构中的观察者实例一起打包,形成委托。因此该Subscribe()方法可能具有以下签名:

// Delegate = object pointer + member function pointer.
void Subject::Subscribe( EventId aEventId, Delegate aDelegate )
{
   //...
}

实际存储(可能在 a 中std::map)将涉及事件 id 作为键和委托作为值。

实现事件 ID

在触发它们的主题类之外定义事件 ID 可以简化对这些 ID 的访问。但一般来说,主体触发的事件是该主体独有的。因此,在大多数情况下,在主题类中声明事件 ID 是合乎逻辑的。

虽然声明事件 ID 的方法不止几种,但这里只讨论最感兴趣的 3 种:

从表面上看,枚举似乎是最合乎逻辑的选择:

class FigureSubject : public Subject
{
public:
    enum {
        evSizeChanged,
        evPositionChanged
    };
};

枚举的比较(将在订阅和触发时发生)很快。也许这种策略唯一的不便是观察者需要在订阅时指定类:

// 'FigureSubject::' is the annoying bit.
aSubject->Subscribe( FigureSubject::evSizeChanged, this );

字符串为枚举提供了一个“更宽松”的选项,因为通常主题类不会像枚举一样声明它们;相反,客户将只使用:

// Observer code
aFigure->Subscribe( "evSizeChanged", this );

字符串的好处是大多数编译器对它们进行颜色编码与其他参数不同,这以某种方式提高了代码的可读性:

// Within a concrete subject
Fire( "evSizeChanged", mSize, iOldSize );

但是字符串的问题是我们无法在运行时判断我们是否拼错了事件名称。此外,字符串比较比枚举比较花费更长的时间,因为字符串必须逐个字符进行比较。

类型是这里讨论的最后一个选项:

class FigureSubject : public Subject
{
public:
    // Declaring the events this subject supports.
    class SizeChangedEventType     : public Event {} SizeChangedEvent;
    class PositionChangedEventType : public Event {} PositionChangedEvent;
};

使用类型的好处是它们允许重载方法,例如Subscribe()(我们很快就会看到它可以解决观察者的常见问题):

// This particular method will be called only if the event type is SizeChangedType
FigureSubject::Subscribe( SizeChangedType aEvent, void *aObserver )
{
    Subject::Subscribe( aEvent, aObserver );

    Fire( aEvent, GetSize(), aObserver );
}

但同样,观察者需要一些额外的代码来订阅:

// Observer code
aFigure->Subscribe( aFigure->SizeChangedEvent, this );

在哪里存储观察者?

设计模式中的实现点 1 处理每个主题的观察者应该存储在哪里。本节补充了该讨论,提供了 3 个选项:

  • 全局哈希
  • 每个主题
  • 每个事件

正如设计模式中所建议的,存储主体-观察者映射的一个地方是全局哈希表。该表将包括主题、事件和观察者(或代表)。在所有方法中,这是最节省内存的方法,因为主体不使用成员变量来存储观察者列表——只有一个全局列表。如果由于浏览器提供的内存有限,该模式在 javascript 框架中实现,这可能很有用。这种方法的主要缺点是它也是最慢的——对于每个被触发的事件,我们首先必须从全局哈希中过滤请求的主题,然后过滤请求的事件,然后才遍历所有观察者。

设计模式中还建议每个主题都有一个观察者列表。这将消耗稍多的内存(以std::map每个主题的成员变量的形式),但它提供比全局哈希更好的性能,因为主题只需要过滤请求的事件,然后遍历该事件的所有观察者。代码可能如下所示:

class Subject
{
protected:    
    // A callback is represented by the event id and the delegate.
    typedef std::pair< EventId, Delegate > Callback;

    // A map type to store callbacks
    typedef std::multimap< EventId, Delegate > Callbacks;

    // A callbacks iterator
    typedef Callbacks::iterator CallbackIterator;

    // A range of iterators for use when retrieving the range of callbacks
    // of a specific event.
    typedef std::pair< CallbackIterator, CallbackIterator> CallbacksRange;

    // The actual callback list
    Callbacks mCallbacks;
public:
    void Fire( EventId aEventId )
    {
        CallbacksRange   iEventCallbacks;
        CallbackIterator iIterator;

        // Get the callbacks for the request event.
        iEventCallbacks = mCallbacks.equal_range( aEventId );

        for ( iIterator = iEventCallbacks.first; iIterator != iEventCallbacks.second; ++iIterator  )
        {
            // Do the firing.
        }
    }
};

设计模式中不建议将每个事件作为成员变量,然后将观察者存储在事件本身中。这是最消耗内存的策略,因为不仅每个事件消耗一个成员变量,而且每个事件还有一个std::vector存储观察者。然而,这种策略提供了最好的性能,因为没有过滤要做,我们可以遍历附加的观察者。与其他两个相比,此策略还将涉及最简单的代码。要实现它,事件必须提供订阅和触发方法:

class Event
{
public:
    void Subscribe( void *aDelegate );
    void Unsubscribe( void *aDelegate );

    void Fire();
};

主题可能看起来像这样:

class ConcreteSubject : public Subject
{
public:
    // Declaring the events this subject supports.
    class SizeChangedEventType     : public Event {} SizeChangedEvent;
    class PositionChangedEventType : public Event {} PositionChangedEvent;
};

尽管观察者理论上可以直接订阅事件,但我们会看到通过主题是值得的:

// Subscribing to the event directly - possible but will limit features.
aSubject->SizeChangedEvent.Subscribe( this );

// Subscribing via the subject.
aSubject->Subscribe( aSubject->SizeChangedEvent, this );

这 3 种策略提供了存储与计算权衡的明确案例。并且可以使用下表进行比较:

在此处输入图像描述

所采取的方法应考虑以下因素:

  • 受试者/观察者比率——在观察者少而受试者多的系统中,记忆损失会更高,尤其是在典型的受试者没有观察者或只有一个观察者的情况下。
  • 通知的频率——通知越频繁,性能损失就越高。

当使用观察者模式来通知MouseMove事件时,可能需要更多地考虑实现的性能。就内存损失而言,以下计算可能会有所帮助。鉴于:

  • 使用按事件策略
  • 典型的 64 位系统
  • 每个科目平均有 8 个事件

800 万个主题实例将消耗略低于 1GB 的 RAM(仅限事件内存)。

同一个观察者,同一个事件?

观察者模式实现中的一个关键问题是我们是否允许同一个观察者多次订阅同一个事件(同一个主题)。

首先,如果我们允许它,我们可能会使用std::multimap而不是std::map. 此外,以下行将有问题:

aSubject->Unsubscribe( evSizeChanged, this );

由于主题无法知道要取消订阅之前的哪个订阅(可能不止一个!)。因此Subscribe()必须返回一个Unsubscribe()将使用的令牌,整个实现变得更加复杂。

从表面上看,这似乎很愚蠢——为什么同一个对象要多次订阅同一个事件?但请考虑以下代码:

class Figure
{
public:
    Figure( Subject *aSubject )
    {
        // We subscribe to the subject on size events
        aSubject->Subscribe( evSizeChanged, this, &Figure::OnSizeChanged );
    }

    void OnSizeChanged( Size aSize )
    {
    }
};

class Circle : public Figure
{
public:
    Circle( Subject *aSubject )
      : Figure( aSubject) 
    {
        // We subscribe to the subject on size events
        aSubject->Subscribe( evSizeChanged, this, &Circle::OnSizeChanged );
    }

    void OnSizeChanged( Size aSize )
    {
    }
};

此特定代码将导致同一对象订阅同一事件两次。还值得注意的是,由于该OnSizeChanged()方法不是虚拟的,因此两次订阅调用之间的成员函数指针会有所不同。因此,在这种特殊情况下,主题还可以比较成员函数指针,取消订阅签名将是:

aSubject->Unsubscribe( evSizeChanged, this, &Circle::OnSizeChanged );

但是如果OnSizeChanged()是虚拟的,那么没有令牌就无法区分两个订阅调用。

说实话,如果OnSizeChanged()是虚拟的,则该类没有理由Circle再次订阅该事件,因为它将被调用的是它自己的处理程序,而不是基类的处理程序:

class Figure
{
public:
    // Constructor
    Figure( Subject *aSubject )
    {
        // We subscribe to the subject on size events
        aSubject->Subscribe( evSizeChanged, this, &Figure::OnSizeChanged );
    }

    virtual void OnSizeChanged( Size aSize )
    {
    }
};

class Circle : public Figure
{
public:
    // Constructor
    Circle( Subject *aSubject )
      : Figure( aSubject) { }

    // This handler will be called first when evSizeChanged is fired.
    virtual void OnSizeChanged( Size aSize )
    {
        // And we can call the base class handler if we want.
        Figure::OnSizeChanged( aSize );
    }
};

当基类及其子类都必须响应同一事件时,此代码可能代表了最佳折衷方案。但它要求处理程序是虚拟的,并且程序员需要知道基类订阅了哪些事件。

不允许同一个观察者多次订阅同一个事件可以极大地简化模式的实现。它省去了比较成员函数指针的需要(一项棘手的工作),并且它允许Unsubscribe()像这样短(即使 MFP 提供了Subscribe()):

aSubject->Unsubscribe( evSizeChanged, this );

订阅后一致性

观察者模式的主要目标之一是让观察者与其主体状态保持一致——我们已经看到状态变化事件正是这样做的。

有点令人惊讶的是,设计模式的作者断言当观察者订阅一个主题时,前者的状态与后者的状态不一致。考虑这段代码:

class Figure
{
public:
    // Constructor
    Figure( FigureSubject *aSubject )
    {
        // We subscribe to the subject on size events
        aSubject->Subscribe( evSizeChanged, this, &Figure::OnSizeChanged );
    }

    virtual void OnSizeChanged( Size aSize )
    {
        mSize = aSize;

        // Refresh the view.
        Refresh();
    }
private:
    Size mSize;
};

创建时,Figure该类确实订阅了它的主题,但它的大小与主题的大小不一致,它也不会刷新视图以显示其正确大小。

当使用观察者模式触发状态改变事件时,经常会发现订阅后需要手动更新观察者。实现这一点的一种方法是在观察者内部:

class Figure
{
public:
    Figure( FigureSubject *aSubject )
    {
        // We subscribe to the subject on size events
        aSubject->Subscribe( evSizeChanged, this, &Figure::OnSizeChanged );

        // Now make sure we're consistent with the subject.
        OnSizeChanged( aSubject->GetSize() );
    }

    // ...
};

但是想象一个有 12 个状态变化事件的主题。如果整个事情会自动发生,那就太好了,在订阅时,主题将向观察者发送正确的事件。

实现这一点的一种方法需要Subscribe()在具体主题中使用重载方法:

// This method assumes that each event has its own unique class, so the method
// can be overloaded.
FigureSubject::Subscribe( evSizeChanged aEvent, Delegate aDelegate )
{
    Subject::Subscribe( aEvent, aDelegate );

    // Notice the last argument in this call.
    Fire( aEvent, GetSize(), aDelegate );
}

然后是观察者代码:

class Figure
{
public:
    Figure( FigureSubject *aSubject )
    {
        // We subscribe to the subject on size events.
        // The subject will fire the event upon subscription
        aSubject->Subscribe( evSizeChanged, MAKEDELEGATE( this, &Figure::OnSizeChanged ) );
    }

    // ...
};

请注意,该Fire调用现在需要一个额外的参数 ( aDelegate),因此它只能更新特定的观察者,而不是已经订阅的观察者。

gxObserver通过定义绑定事件来处理这种情况。这些事件的唯一参数(除了可选的发送者)绑定到 getter 或成员变量:

class Subject : virtual public gxSubject
{
public:
    gxDefineBoundEvent( evAge, int, GetAge() )

    int GetAge() { return mAge; }
private:
    int mAge;    
}

这也允许主体触发仅提供事件类型的事件:

// Same as Fire( evAge, GetAge() );
Fire( evAge );

无论使用何种机制,都值得记住:

  • 需要有一种方法来确保观察者在状态事件订阅后与他们的主题一致。
  • 最好在主题类中实现,而不是在观察者代码中实现。
  • Fire()方法可能需要一个额外的可选参数,以便它可以触发单个观察者(刚刚订阅的那个)。

烧制过程

来自基类的火灾

以下代码片段显示了JUCE中事件触发的实现:

void Button::sendClickMessage (const ModifierKeys& modifiers)
{
    for (int i = buttonListeners.size(); --i >= 0;)
    {
        ButtonListener* const bl = (ButtonListener*) buttonListeners[i];
        bl->buttonClicked (this);
    }
}

这种方法有几个问题:

  • 从代码中可以明显看出,该类维护自己的 列表buttonListeners,这意味着它也有自己的AddListenerRemoveListener方法。
  • 具体主题是遍历观察者列表的主题。
  • 主体与其观察者高度耦合,因为它知道自己的类(ButtonListener)和其中的实际回调方法(buttonClicked)。

所有这些都意味着没有基础学科类。如果采用这种方法,则必须针对每个具体主题重新实现任何触发/订阅机制。这是反面向对象的编程。

在主题基类中完成观察者的管理、遍历和实际通知是明智的;这样,对下划线机制的任何更改(例如引入线程安全)都不需要更改每个具体主题。这将为我们的具体主题留下一个封装良好且简单的界面,并将触发简化为一行:

// In a concreate subject
Fire( evSize, GetSize() );

活动暂停和恢复

许多应用程序和框架会发现需要暂停特定主题的事件触发。有时我们希望暂停的事件排队并在我们恢复触发时被触发,有时我们只想忽略它们。就主题界面而言:

class Subject
{
public:
    void SuspendEvents( bool aQueueSuspended );
    void ResumeEvents();
};

事件暂停有用的一个示例是在复合对象的销毁期间。当一个复合对象被销毁时,它首先销毁它的所有子对象,这些子对象首先销毁它们的所有子对象,依此类推。现在,如果这些复合对象驻留在模型层中,它们将需要通知视图层中的相应对象(例如使用evBeforeDestroy事件):

在此处输入图像描述

现在在这种特定情况下,不需要每个对象触发一个evBeforeDestroy事件——只要顶层模型对象会触发它就会这样做(删除顶层视图对象也会删除它的所有子对象)。因此,每当这样的组合被销毁时,它都希望暂停其子项的事件(而不对它们进行排队)。

另一个例子是加载涉及许多对象的文档,一些对象观察其他对象。虽然主题可能首先加载并根据文件数据设置其大小,但其观察者可能尚未加载,因此不会收到大小更改通知。在这种情况下,我们希望在加载之前暂停事件,但将它们排队直到文档完全加载。然后触发所有排队的事件将确保所有观察者与其主题一致。

最后,优化的队列不会对同一主题的同一事件进行多次排队。当通知恢复时,如果稍后排队的事件将通知 (20,20),则通知观察者大小更改为 (10,10) 是没有意义的。因此,每个事件的最新版本是队列应该保留的版本。

如何为课程添加学科能力?

一个典型的主题界面看起来像这样:

class Subject
{
public:
    virtual void Subscribe( aEventId, aDelegate );
    virtual void Unsubscribe( aEventId, aDelegate );
    virtual void Fire( aEventId );
}

问题是我们如何将这个接口添加到各种类中。有三个选项需要考虑:

  • 遗产
  • 作品
  • 多重继承

遗产

在设计模式中,一个ConcreteSubject类继承自一个Subject类。

class ScrollManager: public Subject
{
}

设计模式中的类图和示例代码很容易让人认为这是如何去做的。但是同一本书警告不要继承,并建议优先组合而不是它。这是明智的:考虑一个具有许多复合材料的应用程序,其中只有一些是主题;类应该从Composite类继承Subject吗?如果是这样,许多组合将具有他们不需要的主题功能,并且可能会以始终为空的观察者列表变量的形式存在内存损失。

作品

大多数应用程序和框架会发现只需要将主题功能“插入”到选定的类中,这些类不一定是基类。组合正是允许这样做的。在实践中,一个类将有一个成员mSubject为所有主题方法提供接口,如下所示:

class ScrollManager: public SomeObject
{
public:
    Subject mSubject;
}

这种策略的一个问题是它会为每个主题支持的类带来内存损失(一个成员变量)。另一个是它使访问主题协议有些麻烦:

// Notification within a class composed with the subject protocol.
mSubject.Fire( ... );

// Or the registration from an observer.
aScrollManager.mSubject.Subscribe( ... );

多重继承

多重继承允许我们随意将主题协议组合成一个类,但没有成员组合的陷阱:

class ScrollManager: public SomeObject,
                     public virtual Subject
{
}

这样,我们就摆脱mSubject了前面的例子,所以我们剩下:

// Notification within a subject class.
Fire( ... );

// Or the registration from an observer.
aScrollManager.Subscribe( ... );

请注意,我们public virtual用于主题继承,因此如果子类ScrollManager决定重新继承协议,我们不会获得两次接口。但是可以公平地假设程序员会注意到基类已经是一个主题,因此没有理由重新继承它。

虽然通常不鼓励多重继承,并且并非所有语言都支持它,但为此目的值得考虑。基于 Javascript 的 ExtJs 不支持多重继承,使用 mixins 来实现同样的功能:

Ext.define('Employee', {
    mixins: {
        observable: 'Ext.util.Observable'
    },

    constructor: function (config) {
        this.mixins.observable.constructor.call(this, config);
    }
});

结论

总结本文,观察者模式的通用实现应考虑以下关键点:

  • 主体会触发有状态和无状态事件——后者只能通过推送模型来实现。
  • 对象通常会触发不止一种类型的事件
  • 观察者可以订阅具有多个主题的同一事件。这意味着主题观察者协议应该允许发送者被拼写
  • 任意事件处理程序极大地简化了客户端代码并促进了首选的可变性推送模型;但它们的实现并不直接,并且会导致更复杂的主题代码。
  • 事件处理程序可能需要是虚拟的。
  • 观察者可以存储在全局散列中、每个主题或每个事件中。该选择形成了内存、性能和代码简单性之间的权衡。
  • 理想情况下,观察者只订阅一次相同主题的相同事件。
  • 在订阅后立即让观察者与其主题的状态保持一致是需要牢记的。
  • 事件的触发可能需要使用排队选项暂停,然后再恢复
  • 向类添加主题功能时,值得考虑多重继承。

(第二部分结束)

于 2013-01-31T19:43:04.797 回答