我正在编写的 C++ MVC 框架大量使用了观察者模式。我已经彻底阅读了设计模式(GoF,1995)中的相关章节,并查看了文章和现有库(包括 Boost)中的大量实现。
但是当我实现这个模式时,我不禁感到一定有更好的方法 - 我的客户端代码涉及我认为应该重构到模式本身的行和片段,如果我能找到克服的方法一些 C++ 限制。此外,我的语法从未像 ExtJs 库中使用的那样优雅:
// Subscribing
myGridPanel.on( 'render', this.onRender );
// Firing
this.fireEvent( 'render', null, node );
因此,我决定进行进一步的研究,试图得出一个通用的实现,同时优先考虑代码的优雅、可读性和性能。我相信我已经在第 5 次尝试中中了头奖。
名为
gxObserver
的 actual 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 部分是从设计模式中的类图开始的。
最佳答案
(第一部分开始)
先决条件
这不仅仅是关于状态
设计模式将观察者模式与对象“状态”联系起来。如上面的类图(来自设计模式)所示,可以使用 SetState()
方法设置主题的状态;在状态改变时,主题将通知其所有观察者;然后观察者可以使用 GetState()
方法查询新状态。
但是, GetState()
不是主题基类中的实际方法。相反,每个具体的主题都提供了自己的专门状态方法。实际代码可能如下所示:
SomeObserver::onUpdate( aScrollManager )
{
// GetScrollPosition() is a specialised GetState();
aScrollPosition = aScrollManager->GetScrollPosition();
}
什么是对象状态?我们将其定义为状态变量的集合——需要持久化的成员变量(用于以后恢复)。例如,
BorderWidth
和 FillColour
都可以是 Figure 类的状态变量。我们可以拥有多个状态变量——因此一个对象的状态可以以不止一种方式改变——这一想法很重要。这意味着受试者可能会触发不止一种类型的状态变化事件。它还解释了为什么在主题基类中使用
GetState()
方法毫无意义。但是只能处理状态变化的观察者模式是不完整的——观察者观察无状态通知是很常见的,即与状态无关的通知。例如,
KeyPress
或 MouseMove
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/