Observer pattern - additional considerations and a generalized implementation in C ++

Structure C ++ MVC Im writing makes heavy use of the observer pattern. I looked in detail at the corresponding chapter in Design Patterns (GoF, 1995) and reviewed many implementations in articles and existing libraries (including Boost).

But since I implemented the template, I could not help but feel that there should be a better way - my client code included lines and fragments, which, as I thought, should have been reorganized into the template itself, if I could find a way to overcome some restrictions on C ++. Also, my syntax never seemed as elegant as the one used in the ExtJs library:

// Subscribing myGridPanel.on( 'render', this.onRender ); // Firing this.fireEvent( 'render', null, node ); 

So, I decided to continue the research, trying to come to a generalized implementation, while prioritizing elegance, readability, and code performance. I believe that I hit the jackpot on the 5th attempt.

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

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

Having done what turned into overwork, I felt that it was easy to share my results with the SO community. So, below I will answer this question:

What additional considerations (for them are presented in design patterns) should programmers consider when implementing an observer pattern?

Focusing on C ++, many of the ones written below will be applied in any language.

Please note:. Since SO limits the answers to 30,000 words, my answer must be provided in 2 parts, but sometimes the second answer (first begins with "Subjects"). Part 1 of the answer is, starting with a class diagram from design patterns.

+7
source share
2 answers

enter image description here

(beginning of part I)

Preconditions

Not all about the condition

Design patterns associate an observer pattern with the state of an object. As you can see from the class diagram above (from design patterns), the state of objects can be set using the SetState() method; upon changing the state, the subject will notify all its observers; then observers can request a new state using the GetState() method.

However, GetState() not the actual method in the base class of the subject. Instead, each specific subject provides its own specialized state methods. Actual code might look like this:

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

What is the state of an object? We define it as a set of state variables - member variables that need to be preserved (for subsequent recovery). For example, both BorderWidth and FillColour can be state variables of the Figure class.

The idea that we can have more than one state variable and therefore the state of objects can change in more than one way is important. This means that objects can fire more than one type of state change event. This also explains why it makes no sense to have the GetState() method in the base class of the subject.

But the observer pattern, which can only process state changes, is incomplete - observers usually observe stateless messages, that is, those that are not related to the state. For example, KeyPress or MouseMove OS events; or events of type BeforeChildRemove , which clearly does not mean an actual state change. These events without state preservation are sufficient to justify the push mechanism - if observers cannot receive information about the change from the subject, all information should be sent with a notification (more on this in brief).

There will be many events

It is easy to understand how in "real life" a subject can trigger many types of events; A quick look at the ExtJs library will show that some classes offer more than 30 events. Thus, the generalized protocol of the observer subject should integrate what Design Designs calls โ€œinterest,โ€ allowing observers to subscribe to a specific event and expose that event only to interested observers.

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

It can be many-to-many

One observer can observe the same event from many objects (which makes the observer-subject relationship many-to-many). For example, a property inspector may listen for a change in the same property of many selected objects. If observers are interested in who sent the notification, the notification will have to include 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 ) { } 

However, it should be noted that in many cases, observers do not care about the identity of the sender. For example, when the subject is singleton or when the observers controlling the event are independent of the subject. Therefore, instead of forcing the sender to be part of the protocol, we should let him do this, leaving him to the programmer, regardless of whether he calls the sender.

Observers

Event handlers

The observer method that processes events (i.e., an event handler) can take two forms: overridden or arbitrary. Providing a critical and complex role in observer implementation, the two are discussed in this section.

Redefined handler

An overridden handler is a solution represented by design patterns. The base class Subject defines the 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 taken into account the idea that subjects usually fire more than one type of event. But processing all events (especially dozens of them) in the OnEvent method OnEvent cumbersome - we can write better code if each event is processed in its own handler; This effectively makes OnEvent event router for 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 of using an overridden handler (base class) is that it can be easily implemented. An observer subscribing to a subject can do this by providing a link to himself:

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

