Click here to Skip to main content
15,881,812 members
Articles / Programming Languages / C++

Implementing a Subject/Observer Pattern with Templates

Rate me:
Please Sign up or sign in to vote.
4.71/5 (34 votes)
10 Mar 20034 min read 174.4K   1.1K   59   24
Using C++ Templates to overcome some of the original Subject/Observer design pattern problems

Introduction

The traditional Subject/Observer pattern as described in the famous Design Patterns book has one annoying disadvantage: the observer does not get a handle to the notifying subject in its update method. This means that:

  • The observer needs to store a pointer to the subject to which it is attached.
  • If the observer is attached to multiple objects, it has no way of determining which one of them is notifying him.

This article tries to solve these problems by making use of C++ templates.

The Original Design Pattern

The original design pattern makes use of two base classes: Subject and Observer. The Subject base class contains all the logic of storing all the attached observers. The Observer class just contains a pure virtual method (update()), that needs to be filled in by the inheriting observer class. Please read the 'Design Patterns' book for all details. The update() method of the Observer class does not get any parameters, which means that a class that inherits from Observer does not know where the notification came from. It is not difficult to add a 'Subject' parameter to the update method, but since the real subject inherited from the 'Subject' base class, the observing class always needs to perform a down-cast, which could be dangerous.

The Solution

Instead of defining two base classes, we will define two template classes. Both template classes will be based on the subject-class that is able to notify other classes (the observers).

C++
template <class T>
class Observer
   {
   public:
      Observer() {}
      virtual ~Observer() {}
      virtual void update(T *subject)= 0;
   };

The first enhancement here is that our pure virtual update method gets a pointer to the subject as argument; not the base Subject class (which is shown hereafter), but the class that was given as parameter to the template definition.

C++
template <class T>
class Subject
   {
   public:
      Subject() {}
      virtual ~Subject() {}
      void attach (Observer<T> &observer)
         {
         m_observers.push_back(&observer);
         }
      void notify ()
         {
         std::vector<Observer<T> *>::iterator it;
         for (it=m_observers.begin();it!=m_observers.end();it++) 
              (*it)->update(static_cast<T *>(this));
         }
   private:
      std::vector<Observer<T> *> m_observers;
   };

Here, we defined the basic Subject class/template. The attach method simply adds the observer (which is of the basic Observer<T> class) to a vector. The notify method simply notifies all observers. Both templates can be used in any situation where the Subject/Observer pattern can be used. The following classes describe how they are used.

C++
class Temperature : public Subject<Temperature>
   {
   public:
      Temperature() {}
      ~Temperature() {}
      void temperatureChanged () {notify();}
      void getTemperature() {std::cout << 
         "   Getting the temperature." << std::endl;}
   };

Our Temperature class is a class that monitors the temperature, and notifies its observers when the temperature changes. As you can see, all it has to do is call the notify() method. The getTemperature method simply writes something on the screen, but of course in real-life situations, it should return the actual temperature. Taking a look at the implementation of the notify() method. It simply calls the update() method of all attached observers, but with itself as argument. Since 'this' (which is the Subject<t> class) is cast to the type T, the update() method of the observer will get the correct argument type, as shown in the following example:

C++
class PanicSirene : public Observer<Temperature>
   {
   public:
      PanicSirene() {}
      ~PanicSirene() {}
      void update (Temperature *subject)
         {
         std::cout << "Temperature was changed, sound the sirene" 
               << std::endl;
         subject->getTemperature();
         }
   };

As you can see, a pointer to the Temperature instance that triggers the notification is given as argument to the update method. The observing class (PanicSirene in this case) can simply call any method of the notifying subject, in this case simply getTemperature. The following source shows how it is effectively used:

C++
Temperature       temp;
PanicSirene       panic;

temp.attach (panic);

temp.temperatureChanged ();

The following output will be generated:

Temperature was changed, sound the sirene
   Getting the temperature.

Observing Multiple Subjects of a Different Type

The templates are still easy to use if you need to attach the observer to multiple objects. Suppose that we have a similar subject-class for measuring the pressure.

C++
class Pressure : public Subject<pressure>
   {
   public:
      Pressure() {}
      ~Pressure() {}
      void pressureChanged () {notify();}
      void getPressure() {std::cout << "   Getting the pressure." 
          << std::endl;}
   };

If we want to show both the temperature and pressure in a window that shows all environment-related information, we simply create our EnvironmentWindow like this:

C++
class EnvironmentWindow : public Observer<Temperature>, 
        public Observer<Pressure>
   {
   public:
      EnvironmentWindow() {}
      ~EnvironmentWindow() {}
      void update (Temperature *subject) {std::cout << 
             "Temperature was changed" << 
             std::endl; subject->getTemperature();}
      void update (Pressure    *subject) {std::cout << 
             "Pressure was changed"    << 
             std::endl; subject->getPressure   ();}
   };

The class simply inherits twice from the Observer template, for both the Temperature and for the Pressure. Notice that we have two update methods here, one for the temperature, one for the pressure. The following example shows how it can be used:

