Experiences of Implementing the Observer Design Pattern (Part 1)

Experiences of Implementing the Observer Design Pattern (Part 1)

By Pete Goodliffe

Overload, 8(37):, May 2000


This article describes my experiences of applying the observer design pattern [ GoF ] in a C++ library. I present it not as an authoritative tutorial on how to use the observer (it isn't) but as fuel for discussion. I hope that my experiences will be beneficial to others.

The C++ library being developed was based on a much older version. This older library required the user to maintain consistency between a number of objects by using the API in a particular way. This is the sure sign of a badly designed API. The obvious solution to the problem was to ensure that the objects maintained their consistency themselves - requiring them to notify each other of certain state changes.

This is a classic context for the observer pattern, and a number of implementation approaches for this pattern were considered, with varying success. This article presents these implementations.

What is the observer pattern?

The observer pattern is a means of implementing object notification in a generic way. It is a useful method of decoupling objects, that is breaking the hard-coded ties between them. Using this pattern, an object does not need to know who/what is interested in notifications it may wish to publish. From this it follows that the interested parties may even change at run time.

We will see the structure of interacting classes in this pattern later in this article.

There are issues with the observer pattern that are largely ignored when it is described, the most important arising from its compile-time decoupling/run-time coupling nature. It can be very hard to find bugs related to the pattern implementation - who is to know what is/is not currently listening to a particular object? Any variation in run-time behaviour from what is expected is much harder to track down than a coding fault that shows up clearly at compile-time.

Design 1: Java-like listener interfaces

I have spent quite some time working in Java (actually, implementing the language) and so was accustomed to the Java event model implementation. My first observer design closely paralleled this mechanism. As it was to turn out it does not translate neatly into C++ - the idioms of one language rarely stretch into another.

The Java idiom uses the language's interface feature. interfaces are equivalent to pure abstract classes in C++ and are an essential part of the Java equivalent of multiple inheritance - you can only inherit multiply from interface s. These particular interface s, known as listener interfaces are defined to receive update events through their member functions. For example, an AWT [ 1 ] MouseListener looks something like the following.

public interface MouseListener {
public void mouseClicked(MouseEvent e);
public void mousePressed(MouseEvent e);
// ...and so on...
}

A class that wants to listen to mouse events implements the MouseListener interface and attaches itself to an AWT user interface component using a method in the java.awt.component class:

public synchronized void addMouseListener(MouseListener l);

This means that a calling mechanism must be implemented in the notifying class for each different type of listener interface.

In the C++ design based on this Java idiom templates were introduced to make the code generic - it is easy to produce a version of this idiom tailored to one particular listener class but much more beneficial to make it a generic mechanism. The possible savings from doing this include reduction in development time, code size, maintenance costs, and the possible number of bugs.

Notifier is defined to be the class that publishes information about its state changes. There exists some type of listener class that receives this information.

The Notifier class uses the type of listener class as it's template parameter. In this way any class that needs to publish state change information can provide an abstract listener interface class and then inherit publicly from a Notifier parameterised by this listener interface type.

An implementation of this listener class can attach/detach itself to the Notifier (which keeps an internal list of which listeners are attached, probably with an STL list). The class implementing the Notifier can then call its protected notify method whenever an event occurs.

To understand this better we need to see it in action. For a particular case let's say we have a Voyeur class that watches objects of the Exhibitionist class and wants to be kept abreast of their changes. The design would be used as follows.

For some Exhibitionist , e , that the Voyeur object uses, it will call e->attach(this) . So if in the future some external agent calls changeA on the Exhibitionist it uses the notify member function to send an aChanged event to all attached ExhibitionistListener s, including the Voyeur object.

This appears to be quite a neat design, the only issue left unresolved is how to implement the notify method in C++. We need to supply an argument to notify that specifies which method on the ExhibitionistListener to call.

This is a candidate for the use of a function object (or functor) . These are classes which may be called via their function operator, operator() . We use function objects like the one shown below. There is an issue over where it should be placed in the namespace / class hierarchy. The logical place is as a public member of the ExhibitionistListener class.

class call_aChanged {
public:
  operator()(ExhibitionistListener *e){
    e->aChanged();
  }
};

Now when the Exhibitionist calls notify it uses code like:

notify(ExhibitionistListener::call_aChanged());

If the listeners are held in an STL list<ExhibitionistListener> , notify can be easily implemented using a template member function thus:

template <class Functor>
Notifier::notify(Functor f) {
  for_each(listeners.begin(),
       listeners.end(), f);
}

If the listener event method needs some parameter (often the source Notifier is required at the very least if the Voyeur listens to several Exhibitionist s at once) then this can be supplied in the constructor of the functor, e.g.

class call_bChanged {
public:
  call_bChanged(int newB) : b(newB) {}
  operator()(ExhitionistListener *e){
    e->bChanged(b);
  }
private:
  int b;
}

This is a fairly neat design, allowing you to create Java-like listener interfaces. It is easy to use. Using multiple inheritance allows the Voyeur to be several different types of listener and so keep itself up to date with respect to a number of different types of Notifier .

However, there are a number of issues with this design. Consider the following example in which a new class, NamedExhibitionist , acts like an Exhibitionist but also adds a stored name facility.

So how should the NamedExhitionistListener fit into the design? It could either

  1. inherit from ExhibitionistListener and be used in place of ordinary ExhibitionistListener s, or

  2. remain distinct from ExhibitionistListener and be used in addition to it

Neither option is useful. In the case of (1) when the NamedExhibitionistListener implementation calls NamedExhibitionist::attach(this) , the this parameter is converted into a base ExhibitionistListener pointer. The NamedExhibitionistListener 's extra functionality is not available to the NamedExhibitionist , since Exhibitionist::notify cannot call nameChanged on an ExhibitionistListner . dynamic_cast<NamedExhibitionist> could be employed by the functor but in often used code it would present a significant performance hit.

We can try to get around this problem by making NamedExhibitionist inherit from both Exhibitionist and a new version of the Notifier , a Notifier<NamedExhibitionistListener> . By doing this though, things get really nasty. The NamedExhibitionist has two attach methods. When you call attach , which copy is called? The NameExhibitionist::attach hides its base class' method. So it is necessary to qualify the attach with a class name and call either Exhibitonist::attach or NamedExhibitonist::attach on the NamedExhibitionist object.

Similarly, the compiler cannot deduce which notify to call, and so the NamedExhibitionist needs to call Notifer<NamedExhibitionistListener>::notify(NamedExhibionistListener::call_nameChanged()) . Euch. Also, when using this method, if a Voyeur cares both about the basic Exhibitionist changes and the name changes in NamedExhibitionist then it has to attach itself twice to the object.

This Java-like method still works but in certain cases the Notifier API is burdensome. As stated before, this is a sure indication of a bad design.

Key benefits
  • For simple class hierarchies it is clear and easy to use.

  • Provides Java-like listener interfaces, one method is called for each event that could occur, no unnecessary switches on event type.

  • Information describing the event can be passed to listener methods.

Drawbacks
  • Requires a functor for each listener method.

  • Although a class can listen to any number of different Notifier s, it can only be one kind of Notifier without incurring a burdensome API.

Design 2: Remove templates

A different approach is to get rid of the parameterised Notifier and have a single Listener base interface class. This avoids problems involved when adding extra functionality to the Exhibitionist through inheritance. In doing this we lose the ability to have different methods called for each kind of event raised, which was the Java listener style.

We can try to compensate for this by using an Event base class to represent the kind of event that has occurred.

Now applying this framework to the previous example:

This kind of hierarchy involving a set of congruent class trees is known as covariant typing. Practically the only language that provides any support for covariant typing is Eiffel. There are different schools of thought on whether it is a good or bad thing. However, C++ provides no support for it and so using it is not very useful in this context.

If a Listener is attached to a NamedExhibitionist and gets notified of an action through a call to its update method, it will have to employ RTTI to deduce what sort of Event has been passed through to it. This is not a very clever mechanism for such a often-performed task. We will also need to have some additional mechanism for representing the information relating to each type of event within each Event class.

If you think that this is a good way to code then you should be forced to write in BASIC for the rest of your life.

Key benefits
  • Any class can listen to more than one Notifier .

  • Any Notifier can publish more than one kind of Event.

Drawbacks
  • Needs RTTI and a sick mind to be used.

Design 3: Simplicity

The practical solution to the problem was to strip back all the fancy bits and have a simple Notifier , and a simple Listener . The best designs are nearly always the most simple. This is the class hierarchy used:

This allows an object to listen to as many Notifier s as required - the source is identified in the event methods. There is no problem with being more than one kind of Notifier - there is only one.

A reason code ( rc ) describing the type of change is passed around by notify . The definitions for this integer could be held as static const int s in either the Notifier imple-mentation, or in some related scope. If the Listener is attached to more than one 'kind' of Notifier it can inspect the source reference it is passed to see which type of object it is (a Listener will always know what objects it has attached itself to) and interpret the integer accordingly.

The Notifier has to be passed around as a parameter otherwise you can only be one kind of Listener - it would be impossible to determine what the integer reference referred to in the presence of more than one Notifier .

The code for this is fairly simple and looks as follows. Note that again we use functors as the mechanism for dispatching messages to Listener s. This time, though, we only need two.

class Notifier;

class Listener {
public:
  virtual ~Listener() {}
  virtual void updated(
        Notifier *source, int rc){}
  virtual void deleted(
                Notifier *source){}
};

class Notifier {
public:
  void attach(Listener *l) 
        { listeners.push_back(l); }
  void detach(Listener *l) 
          { listeners.remove(l); }
protected:
  class NotifyUpdated {
  public:
    NotifyUpdated(Notifier *n, int rc)
        : source(n), rc(rc) {}
    void operator()(Listener *l)
        { l->updated(source, rc); }
  private:
    Notifier *source;
    int *rc;
  };
  
void notify(int rc = 0) {
  for_each(listeners.begin(),
          listeners.end(),
          NotifyUpdated(this, rc));
  }
private:
  class NotifyDeleted {
  public:
    NotifyDeleted(Notifier *n) 
                    : source(n) {}
    void operator()(Listener *l)
          { l->deleted(source); }
    private:
      Notifier *source;
  };
  virtual ~Notifier() {
    for_each(listeners.begin(),
            listeners.end(),
            NotifyDeleted(this));
  }
  list<Listener *> listeners;
};

Note the addition of the deleted event member function in the Listener which is called by the Notifier 's destructor. I found that many events tracked by my classes were 'about to be deleted' messages, used to prevent any dangling references to dead objects existing in the system. It makes sense to roll these up into a separate event and provide explicit support.

Another important point to note is the virtual destructors. This ensures that when a Notifier is destroyed the deleted message is sent. Without this code along the lines of

void foo()  {
  Exhibitionist *e = new Exhibitionist();
  Notifier    *n = e;
// ...do something...
  delete n; 
// possibly in another function
}

would only call the Notifier destructor and not the Exhibitionist destructor as well. This is such a simple piece of C++, yet it's frightening how often it is overlooked. All destructors in classes with virtual functions should be declared virtual for this reason. It is good practice to do this even if you do not think that a class you are writing will ever be inherited from.

Applying this new framework to the Exhibitionist / Voyeur classes:

So the Voyeur object will call attach on an Exhibitionist object. From then on, whether it was an Exhibitionist or a NamedExhibitionist , it will receive all updates. A parameter may tell it what sort of update occurred.

This design is not as neat as the Java event model, but the benefits are that it is generic enough, is simple to use and to understand. We have to be careful, though. In particular we have to ensure that the definition of integer update reason codes for the NamedExhibitionist codes must not cross into the Exhibitionist ones.

I toyed with the idea of inheriting specific kinds of Listener that describe the event codes as shown

However, this gains us nothing. When a call to attach is made, it is not possible to check that you are passing the correct type of Listener . However, non inherited versions of this class are a neat place to group reason code values.

Key benefits
  • Simple design.

  • It works.

Drawbacks
  • The integer reason code is messy.

  • Again, not really the Java listener idiom. Ho hum.

  • Presents a number of possible bug sources - read on...

Design 4: Safety

Using this design for quite some time revealed minor problem areas, especially when a number of more dynamic classes (whose objects are frequently created and deleted) used the observer mechanism. This is an example of the issue mentioned in the observer pattern introduction - it can be notoriously difficult to find run-time problems manifested with the observer pattern.

A number of Listener s attach themselves to more than one dynamically-lived Notifier . It is very easy to not override the default deleted member function, and so lose track of which objects still exist and which do not. Errors can easily occur from trying to dereference a pointer to an object that you think still exists, but in fact does not. This is problem must be carefully watched for.

An additional source of bugs with this mechanism is failing to detach a Listener from a Notifier before destroying it. Often a destructor will have to detach from a number of objects, but it is very easy to miss one out.

We can introduce mechanisms to combat this fairly easily with the following revision to the code:

class Notifier {
private:
  void attach(Listener *l)
  void detach(Listener *l)
  virtual ~Notifier();
  friend class Listener
};
class Listener  {
  virtual updated(Notifier *source,
                   int rc) {}
  virtual deleted(Notifier *source){
    attachments.remove(source);
// If you override this, ensure that you 
// perform this operation
  }
protected:
  void attach(Notifier *n) {
    n->attach(this);
    attachments.push_back(n);
  }
  void detach(Notifier *n) {
    n->detach(this);
    attachments.remove(n);
  }
  list<Notifier *> attachments;
private:
  virtual ~Listener() {
  for_each(attachments.begin(),
          attachments.end(),
      functor_that_calls_detach(this));
  }
};

Now a Voyeur will attach itself to an Exhibitionist e with code like the following.

attach(e);

Note that the Listener is now calling attach on itself.

The use of friendship is a matter of style, however it ensures that other classes cannot call Notifier::attach and circumvent this protection system. We still have a possible hole if the Listener implementor overrides the deleted method but forgets to include the attachments.remove(source) line in their version.

The problem with this solution is that the Voyeur implementation will probably keep its own e pointer for internal use, so the class will now always have two pointers to e . If you want this added protection then this is the price you pay.

Personally, I do not use this extra functionality. It is a matter of debate whether this automatic detachment should be an application object's responsibility or the library's.

Key benefits
  • Safer listener deletion - less chance of leaving dangling references in Notifiers.

Disadvantages:
  • Two pointers to Notifiers will often be kept.

Other issues

Issue 1:

If we use an object that derives from two objects, both of which are Listener s, then two versions of the Listener abstract interface will be inherited. This can lead to all sorts of confusion as to which copy of the Listener base class is used in which situation. Fortunately there is a language facility to help us in this respect: virtual inheritance.

If we inherit from the Listener class virtually then we are guaranteed to have only one copy of this base class. It should be noted that this may produce a small performance hit due to implementation issues. Virtual inheritance is covered in depth in section 15.2.4 of [ Stroustrup ] and in [ Meyers ].

Issue 2:

The visibility level of the Notifier::notify method is alterable according to taste. Some implementers choose to make it public. This would mean that any object is capable of causing a notification from any Notifier . In my opinion this is a little strange.

However, it does allow certain operational optimisations. For example, say you are going to make a number of changes to an object's state through its member functions. Each of those will generate a notification, but you would rather get one notification at the end rather than several one after the other. If it is up to you to trigger the notification by manually calling notify then you can choose what is sent and when. The danger with this style of usage, though, is that you will almost certainly forget to call notify at some point and then wonder why your program does not work as expected.

The observer design can be bent to try to accommodate this kind of delayed notification, but that is another story…

Issue 3:

What you do in a notify method should be carefully controlled. You should be aware of any changes that you make that could cause cascading notifications to be sent, especially if there is a circular dependency that will cause an infinite loop.

Another possible cause of difficulty is deleting an object inside a notify method. This is especially likely to cause program instability if you are deleting (directly or indirectly) the object that caused the notification. It may be the case that you are not the last object to receive that particular notify call. A subsequent Listener object's notify will be called with a source pointer that is now invalid. What will happen then is anyone's guess, but we can be sure that it will not always be pretty.

Issue 4:

As with any other coding problem, the presence of multiple threads opens a whole new can of worms. The observer pattern relies on run-time dependencies, as do multiple threads. This makes the problem an order of magnitude more difficult.

Conclusions

We've seen a number of different approaches to the implementation of the observer design pattern. What lessons do we learn? Even if you are not directly involved with the observer pattern, there are a number of principles we can walk away with:

  • Idioms from one language don't always travel easily into another.

  • There is more than one way to implement a particular design pattern - which one you choose will depend on the context in which you need to apply it.

Next time

In the next article we'll see how we can extend this Notifier / Listener framework to get closer to the Java-like interfaces I have been pining for.

References

[GoF] Gamma, Helm, Johnson, Vlissades. Design Patterns - Elements of Reusable Object-Oriented Software . Addison-Wesley, 1995. Page 293.

[Stroustrup] Bjarne Stroustrup. The C++ Programming Language 3ed . Addison Wesley. Section 15.2.4. Page 396.

[Meyers] Scott Meyers. More Effective C++ . Addison Wesley. Item 24, Page 29.



[ 1 ]






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.