观察者模式 - 进一步的考虑和广义的C ++实现 [英] The Observer Pattern - further considerations and generalised C++ implementation

查看:165
本文介绍了观察者模式 - 进一步的考虑和广义的C ++实现的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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



但是当我正在实现这个模式的时候,我忍不住有一个更好的方式 - 我的客户端代码涉及到我觉得应该被重构到模式本身的线和片段,只有我能找到一种方式克服了一些C ++的限制。此外,我的语法从来没有像ExtJs库中使用的那样优雅:

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

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

所以我决定进行进一步的研究,试图达成一般化的实现,同时优先考虑代码优势,可读性和性能。我相信我在第五次尝试中击中了大奖。



。但是,C ++中任意回调的执行并不是一个直截了当的事情。 Herb在他的文章中使用了函数,但不幸的是他的提案中的一个关键问题没有完全解决。问题及其解决方案如下所述。



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


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


执行主题 class 可能会依赖于比较代表例如,如果我们希望将某个事件触发给特定代表,或者当我们要取消订阅特定代表时。如果处理程序不是虚拟的,并且属于订阅类(与基类中声明的处理程序相反),则代理可能是可比较的。但是在大多数其他情况下,编译器或继承树的复杂性(虚拟或多重继承)将使它们无法比拟。 唐·克斯顿(Don Clugston)撰写了一个神奇的深入文章关于这个问题,他还提供了一个克服问题的C ++库;虽然不符合标准,但图书馆几乎与每个编译器一起工作。



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



更新协议



设计模式的实现点7描述了拉vs推模型。





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



我们已经确定,拉模型不适用于无状态事件,例如 BeforeChildRemove 。也许值得一提的是,使用拉模型,程序员需要为推模型不存在的每个事件处理程序添加代码行:

  //拉模型
void SomeClass :: OnSizeChanged(Subject * aSubject)
{
//恼人 - 我希望我没有写这条线。
大小iSize = aSubject-> GetSize();
}

//推模型
void SomeClass :: OnSizeChanged(Subject * aSubject,Size aSize)
{
// Nice!我们已经有了大小。
}

另一件值得记住的事情是,我们可以使用推送模型实现拉模型但不是相反。虽然推送模式为观察者提供了所需的所有信息,但程序员可能希望不发送具体事件的信息,并让观察者向主题询问更多信息。



< h3>固定推送

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

  //事件基类
struct evEvent
{
};

//具体事件
struct evSizeChanged:public evEvent
{
//指定了所有参数的构造函数。
evSizeChanged(图* aSender,Size& aSize)
:mSender(aSender),mSize(aSize){}

//只有发件人指定的较短构造函数。
evSizeChanged(图* aSender)
:mSender(aSender)
{
mSize = aSender-> GetSize();
}

图* mSender;
大小mSize;
};

//观察者的事件处理程序,它使用事件基类。
void SomeObserver :: OnSizeChanged(evEvent * aEvent)
{
//我们需要将事件参数转换为派生事件类型。
evSizeChanged * iEvent = static_cast< evSizeChanged *>(aEvent);

//现在我们可以得到大小。
大小iSize = iEvent-> mSize;
}

尽管主题与其观察者之间的协议很简单,但实际实现相当冗长有一些缺点要考虑:



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



其次,有一些设计问题涉及不容易回答:我们应该声明 evSizeChanged Size 类一起,或与其触发的主题一起?如果你考虑,也不是理想的。那么,尺寸变化通知是否始终携带相同的参数,还是依赖于受试者? (答案:后者是可能的。)



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

  // Argh! 3行代码来触发一个事件。 
evSizeChanged * iEvent = new evSizeChanged(this);
Fire(iEvent);
删除iEvent;

或者我们这样做:

  //如果你是一个程序员看这条线而不是放松! 
//虽然你看不到它,但Fire方法会在退出时删除这个
//事件,所以没有内存泄漏!
//是的,是的...我知道,这是一个糟糕的编程习惯,但它有效。
//哦,我不会在每次调用Fire()时给出这样的评论,
//我只是希望这是第一个Fire(),你会看,只是
//记住。
Fire(new evSizeChanged(this));