Then the object simply saves the list of Observer objects, and the launch code might look like this:

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

The disadvantage of an overridden handler is that its signature is fixed, which makes passing additional parameters (in the push model) difficult. In addition, for each event, the programmer must support two bits of code: a router ( OnEvent ) and an actual handler ( OnSizeChanged ).

Custom handlers

The first step in overcoming the shortcomings of the redefined OnEvent handler is ... not having it all! It would be nice if we could tell the subject which method should handle each event. Something like that:

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

Note that with this implementation, we no longer need our class to inherit the Observer class; we donโ€™t really need an Observer class. This idea is not new; it has been described in the section on the Herb Sutters 2003 Dr Dobbs herb under the title Generalizing Observer. But implementing arbitrary callbacks in C ++ is not an easy task. Herb used the function object in his article, but unfortunately, the key problem in his proposal was not completely resolved. The problem and its solution are described below.

Since C ++ does not provide its own delegates, we need to use member function pointers (MFPs). C ++ MFPs are pointers to class functions, not pointers to objects, so we had to provide the Subscribe method of both &ConcreteObserver::OnSizeChanged (MFP) and this (object instance). We will call this combination a delegate.

Member Function Pointer + Object Instance = Delegate

The implementation of the Subject class may be based on the ability to compare delegates. For example, in cases where we want to send an event to a specific delegate or when we want to unsubscribe from a specific delegate. If the handler is not virtual and belongs to a class subscription (as opposed to the handler declared in the 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) makes them incomparable. Don Cloughston wrote a fantastic in-depth article on this issue, in which he also provides a C ++ library that overcomes the problem; although it does not comply with the standard, the library works with almost all compilers.

It is worth asking if virtual event handlers are really needed; that is, do we have a scenario in which a subclass of the observer would like to redefine (or expand) the behavior of the event processing of his base class (specific observer). Unfortunately, the answer is that this is entirely possible. Thus, a generic observer implementation should allow virtual handlers, and we will soon see an example of this.

Update protocol

Clause 7 of the implementation of design patterns describes pull vs push models. This section extends the discussion.

Retractable

With the pull model, the object sends minimal notification data, and then the observer is required to obtain additional information from the subject.

We have already established that the pull model does not work for stateless events such as BeforeChildRemove . It might also be worth mentioning that with the pull model, the developer must add lines of code for 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 to keep in mind is that we can implement the pull model using the push model, but not vice versa. Despite the fact that the push model serves the observer with all the necessary information, the programmer may wish to send information with specific events and ask the observers to request additional information from the subject.

Fixed start

Using the push-fixed model, the information that the notification carries is delivered to the processor through the agreed number and type of parameters. This is very simple to implement, but since different events will have a different number of parameters, you need to find some workarounds. The only workaround in this case would be to pack the event information into a structure (or class), which is then passed 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 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 quite lengthy. There are several disadvantages:

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

Secondly, there are some design-related questions that are not easy to answer: will we declare evSizeChanged next to the Size class or next to the object that runs it? If you think about it, then none of them are perfect. Then will the resize notification always have the same parameters, or does it depend on the subject? (Answer: the latter is possible.)

Thirdly, someone will need to create an instance of the event before starting and delete it after. Thus, either the code of the object 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 it:

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

Fourth, there is a casting. We performed the casting inside the handler, but it is also possible to do this within the framework of the Fire() method. But this will either include dynamic casting (high performance), or we are a static acting process that can lead to disaster if the dismissed event and the one that the handler expects do not match.

Fifth, the handler attribute is little readable:

 // What in aEvent? A programmer will have to look at the event class // itself to work this one out. void SomeObserver::OnSizeChanged( evSizeChanged *aEvent ) { } 

In contrast to this:

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

This leads us to the next section.

Volatility Push

As for the code, many programmers would like to see this topic 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 ) { } 

Subject Fire() methods and observer handlers have different arity for each event. The code is readable and as short as we could hope.