C++
Temperature       temp;
Pressure          press;
EnvironmentWindow win;
PanicSirene       panic;

temp.attach (win  );
temp .attach (panic);
press.attach (win  );

temp.temperatureChanged ();
press.pressureChanged ();

And it shows the following output:

Temperature was changed
   Getting the temperature.
Temperature was changed, sound the sirene
   Getting the temperature.
Pressure was changed
   Getting the pressure.

Observing Multiple Subjects of the Same Type

If our PanicSirene class needs to verify both the internal temperature and the external temperature, we don't need to modify anything to our implementation of the PanicSirene class. We simply attach our class instance to both Temperature classes.

C++
Temperature       internalTemp;
Temperature       externalTemp;
PanicSirene       panic;

internalTemp.attach (panic);
externalTemp.attach (panic);

License

This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.

A list of licenses authors might use can be found here.


Written By
Software Developer (Senior) Patrick Van Cauteren
Belgium Belgium

Comments and Discussions

 
GeneralSTL container of instances issues & other limitations Pin
eohrnberger28-Jan-05 9:42
eohrnberger28-Jan-05 9:42 
The implementation of observer pointers in the subject class is a little bit concerning to me. If observers are collected in an STL container as full instance copies, then anytime the STL container of Observer instances adjusts it memory, or moves the Observer instances around, the Subject's pointers to those Observers are invalidated and will cause an access violation when they are used.

Should the Observer have a back pointer to its Subject (would appear to be the next logical development), and the Subjects are contained in STL containers as instances, when the Subjects move around in the STL container, the Observer's pointer to its Subject are invalidated and will cause an access violation when they are used.

Further analysis would seem to lead to the conclusion that Subject copy and Subject assignment operations of Observer pointers is transitive in nature, i.e. the target Subject of the copy or assignment operation end up with the Observer pointer list. This makes the STL containers of Subject instances work quite naturally as the STL containers move their Subject contents around inside of them. Of course with the Observation transition from one Observer to another, the source Observer needs to be disconnected from it's Subject, and the target Observer needs to be connected to the Subject.

Observer copy and assignment operations are not transitive. It's duplicative, meaning that when you copy or assign an Observer you end up with two Observers observing the same subject. Care must be taken to make sure that the Observer copy and Observer assignment operations attach the target Observer with the source Observer's subject. On Observer destruction, a call to each of it's Subjects Deleting( Observer* ) method needs to be made, so that the Subject can remove the Observer from it's list of Observers. On Subject destruction, it needs to first detach itself from all of its Observers.

The tricky part in all of this is who gets to maintain the Observer's back pointer to it's Subject, especially if the Observer can observe multiple Subjects? I elected to make the Subject always maintain their Observer's back pointers as data is the boss, and the Subject represents the data.

The notion that an observer can observe only and single instance of a subject is limiting. A more flexible implementation would be that an observer can observe any number of subjects. Passing a base class Subject pointer to the Observer::Update( Subject* ) method, it can be determined using RTTI and dynamic casts what the real Subject type is, and the Observer can then, based on which type the subject is, make the appropriate method calls to update it's rendering of the Subject (granted this means that in the Update method you'll need a case / switch or a number of 'if ... else if ...' statements. The final 'else' in the 'if / else' sequence coughed out an error message, or could throw an exception, that an unrecognized Subject was encountered, and you knew that the Observer established an observing relationship with something that it didn't deal with when it should have. I'd not view the multiple 'if / else if's as all that bad, as typically an Observer is only observing very few different types of subjects.

Another alternative is making the Subject and Observer templates, and declaring them as base classes with the template argument being the class being Observed or Subjected. i.e. class AppObserver : public Observer< AppSubject > and class AppSubject : public Subject< AppSubject >. Since the Observers Update and Deleting methods are pure virtual, it forces application subclasses to implement the needed methods. Another advantage is that there is no need for RTTI, as part of the templated method signature determines which method is called.

I like this design much better, but it relies much more heavily on templates, which may or may not be in the capabilities of following C++ code maintainers.

When doing things in this manner, full object instances in the STL containers and making sure that the copy CTOR and assignment operators are correct, it is very important that you really understand the difference between object instance identity and object instance equivalence. I in fact had to implement a base class that did nothing more than provide a static counter which was incremented each time a subclass instance is created and stored on CTOR as it's identity value. This same base class provided the ID method so that you knew that you were comparing identity rather than equivalence. With a simple predicate class comparing the identity value of subclasses, it was easy to get the STL containers of object instances to be searchable based on this criterion.

This leads to the need of providing a DeepCopy method for times when the observation relationships were not copied like in the copy CTOR and assignment operations. This is specifically required when the application really wants a duplicate of an object instance hierarchy which has a completely separate identity.

The only question I have is this design going to bite me in the butt later?
GeneralRe: STL container of instances issues &amp; other limitations Pin
Patje31-Jan-05 0:24
Patje31-Jan-05 0:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.