第四,有一个铸造业务。我们在处理程序中完成了转换,但也可以在主题的 Fire()方法中进行。但是这将涉及动态投射(性能代价高昂),或者我们做一个静态投射,如果事件被触发,并且处理程序期望不匹配,可能会导致灾难。



第五,处理程序可读性很小:

  // aEvent中有什么?一个程序员将​​不得不看事件类
//本身来工作。
void SomeObserver :: OnSizeChanged(evSizeChanged * aEvent)
{
}

与此相反:

  void SomeObserver :: OnSizeChanged(ZoomManager * aManager,Size aSize)
{
}

这导致我们到下一节。



Vari-arity Push



就代码而言,很多程序员都希望看到这个主题代码:

  void Figure :: AdjustBounds(Size& aSize)
{
//在这里做某事。

//现在fire
Fire(evSizeChanged,this,aSize);
}

void图::隐藏()
{
//在这里做某事。

//现在fire
Fire(evVisibilityChanged,false);
}

此观察者代码:

  void SomeObserver :: OnSizeChanged(图* aFigure,Size aSize)
{
}

void SomeObserver :: OnVisibilityChanged aIsVisible)
{
}

主题的 Fire ()方法和观察者处理程序对每个事件有不同的特征。代码是可读的,尽可能的短,我们希望的。



这个实现涉及一个非常干净的客户端代码,但会带来一个相当复杂的主题代码(具有多种功能模板和可能的其他好处)。这是大多数程序员将采取的权衡 - 最好在一个地方(主题类)中复制代码,而不是许多(客户端代码);并且考虑到课程学习班是非常成功的,程序员可能会把它视为一个黑盒子,关心如何实现这个黑匣子。



值得考虑的是什么以及何时确保 Fire arity和处理程序相匹配。我们可以在运行时做到这一点,如果两者不匹配,我们提出一个断言。但是,如果我们在编译时遇到错误,那么这样做真的很好,为了工作,我们必须明确声明每个事件的真实性,如下所示:

  class图:public Composite,
public virtual Subject
{
public:
// DeclareEvent宏将以某种方式存储arity,将
//然后由Subscribe()和Fire()使用,以确保在编译时匹配
//。
DeclareEvent(evSizeChanged,Figure *,Size)
DeclareEvent(evVisibilityChanged,bool)
};

我们稍后会看到这些事件声明有另外重要的作用。



(第一部分的结尾)


A C++ MVC framework I’m writing makes heavy use of the observer pattern. I have had a thorough read of the related chapter in Design Patterns (GoF, 1995) and had a look at a multitude of implementations in articles and existing libraries (including Boost).

But as I was implementing the pattern, I could not help the feeling that there must be a better way - my client code involved lines and snippets that I felt should have been refactored into the pattern itself, if only I could find a way to overcome a few C++ limitations. Also, my syntax never appeared as elegant as that used in the ExtJs library:

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

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

So I decided to conduct further research in attempt to arrive at a generalised implementation, while prioritizing code elegancy, readability and performance. I believe I've hit the jackpot on the 5th attempt.

The actual implementation, called gxObserver, is available on GitHub; it is well-document and the readme files spell out the pros as well as the cons. Its syntax is:

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

// Firing
Fire( evAge, 69 );

Having done what turned into an excessive work, I felt it would be just to share my findings with the SO community. So below I will answer this question:

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

While focused around C++, many of the written below will apply in any language.

Please note: As SO limits answers to 30000 words, my answer had to be provided in 2 parts, but sometimes the second answer (the one starting with 'Subjects' shows up) first. Part 1 of the answer is the one starting with the class diagram from Design Patterns.

解决方案

(start of part I)

Prerequisites

It’s Not All About State

Design Patterns ties the observer pattern to an object 'state'. As seen in the class diagram above (from Design Patterns), a subject’s state can be set using the SetState() method; upon state change the subject will notify all of its observers; then observers can inquire the new state using the GetState() method.

However, GetState() is not an actual method in subject base class. Instead, each concrete subject provides its own specialised state methods. An actual code might look like this:

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