This implementation includes very clean client code, but will bring quite complex Subject code (with many function templates and, possibly, other goodies). This is a compromise that most programmers will take - it is better to have complex code in one place (Subject class) than in many (client code); and given that the subject class works flawlessly, the programmer can simply view it as a black box, with little concern for how it is implemented.

What to consider is when and when you need to make sure that arity Fire arity and arctor arity match. We could do this at runtime, and if the two do not match, we will raise a statement. But it would be very nice if we get an error at compile time, which is good for working to declare the arity of each event explicitly, something like this:

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

See later how this event announcement has another important role.

(end of part I)

+11
source

(beginning of part II)

Subjects

Subscription process

What is stored?

Depending on the particular implementation, entities may store the following data when subscribers subscribe:

  • Event id Interest or what event is signed by the observer.
  • An instance of an observer is most often in the form of an object pointer.
  • Member function pointer - if an arbitrary handler is used.

This data will form the parameters of the subscription method:

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

It is worth noting that if an arbitrary handler is used, pointers to member functions are likely to be packaged with an observer instance in a class or struct to form a delegate. And so the Subscribe() method may have the following signature:

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

The actual save (possibly within std::map ) will include the event identifier as a key and delegate as a value.

Implement event identifiers

Defining event identifiers outside the class of objects that trigger them can simplify access to these identifiers. But, generally speaking, the events released by an item are unique to that item. Thus, in most cases, it is logical to declare event identifiers in the topic class.

Although there are several ways to declare event identifiers, only 3 of the most interest are discussed here:

Enums seem at first glance the most logical choice:

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

Comparison of transfers (which will occur during subscription and shelling) is quick. Perhaps the only inconvenience in this strategy is that observers should indicate the class after subscribing:

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

Strings provide the "looser" option for an enumeration, since usually an object class does not declare them as an enumeration; instead, customers will use:

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

The good thing about strings is that most compilers color them differently from other parameters, which somehow improves the readability of the code:

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

But the problem with the strings is that we cannot say at run time if we mistakenly wrote the name of the event. In addition, string comparison takes longer than enumeration comparison, since strings need to be compared by characters.

Types is the last option discussed here:

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

The advantage of using types is that they allow you to overload methods such as Subscribe() (which we will see soon, can solve a common problem with observers):

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

But then again, observers need some extra code to subscribe:

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

Where to store observers?

Clause 1 of the implementation in the design template relates to where observers for each object should be stored. This section adds to this discussion by providing 3 options:

  • Global hash
  • Per object
  • Per event

As shown in the design patterns, one place to store the map of the observer subject is in the global hash table. , ( ). , - - . , javascript - , . , - , .

Design Patterns , . ( - 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. } } }; 

Design Patterns , -, . , -, 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 store-vs-compute. :

enter image description here

:

  • / . , , .
  • . , .

MouseMove , . , . Given:

  • 64-
  • 8

8 1 ( ).

, ?

, ( ).

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

, - , .

, , , , . Consider this code:

 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 , . , ( ) -:

 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 , , AddListener RemoveListener .
  • - , .
  • , ( ButtonListener ), ( buttonClicked ).

, . , / . - .

, , ; , (, ) . , :

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

. wed, , , , . :

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

, , . , , , . , , (, evBeforeDestroy ):

enter image description here

, evBeforeDestroy - , ( ), , , ( ).

, . , , , . wed , , . , .

, . (10,10), (20,20). , - , .

?

:

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

, . There are three options:

  • Composition

ConcreteSubject Subject .

 class ScrollManager: public Subject { } 

, , . . : , ; Composite Subject ? , , , , .

Composition

" , . . 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 , . , , , .

, , , , . ExtJs, Javascript, , mixins :

 Ext.define('Employee', { mixins: { observable: 'Ext.util.Observable' }, constructor: function (config) { this.mixins.observable.constructor.call(this, config); } }); 

Conclusion

:

  • , - push.
  • .
  • . subject-observer .
  • push-; .
  • .
  • , . , .
  • .
  • - -, .
  • , .
  • .

( II)

+7
source

All Articles