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

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

问题描述

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



但是当我实现模式,我不能帮助感觉,必须有一个更好的方式 - 我的客户端代码涉及线和片段,我觉得应该被重构到模式本身,如果只有我可以找到一种方式克服了几个C ++的局限性。此外,我的语法从来不像ExtJs库中使用的那样优雅:

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

//触发
this.fireEvent('render',null,node);因此,我决定进行进一步的研究,以达到一个广义的实现,同时优先考虑代码优势,可读性和性能。我相信我已经在第五次尝试中赚了大奖。



。但是,在C ++中的任意回调的实现不是一个简单的。 Herb在他的文章中使用了功能工具,但不幸的是他的提案中的一个关键问题没有完全解决。该问题及其解决方案如下所述。



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


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


主题的实现可能依赖于比较代表。例如,在我们希望向特定代表触发事件的情况下,或者当我们要取消订阅特定代表时。如果处理程序不是虚拟的并且属于订阅类(而不是在基类中声明的处理程序),那么委托可能是可比较的。但在大多数其他情况下,编译器或继承树(虚拟或多重继承)的复杂性将使它们无法比拟。 Don Clugston撰写了一篇精彩的深入文章对这个问题,其中他还提供了一个克服这个问题的C ++库;虽然不是标准兼容的,但是该库几乎与每个编译器一起工作。



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



更新协议



设计模式的实现点7描述了pull vs push模型。



Pull



使用pull模型,主题会发送最少的通知数据,然后



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

  //拉模型
void SomeClass :: OnSizeChanged(Subject * aSubject)
{
// Annoying - 我希望我不必写这行。
Size iSize = aSubject-> GetSize();
}

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

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



< h3>固定推送

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

  //事件基类
struct evEvent
{
};

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

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

图* mSender;
Size mSize;
};

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

//现在我们可以得到尺寸。
Size iSize = iEvent-> mSize;
}



现在虽然主题和它的观察者之间的协议很简单,是相当冗长。有几个缺点需要考虑:



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



其次,有一些设计问题不容易回答:我们应该声明 evSizeChanged Size 类旁边,或者在激发它的主题旁边?如果你想想,两者都不理想。那么,尺寸变化通知总是携带相同的参数,还是取决于主体? (回答:后者是可能的。)



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

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

或者我们这样做:

  //如果你是一个程序员看这行而不只是放松! 
//虽然你不能看到它,Fire方法将删除这个
//事件,当它退出时,所以没有内存泄漏!
//是的,是的...我知道,这是一个坏的编程实践,但它的工作原理。
//哦,我不会把这样的评论每一次调用Fire(),
//我只是希望这是第一个Fire(),你会看到,只是
//记住。
Fire(new evSizeChanged(this));

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



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

  // AEvent中有什么?程序员必须查看事件类
//本身才能工作。
void SomeObserver :: OnSizeChanged(evSizeChanged * aEvent)
{
}

与此相反:

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

这将引导我们进入下一部分。



变量推送



对于代码看来,许多程序员想要看到这个主题代码:

  void图:: AdjustBounds(Size& aSize)
{
//在这里做一些事情。

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

void图:: Hide()
{
//在这里做一些事情。

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

此观察员代码:

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

void SomeObserver :: OnVisibilityChanged aIsVisible)
{
}

主题 ()方法和观察者处理程序每​​个事件具有不同的arity。



这个实现涉及到一个非常干净的客户端代码,但会带来一个相当复杂的 Subject 代码(带有多个函数模板和可能的其他好东西)。这是大多数程序员将要采取的折衷 - 最好是在一个地方(主题类),而不是在许多(客户端代码)有复杂的代码;并且考虑到主题类完美地工作,程序员可能只是把它看作一个黑盒子,关心它如何实现。



值得考虑的是如何并且何时确保 Fire arity和处理器arity匹配。我们可以在运行时做,如果两者不匹配,我们提出一个断言。但是如果我们在编译期间得到一个错误,这将是非常好的,为了工作,我们必须明确声明每个事件的arity,像这样:

  class Figure:public Composite,
public virtual Subject
{
public:
// DeclareEvent宏将以某种方式存储arity将
//然后被Subscribe()和Fire()使用,以确保在编译时arity匹配
//。
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天全站免登陆