What’s an object state? We define it as the collection of state variables – member variables that need to be persisted (for later reinstatement). For instance, both BorderWidth and FillColour could be state variables of a Figure class.

The idea that we can have more than one state variable – and thus an object’s state can change in more than one way – is important. It means that subjects are likely to fire more than one type of state change event. It also explains why it makes little sense to have a GetState() method in the subject base class.

But an observer pattern that can only handle state changes is an incomplete one – it is common for observers to observe stateless notifications, i.e., ones that are not state related. For example, the KeyPress or MouseMove OS events; or events like BeforeChildRemove, which clearly does not denote an actual state change. These stateless events are enough to justify a push mechanism – if observers cannot retrieve the change information from the subject, all the information will have to be served with the notification (more on this shortly).

There Will Be Many Events

It is easy to see how in 'real life' a subject may fire many types of events; a quick look at the ExtJs library will reveal that some classes offer upwards of 30 events. Thus, a generalised subject-observer protocol has to integrate what Design Patterns calls an 'interest' – allowing observers to subscribe to a particular event, and subjects to fire that event only to interested observers.

// A subscription with no interest.
aScrollManager->Subscribe( this );

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

It Could Be Many-to-many

A single observer may observe the same event from a multitude of subjects (making the observer-subject relationship many-to-many). A property inspector, for instance, may listen to a change in the same property of many selected objects. If observers are interested in which subject sent the notification, the notification will have to incorporate the sender:

SomeSubject::AdjustBounds( aNewBounds )
{
    ...
    // The subject also sends a pointer to itself.
    Fire( "BoundsChanged", this, aNewBounds );
}

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

It is worth noting, however, that in many cases observers don't care about the sender identity. For instance, when the subject is a singleton or when the observer’s handling of the event is not subject-dependant. So instead of forcing the sender to be part of a protocol we should allow it to be, leaving it to the programmer whether or not to spell the sender.

Observers

Event Handlers

The observer’s method which handles events (ie, the event handler) can come in two forms: overridden or arbitrary. Providing a critical and complex part in the implementation of the observers, the two are discussed in this section.

Overridden Handler

An overridden handler is the solution presented by Design Patterns. The base Subject class defines a virtual OnEvent() method, and subclasses override it:

class Observer
{
public:
    virtual void OnEvent( EventType aEventType, Subject* aSubject ) = 0;
};

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

Note that we have already accounted for the idea that subjects typically fire more than one type of event. But handling all events (particularly if there are tens of them) in the OnEvent method is unwieldy - we can write better code if each event is handled in its own handler; effectively, this makes OnEvent an event router to other handlers:

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

The advantage in having an overridden (base class) handler is that it is dead easy to implement. An observer subscribing to a subject can do so by providing a reference to itself:

void ConcreteObserver::Hook()
{
    aSubject->Subscribe( evSizeChanged, this );
}

Then the subject just keeps a list of Observer objects and the firing code might look like so:

void Subject::Fire( aEventType )
{
    for ( /* each observer as aObserver */)
    {
        aObserver->OnEvent( aEventType, this );
    }
}

The disadvantage of overridden handler is that its signature is fixed, which makes the passing of extra parameters (in a push model) tricky. In addition, for each event the programmer has to maintain two bits of code: the router (OnEvent) and the actual handler (OnSizeChanged).

Arbitrary Handlers

The first step in overcoming the shortfalls of an overridden OnEvent handler is… by not having it all! It would be nice if we could tell the subject which method is to handle each event. Something like so:

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

Notice that with this implementation we no longer need our class to inherit from the Observer class; in fact, we don’t need an Observer class at all. This idea is not a new one, it was described in length in Herb Sutter’s 2003 Dr Dobbs article called ‘Generalizing Observer’. But, the implementation of arbitrary callbacks in C++ is not a straightforward afair. Herb was using the function facility in his article, but unfortunately a key issue in his proposal was not fully resolved. The issue, and its solution are described below.

Since C++ does not provide native delegates, we need to use member function pointers (MFP). MFPs in C++ are class function pointers and not object function pointers, thus we had to provide the Subscribe method with both &ConcreteObserver::OnSizeChanged (The MFP) and this (the object instance). We will call this combination a delegate.

