I had not originally intended to write a follow-up article to my previous observer design pattern article [Goodliffe]. However, four more months of experience have given me further insights and augmented my implementation of the design. My original article has accidentally grown into a series of two articles, and even now I am convinced I have not found all the answers. One of the things that I like about being a programmer is that I am constantly learning.
Since the previous article described the observer design pattern, I do not intend to cover that ground again - I will just put in the obligatory reference to the GoF book [GoF] and move on.
In this article, as in the previous one, I present a number of implementation approaches, and discuss their relative merits and problems.
In [Goodliffe] I presented four different approaches to an observer pattern implementation (which worked with varying degrees of success). I favoured the penultimate (slightly less safe) version. Four months later, do I still prefer the less safe version?
When my code works, I do. I find the logic of calling notifer->attach more intuitive than calling attach(notifier). I also like to be made to think about detaching from Notifiers rather than blindly trusting that it will be done for me.
However, as I mentioned in [Goodliffe], debugging broken observer links is a nightmare, since it is pure run-time stuff - the compiler will not catch any of our mistakes. The debugger is your best friend in these situations. There have been a number of times (when scratching my head) that I have been tempted to move to the 'safety' model where this would be less necessary.
Perhaps a good argument in favour of the safe version is the particular use the pattern will be put to in my code. I am writing a library. It provides the observer pattern as a means for the user to monitor state changes, as well as using it internally. The 'less safe but less overhead' pattern implementation therefore requires the library user to be very careful about detaching from objects. If the user gets something wrong, they will probably have an even more confusing debugging session than I would.
The various implementation approaches that I present below are based on the 'less safe' version, but will equally be applicable to the 'safe' one. (Indeed, the extensions themselves add a degree of safety).
The classes involved in the original implementation presented in [Goodliffe] are Notifier and Listener. The Listener has only two methods that are called when an observable event is raised, updated and deleted.
My dissatisfaction with the design I arrived at previously is that I wanted Java-like listener interfaces, where a specific listener method is called for each type of event. In the end I got a single method being called for all updates, which is less intuitive (to my mind, anyway).
The discussion in this article will be somewhat nebulous unless we have something concrete to use as an example. So let us consider a Book class[1]. A Book contains a number of Chapters and a Chapter contains a number of Pages. These classes sit in a Notifier/Listener framework as shown below.
Book and Chapter are Listeners, attached to their contents. Chapter and Page are Notifiers, informing their Listeners when they are altered. For example, when a Chapter gains a new page, it will send an updated message to the Book it is in. Chapter has the following updated reason codes that the Book class uses to maintain its table of contents:
enum ChapterReasonCodes {chapter_TitleAltered, chapter_NoPagesAltered };
I am sure that you could argue that this is either a sublime or ridiculous set of classes. However, they are sufficient to give us something concrete to talk about, so in my mind they are more than perfectly formed.
Well, here is the good news. It is possible to get the Java-like listener type of interface that I was looking for, but it will not come for free.
By adding two new classes to our previous solution we can get these Java-like listener interfaces. We add an 'adaptor' class[2] for each Notifier. Where before our class inherited from Listener, it is now the adaptor class that inherits from Listener. The adaptor's updated method switches on the reason code (or aspect in GoF parlance) and sends it to a suitably named method. To make life easier for us, this is implemented as an array lookup.
We define the methods that will be called in the second class, an abstract interface (mixin) class. This interface class is shown below; it is what our Book class will now inherit from.
class ChapterListenerInterface { public: virtual void ~ChapterListenerInterface() = 0; // These methods are called when the Chapter Notifier sends an event virtual void chapter_titleAltered() = 0; virtual void chapter_noPagesAltered() = 0; virtual void chapter_deleted() = 0; private: // Are you following this? Or just assuming it is correct? ChapterListenerInterface() {} ChapterListenerInterface(const ChapterListenerInterface &); ChapterListenerInterface &operator= (const ChapterListenerInterface &); };
Now we can define the adaptor class, ChapterListener, that derives from the above abstract interface:
class ChapterListener : public Listener { public: ChapterListener(ChapterListenerInterface *if) : dest(if), src(0) {} ~ChapterListener() {if (src) src->detach(this); } void attachTo(Chapter *chapter) { if (src) detach(); src = chapter; src->attach(this); } virtual void updated(Notifier *source, int rc) { switch (rc) { case chapter_TitleAltered: dest->chapter_titleAltered(); break; case chapter_NoPagesAltered: dest->chapter_noPagesAltered(); break; } } virtual void deleted(Notifier *source) { src = 0; dest->chapter_deleted(); } private: ChapterListenerInterface *dest; // where to forward events Chapter *src; // source of events };
The Book class contains a ChapterListener adaptor object by value. This ensures that the Book and its adaptor's lifetimes are bound together. For example:
class Book : public ChapterListenerInterface { public: Book() : chapterListener(this){ chapterListener.attachTo(&chapter);} // Overriden versions of the ChapterListenerInterface methods private: Chapter chapter; // say we only have one chapter ATM! ChapterListener chapterListener; };
When the Book is deleted it will be automatically detached from the Chapter thanks to the ChapterListener class' destructor.
Key benefits:
-
The sought after Java style interface
-
Can listen to more than one different type of Notifier because you can inherit from different adaptor classes
Drawbacks:
-
Have to write an adaptor for each class you want a Java-like interface for
-
You need to create a second object which must be contained by value (not actually a drawback, but we have no way of enforcing this relationship in the code)
-
You can only listen to one Chapter at once
We can fix the last disadvantage very easily: we only need to add an extra parameter to the ListenerInterface methods that specifies which Notifier caused the event:
class ChapterListenerInterface { public: virtual void ~ChapterListenerInterface() = 0; virtual void chapter_titleAltered(Notifier *src) = 0; //(*) virtual void chapter_noPagesAltered(Notifier *src) = 0; //(*) virtual void chapter_deleted(Notifier *src) = 0; //(*) private: ChapterListenerInterface() {} ChapterListenerInterface(const ChapterListenerInterface &); ChapterListenerInterface &operator=(const ChapterListenerInterface &); };
There is a slightly annoying 'feature' of this design: we cannot pass Chapter pointers into the functions marked (*), only the base Notifier pointers. The Listener class interface forces everything to be cast to the common Notifier type, despite the fact that we know that in this interface the source object will most certainly be a Chapter pointer.
There is not a lot we can do to get around this. We could certainly perform a dynamic_cast[3] in the ChapterListener's updated method to convert the Notifier* to a Chapter*. But it is not a very good solution - for every event raised we have to perform RTTI, which is an expensive operation that we should avoid if at all possible (and of course, some environments which like to compile without RTTI will not cope). For this reason, we stick with the Notifier*. If the user needs the real Chapter* they can perform the cast themselves.
In order to attach to more than one Chapter, we need to contain more than one ChapterListener adaptor object, since each of these adaptor classes can only listen to one Notifier.
Since we really want the Book object to listen to a variable number of Chapters, extra work is needed in the implementation of the adaptor class. In this kind of situation we really need to balance this effort and decide whether an adaptor class is really warranted.
Key benefits:
-
Can now listen to more than one object of the same type
Drawbacks:
-
The interface passes a base pointer who's real type you know - this requires us to use RTTI, or lump it
-
Can only attach one adaptor to one Notifier
-
Extra work required to listen to a variable number of Notifiers
We must write quite a lot of code for each Listener adaptor. Can we use templates to remove some of that redundancy? If we can then we will gain a number of benefits:
-
less typing for each implementation
-
less chance of bugs, since there are fewer lines of code
-
clearer design to understand
We introduce the Listener adaptor class concept. A class concept describes what facilities a class should provide with no syntactic backing; it is often used to describe what a template parameter type should be able to do. The STL, for example, contains many class concepts, such as the 'forward iterator'. An instance of our adaptor class must contain a number of items - typedefs, integer definitions, and so on. The idea is shown below:
// The ChapterListenerInterface is based on the ListenerAdpator class concept class ChapterListenerInterface { public: typedef Chapter Notifier_type; // Defines the source Notifer const int no_aspects = 2; // The number of aspects enum // enum of all aspects { chapter_titleAltered, chapter_noPagesAltered } // typedef to provide the type of method called typedef ChapterListenerInterface::*callback_t)(Notifier_type*); // Using it we define an array of updated callbacks and a single deleted callback static const callback_t updated_callback[];updated callbacks static const callback_t deleted_callback; callback // After the mandatory items we define the abstract interface for callbacks void chapter_titleAltered(Notifier *src) = 0; void chapter_noPagesAltered(Notifier *src) = 0; void chapter_deleted(Notifier *src) = 0; };
In a .cpp file, we define the callback array:
ChapterListenerInterface::callback_t ChapterListenerInterface::updated_callback [ChapterListenerInterface::no_aspects] = { &ChapterListenerInterface::chapter_titleAltered, &ChapterListenerInterface::chapter_noPagesAltered, }; ChapterListenerInterface::deleted_callback = &ChapterListenerInterface::chapter_deleted;
It is a shame that we have to do this in a separate .cpp file - it would be nice to have defined the callbacks in the class declaration. However, C++ only allows us to define static constant integers in a class declaration, and certainly not arrays.
Now we can use this adaptor class by containing an adaptor object that uses this ChapterListenerInterace:
class Book { public: Book() : chapterListener(this) { chapterListener.attachTo(&chapter); } private: Chapter chapter; ListenerAdaptor<ChapterListenerInterface> chapterListener; };
So we need to see what this ListenerAdaptor looks like. Well, I am sure you could work out that it looks like this:
template <class ifclass> class ListenerAdaptor : public Listener { public: ListenerAdaptor(ifclass *dest) : Listener(), src(0), dest(dest) {} virtual ~ListenerAdaptor() { detach(); } void attachTo(typename ifclass::Notifier_type *src) { if (src) detach(); this->src = src; src->attach(this); } void detach() { if (src) { src->detach(this); src = 0; } } private: virtual void updated(Notifier *source, int aspect) { if (aspect < ifclass::no_aspects) (dest->*ifclass::callback_table[aspect])(source); } virtual void deleted(Notifier *source) { (dest->*ifclass::callback_deleted)(source); src = 0; } private: typename ifclass::Notifier_type *src; ifclass *dest; };
It looks like quite a lot to take in one step, but doing this reduces the amount of effort that is needed to add new ListenerAdaptor interfaces to the system since the ListenerAdaptor template class already does all the hard work.
Note the use of typename in the code above, which is needed to tell the compiler that certain definitions are actually type names that will be pulled in from the ifclass definition at template instantiation time. If we do not supply this hint the compiler will assume that ifclass::Notifier_type, for example, is either a method or variable in the template, and complain bitterly at the apparent syntactic error.
Another problem cited in 'design 5' is that we have to create separate interface and adaptor classes. We also have to contain the adaptor object by value. Could we not simply roll these two classes together? When inheriting from the listener interface class you would then also get the event conversion code for free.
We are forgetting our clever use of templates for the moment.
To illustrate this approach we will deviate from the Book/Chapter/Page example and consider a couple of example classes imaginatively called A and B. They are both Notifiers, both contain enum aspect definitions, and both have a couple of methods that do something to cause a notification to be sent. Marvellously contrived.
class NotifierA : public Notifier { public: enum { a_1, a_2 }; void do_1() { notify(a_1); } void do_2() { notify(a_2); } }; class NotifierB : public Notifier { public: enum {b_1, b_2 }; void do_1() { notify(b_1); } void do_2() { notify(b_2); } };
So if we write an adaptor class that is also an abstract interface class, we get the following:
class ListenerA : public Listener { protected: ListenerA() : src(0) {} ~ListenerA() { detach(); } void attachTo(NotifierA *a) { if (src) src->detach(this); src = a; src->attach(this); } // detach is implemented as before virtual void a_1(Notifier *src) = 0; virtual void a_2(Notifier *src) = 0; private: NotifierA *src; virtual void updated(Notifier *src, int aspect) { if (src == this->src) { switch (aspect) { case NotifierA::a_1: a_1(src); break; case NotifierA::a_2: a_2(src); break; } } } // deleted is implemented similarly };
Repeat this kind of class for a ListenerB. (In this case you'll largely be swapping As for Bs and as for bs.)
We now need a concrete class that illustrates how we can listen to a NotifierA, to show how little effort is needed. Let us call it ConcreteListenerA for want of a better name.
class ConcreteListenerA : public ListenerA { public: ConcreteListenerA() { attachTo(&_a); } virtual void a_1(Notifier *) { cout << "a_1\n"; } virtual void a_2(Notifier *) { cout << "a_2\n"; } NotifierA *a() { return &_a; } private: NotifierA _a; };
With a little harness to show how this works:
int main() { ConcreteListenerA cl_a; cl_a.a()->do_1(); cl_a.a()->do_2(); }
This produces the output you would expect:
a_1 a_2
Well that is a bit of a success: we now have an interface class that also acts as an adaptor. But (there's always a 'but') what happens when you want to listen to more than one kind of Notifier? Let us say that we have a ConcreteListenerAB that inherits from both ListenerA and ListenerB.
The problem we now have is that ConcreteListenerAB has two attachTo methods. They are ambiguous. We'd use them by calling attachTo(this), but which attachTo do we intend to call? One will attach to a NotifierA and one to a NotiferB. The compiler isn't psychic enough to deduce which one we mean, since the this pointer could be equally converted into either type. We can get around this just by being a little bit more explicit:
class ConcreteListenerAB : public ListenerA, public ListenerB { public: ConcreteListenerAB() { ListenerA::attachTo(&_a); // have to specify ListenerB::attachTo(&_b); // which attachTo to call } virtual void a_1(Notifier *) { cout << "ab a_1\n"; } virtual void a_2(Notifier *) { cout << "ab a_2\n"; } virtual void b_1(Notifier *) { cout << "ab b_1\n"; } virtual void b_2(Notifier *) { cout << "ab b_2\n"; } NotifierA *a() { return &_a; } NotifierB *b() { return &_b; } private: NotifierA _a; NotifierB _b; }; int main() { ConcreteListenerAB cl_ab; cl_ab.a()->do_1(); cl_ab.a()->do_2(); cl_ab.b()->do_1(); cl_ab.b()->do_2(); }
This harness does what we expect, proving that the system works:
ab a_1 ab a_2 ab b_1 ab b_2
So the approach works, but you need to be a little bit more explicit to the compiler. Not necessarily a bad thing. The user does have to think carefully about what they are attaching to, though.
There is, however, another problem with this approach. What if you want to attach to more than one A or B? Using this inheritance based approach we can't contain multiple Listener adaptor classes, and so can only attachTo one A or B at once. That is unless you do extra work to implement a more clever adaptor class.
Take care that the inheritance of a Listener adaptor class must not be virtual. This would mean that rather than having two updated methods which are called according to the context of the pointer (i.e. in the example above ConcreteListenerAB have both a ListenerA::updated and ListenerB::updated that are called by the NotifierA class and NotifierB class respectively) you would only have the one updated method. In this method you would have to tediously delegate each event by hand.
Key benefits:
-
Fewer classes on the system, easier to understand
Drawbacks:
-
Can only conveniently listen to one of each type of Notifier
-
Still have to write logic for each type of Listener, and there is quite a lot of that
-
Have to be careful not to use virtual inheritance
Despite there being more disadvantages listed than advantages, I think that overall this is a pretty good approach, if we can prevent the need for so much legwork...
The problem with the above approach, of course, is that you have to write the updated and deleted methods for each interface. As in 'design 7', we're really after something a little more generic. Call the template fairies again.
Ironically, the code for a generic version of this approach is very similar to the previous generic code. We have to split the single class back into an adaptor interface class, and the generic adaptor template class. When implementing this you need to override each pure virtual method, and provide an array of callbacks. This is shown in the code below.
class AdaptorA { public: typedef NotifierA Notifier_type; static const int no_aspects = 2; virtual void a_1(Notifier *src) = 0; virtual void a_2(Notifier *src) = 0; virtual void a_deleted(Notifier *src) = 0; }; template <class if_type> class ListenerAdaptor : public if_type, public Listener { public: ListenerAdaptor() : src(0) {} typedef if_type Listener_type; typedef void (Listener::*callback_t)(Notifier *); static const callback_t updated_callback[Listener_type::no_aspects]; static const callback_t deleted_callback; void attachTo(typename Listener_type::Notifier_type *n){ if (src) src->detach(this); src = n; src->attach(this); } void detach(){ if (src) { src->detach(this); src = 0; } } virtual void updated(Notifier *n, int aspect){ if (aspect < Listener_type::no_aspects)(this->*updated_callback[aspect])(n); } virtual void deleted(Notifier *n){ (this->*deleted_callback)(n); } private: typename Listener_type::Notifier_type *src; }; const ListenerAdaptor <AdaptorA>::callback_t ListenerAdaptor<AdaptorA>::updated_callback[AdaptorA::no_aspects] = {&AdaptorA::a_1, &AdaptorA::a_2 }; const ListenerAdaptor<AdaptorA>::callback_t ListenerAdaptor<AdaptorA>::deleted_callback = &AdaptorA::a_deleted; class ConcreteAdaptorA : public ListenerAdaptor<AdaptorA> { public: ConcreteAdaptorA() { attachTo(&a); } void do_something() { a.notify_1(); } virtual void a_1(Notifier *) { cout << "adaptor a_1\n"; } virtual void a_2(Notifier *) { cout << "adaptor a_2\n"; } virtual void a_deleted(Notifier *) { cout << "adaptor a_deleted\n"; } private: NotifierA a; }; int main() { ConcreteAdaptorA ca_a; ca_a.do_something(); }
Key benefits:
-
Less work involved in adding a new Listener adaptor
-
Despite splitting into two class again, inheritance prevents the messy containment of the adaptor
Drawbacks:
-
We are back to two classes - darn!
-
You end up implementing bits of the two different classes for each adaptor - somewhat confusing
I think that I have probably used up enough paper on this discussion of the observer pattern, but there is so much more we could discuss. I leave a few parting ideas as excercises for the interested reader:
Is it possible to templatize the Notifier class (as we tried to do in the first article), using the Listener adaptor as a template type? After having done this (if possible), can we convert that updated(int aspect) to updated(Listener_type::callback_t dest)?
There are some other issues to do with using the observer design pattern that have arisen:
-
Listeners inheriting from Listeners
Let us say the Glossary class inherits from Chapter. Glossary gets its Listener capabilities indirectly via Chapter.
How do we create a GlossaryListener? We need to ensure that each aspect number is unique, including being unique between the ChapterListener class and the GlossaryListener class. The approach I adopted is as follows (using the 'design 7' outline).
class GlossaryListener : public ChapterListener { public: enum { glossary_defnAdded = ChapterListener::no_aspects, glossary_defnRemoved } ... // and all the other stuff };
The implementation of an interface hierarchy of Listeners is, seemingly, always untidy.
-
Build dependancies
It should be noted that the addition of these adaptor classes tends to increase the build dependencies in the system.
-
Attaching to a variable number of Notifiers
These designs are not so useful if you want to attach to a number of objects of the same class. Book will want to attach to any number of Chapters. These are only really useful it you know how many objects you will be attached to. The adaptors can be extended to cope with this kind of use - I leave it as an exercise for the reader, who should also judge whether it is worth it.
-
No updated aspects
What happens if a class has no interesting updated aspects, but you want to be informed when it is deleted. If its Listener adaptor class defines no_aspects as 0 the code will not compile - C++ doesn't allow you to create an array with zero elements. (Well, you can if you are creating it using new, but that's another story). You will need a strategy to get around this. Perhaps the easiest is just to define a dummy updated reason code.
-
Choice of inheritance type
In these implementations I have used public inheritance. You may choose to use private inheritance if it suits you. If you're not sure what the difference is, I urge you to go and find out now!
-
Reviewing safe (design 3) vs. unsafe (design 4)
Using the adaptor is so much like the 'safety' model I presented in [Goodliffe] that I think that my preferences have moved towards it (except, perhaps, renaming attach as attachTo).
I am not sure that it will be possible to construct the perfect observer implementation. I am sure that if I were to ever find one I'd have spent so much effort on it I will probably never have written any other code.
In exploring this set of new implementations I hope that we've gained a better understanding of the observer pattern, and the situations in which it could be used. I hope that we have also explored some other useful ideas ('class concepts' and adaptors) and got a feel for some more 'real worldf C++ coding.
[1] One article full of exhibitionists and voyeurs is quite enough!
[3] In fact since we're very sure that the parameter is a Chapter* we could employ a static_cast. I am not sure that this isn't bad form though - and it definitely will not work if the Chapter uses virtual inheritance from the Notifier.
Overload Journal #38 - Jul 2000 + Design of applications and programs
Browse in : |
All
> Journals
> Overload
> 38
(6)
All > Topics > Design (236) Any of these categories - All of these categories |