Member Function Pointer + Object Instance = Delegate

The implementation of the Subject class may rely on the ability to compare delegates. For instance, in cases we wish to fire an event to a specific delegate, or when we want to unsubscribe a specific delegate. If the handler is not a virtual one and belongs to the class subscribing (as opposed to a handler declared in a base class), delegates are likely to be comparable. But in most other cases the compiler or the complexity of the inheritance tree (virtual or multiple inheritance) will render them incomparable. Don Clugston has written a fantastic in-depth article on this problem, in which he also provides a C++ library that overcomes the problem; while not standard compliant, the library works with pretty much every compiler out there.

It is worth asking whether virtual event handlers are something we really need; that is, whether we may have a scenario where an observer subclass would like to override (or extend) the event handling behaviour of its (concrete observer) base class. Sadly, the answer is that this is a well possible. So a generalised observer implementation should allow virtual handlers, and we shall soon see an example of this.

The Update Protocol

Design Patterns’ implementation point 7 describes the pull vs push models. This section extends the discussion.

Pull

With the pull model, the subject sends minimal notification data and then the observer is required to retrieve further information from the subject.

We have already established that the pull model won’t work for stateless events such as BeforeChildRemove. It is perhaps also worth mentioning that with the pull model the programmer is required to add lines of code to each event handler that would not exist with the push model:

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

Another thing worth remembering is that we can implement the pull model using a push model but not the other way around. Although the push model serves the observer with all the information it needs, a programmer may wish to send no information with specific events, and have the observers enquiring the subject for more information.

Fixed-arity Push

With a fixed-arity push model, the information a notification carries is delivered to the handler via an agreed amount and type of parameters. This is very easy to implement, but as different events will have a different amount of parameters, some workaround has to be found. The only workaround in this case would be to pack the event information into a structure (or a class) that is then delivered to the handler:

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

Now although the protocol between the subject and its observers is simple, the actual implementation is rather lengthy. There are a few disadvantages to consider:

First, we need to write quite a lot of code (see evSizeChanged) for each event. A lot of code is bad.

Second, there are some design questions involved that are not easy to answer: shall we declare evSizeChanged alongside the Size class, or alongside the subject that fires it? If you think about it, neither is ideal. Then, will a size change notification always carry the same parameters, or would it be subject-dependent? (Answer: the latter is possible.)

Third, someone will need to create an instance of the event before firing, and delete it after. So either the subject code will look like this:

// Argh! 3 lines of code to fire an event.
evSizeChanged *iEvent = new evSizeChanged( this );
Fire( iEvent );
delete iEvent;

Or we do this:

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

Forth, there’s a casting business going on. We have done the casting within the handler, but it is also possible to do it within the subject’s Fire() method. But this will either involve dynamic casting (performance costly), or we do a static cast which could result in a catastrophe if the event being fired and the one the handler expects do not match.

Fifth, the handler arity is little readable:

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

As opposed to this:

void SomeObserver::OnSizeChanged( ZoomManager* aManager, Size aSize )
{
}

Which leads us to the next section.

Vari-arity Push

As far as looking at code goes, many programmers would like to see this subject code:

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

And this observer code:

void SomeObserver::OnSizeChanged( Figure* aFigure, Size aSize )
{
}

void SomeObserver::OnVisibilityChanged( aIsVisible )
{
}

The subject’s Fire() methods and the observer handlers have different arity per event. The code is readable and as short as we could have hoped for.

This implementation involves a very clean client code, but would bring about a rather complex Subject code (with a multitude of function templates and possibly other goodies). This is a trade-off most programmers will take – it is better to have complex code in one place (the Subject class), than in many (the client code); and given that the subject class works immaculately, a programmer might just regard it as a black-box, caring little about how it is implemented.

What is worth considering is how and when to ensure that the Fire arity and the handler arity match. We could do it in run-time, and if the two don’t match we raise an assertion. But it would be really nice if we get an error during compile time, for which to work we’ll have to declare the arity of each event explicitly, something like so:

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

We’ll see later how these event declaration have another important role.

(end of part I)

这篇关于观察者模式 - 进一步的考虑和广义的C ++实现的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