I thought I'd do a discussion on the Observer pattern, and all the magic it can do for you in C++.
Let us take it all in order, I'll show you the problem an observer is meant to solve, and a multitude of ways to do it, both good and bad.
In the beginning, there was constant polling. It was and still is the most basic way of checking how content changes. It is still used today on the lower end of the software pipeline where other methods aren't even applicable, deep down in the kernel or wherever in the operating system keyboard is handled, it is done via polling and passed to any interested program.
The loop looks something akin to this:
while(1){
keys = getKeyboardKeys();
if(keysChanged(keys)) handleKeys(keys);
}
Polling is a classic way of working in some systems, but software patterns introduce a lot of different techniques we can use to do this better.
Erasing polling is most commonly done with, you guessed it, the observer pattern.
A more common polling structure in our object oriented games might be something akin to this:
class Keys{
//...
bool getKey(char key);
}
//...
class Game {
Keys keys;
bool shooting;
//...
while(running){
if(keys.getKey('z'))
shooting = true;
else
shooting = false;
//...
}
This looks... good. A common way of handling input, fairly easy to work with in practice and not too hard to use.
ERR.
We have a couple of problems here. The most obvious one is apparent if we decide to create a weapon without repeat fire. Suddenly, we want a structure something akin to this:
if(!keys.lastKey('z') && keys.getKey('z'))
shooting = true;
else
shooting = false;
So... That's one more function to implement. Next we decide to create a weapon that has one bullet type on the first shot, then another bullet type on repeat:
if(keys.getKey('z')){
shooting = 1;
if(!keys.lastKey('z'))
shooting = 2;
}
else
shooting = 0;
All of this goes into one function that keeps on growing and growing whenever we decide to add more content and features. Not only is it a large function that has a multitude of purposes, abstracting it out to configurable keybinds is a mess and it gets worse if we decide to move it into separate classes with random keys doing a multitude of tasks all over the place with constants and polling everywhere.
So, how does an Observer solve this?
Well, it doesn't. However, it makes everything more manageable if our structure is good to begin with.
What the observer actually does is quite simple. If we have a multitude of classes and objects that are interested in what an object does, we say to the object "Tell us when you change", rather than ask "Did you change? Did you change? Did you change?" whenever we can, often repeatedly as seen above.
A first attempt by many is to put such a behaviour in a class:
class Observable;
struct Event{/*???*/};
class Observer{
public:
virtual void notify(const Event &e, Observable *o) = 0;
virtual ~Observer(){}
};
class Observable{
std::set<Observer*> observers;
protected:
void notifyAll(Event &e){
for(std::set<Observer*>::iterator i=observers.begin(); i!=observers.end(); ++i)
(*i)->notify(e, this);
}
public:
void addObserver(Observer *o){observers.insert(o);}
void removeObserver(Observer *o){observers.erase(o);}
virtual ~Observable(){}
};
This structure allows us to store a pointer to our observed object in a class, so that when something happens, we can identify where it happened. It also gives us an Event object containing what changes in that object.
This solution removes all of our polling from whatever previously handled this, and gives us a nice, clean function which can handle all our events.
ERR.
This is a bad solution. It is easy to believe that this is what the observer pattern is, but you'll be mistaken.
Looking back at our previous code, what has changed? How is it cleaner?
Now... instead our main loop is empty... but... our new function looks exactly like the old function, and now we have to do raw type casting on our events depending on what we think the event is! Horrible! We even need knowledge of how Keys, which we observe, sends out events, to identify what arrives! Bad! Get it out of your head!
There similar ways of doing this which work a lot better. One of the more common solutions that work a lot better removes type casting and implements the observer pattern for every observable object, leading to several implementations of almost the same code for differing purposes. It is cleaner, but there's still repeated code involved.
But wait! This is C++ we're working with! C++, with the magic of templates!
Look back a bit at that old code again... How could we use templates to improve this? Of course!
class Observable;
template<typename T>
class Observer{
public:
virtual void notify(const T &e, Observable<T> *o) = 0;
virtual ~Observer(){}
};
template<typename T>
class Observable{
std::set<Observer<T>* > observers;
protected:
void notifyAll(T &e){
for(std::set<Observer<T>* >::iterator i=observers.begin(); i!=observers.end(); ++i)
(*i)->notify(e, this);
}
public:
void addObserver(Observer<T> *o){observers.insert(o);}
void removeObserver(Observer<T> *o){observers.erase(o);}
virtual ~Observable(){}
};
At this point, our code actually looks pretty good. Comparing to the last implementation, we went through almost zero changes, and even removed a class, but now we suddenly have both compile-time type safety, and events of differing types have different functions assigned to them.
There are still some problems, but arriving at this solution, or a similar one without templates, leaves most people pleased. Oftentimes, even me.
But we can go further, utilizing templates, to create an even better solution.
So, what problems DO we still have?
Well, first of all, any object that is interested in what an Observable<T> provides, has to implement the interface Observer<T>. A lot of times, we can end up with classes looking like this:
class A : public Observer<B>, public Observer<C>, public Observable<D>, public Observable<E>, public Observer<F> { /*...*/ };
Looking at that signature and figuring out what we can do with the class works, but it's not very nice, and we have to go all the way up there every time we want to add a dependency for something that MIGHT be an event. What if we have a hundred types, or a thousand, and we might subscribe to any of them, but we're only interested in what caller it is? Yeah, we'd have to go through and add all those interfaces.
Now, there is a nice feature in C++ we can use to take this one step further and remove the inheritance relationship, replacing it with a composition solution.
It's called, you know it... templates!
Consider a class like this:
template<typename Type>
class Trigger : public Observer<Type> {
Observable<Type> *from;
public:
Trigger(Observable<Type> *from) : from(from) {
from->addObserver(this);
}
~Trigger(){ from->removeObserver(this); }
void notify(const Type &t, Observable<Type> *o){}
};
So, what can this do for us?
In this form, nothing! But, it does provide an interesting concept to build on. What this is, is an object that observes 'from' as long as it exists.
Notice that we're not actually doing something when we observe an event, let's change this:
template<typename Type, typename Target>
class Trigger : public Observer<Type> {
Observable<Type> *from;
Target *target;
public:
Trigger(Observable<Type> *from, Target *target) : from(from), target(target) {
from->addObserver(this);
}
~Trigger(){ from->removeObserver(this); }
void notify(const Type &t, Observable<Type> *o){
target->notify(t, o);
}
};
Suddenly, this is a magic template. The difference here is that a template knows at compile-time what functions exist on target, as it can be instantiated with the actual type of target rather than the Observer<> class, meaning that we can rid ourselves of all Observer<>-inheritance in any classes we create, instead replacing them with compositions such as this:
class A{
Trigger<Key, A> trigger;
public:
A(Observable<Key> *k) : trigger(k, this) {}
void notify(const Key &k, Observable<Key> *o) {/*...*/}
};
Of course, creating a trigger on construction might not always be ideal, but we can take this one step further:
class TriggerBase{
public:
virtual ~TriggerBase{}
};
template<typename Type, typename Target>
class Trigger : public TriggerBase, public Observer<Type> {
Observable<Type> *from;
Target *target;
public:
Trigger(Observable<Type> *from, Target *target) : from(from), target(target) {
from->addObserver(this);
}
~Trigger(){ from->removeObserver(this); }
void notify(const Type &t, Observable<Type> *o){
target->notify(t, o);
}
};
Now, with a base type for the trigger, we can suddenly instantiate lists of triggers:
class A{
std::vector<TriggerBase*> triggers;
public:
A(Observable<Key> *o){
triggers.push_back(new Trigger<Key, A>(o, this);)
//...
}
~A(){
while(triggers.size()){
delete triggers.back();
triggers.pop_back();
}
}
//We can still easily access all events with a single function and no inheritance
template<typename T>
void notify(const T& t, Observable<T> *o){}
//And overload it when we want a specific result for a single type
void notify(const Key& k, Observable<Key> *o){}
};
Now, not everyone likes having to manually instantiate and keep track of data with new and delete, as it creates other problems. But on the other hand this will often, probably even always, be present when we arrive at object oriented content in C++. Smart pointers are your friends.
For handling concepts like a trigger and other similar content, we could either subclass vector to delete it's own content or simply use a single inheritance to a base class that deletes it for us:
class SimpleTriggers{
protected:
std::vector<BaseTrigger*> triggers;
public:
virtual ~SimpleTriggers(){
while(triggers.size()){
delete triggers.back();
triggers.pop_back();
}
}
};
Here, almost everything is gone. We have a minimal amount of inheritance in our observers, there's no polling, we have multiple functions for handling a multitude of events... What's left?
Well, at this point, we're moving away from good old Observer and into what might be considered personal preferences.
Some people would like to keep all their event handling in the class, others might move it into helper classes:
class A : public SimpleTriggers{
struct B{ notify(const Key &k, Observable<Key> *o){
if(k == 'z') /*act*/;
} } b;
A(Observable<Key> *o) { triggers.push_back(new Trigger<Key B>(o, &b)); }
};
What I did was modify Trigger with further template data:
struct Any{
template<typename T>
bool operator()(const T &t){return true;}
};
template<typename T>
struct EqCond{
T _t;
EqCond(T _t) : _t(_t) {}
bool operator()(const T &t){return (t == _t);}
};
template<typename Type, typename Target, typename Cond = Any >
class Trigger : public TriggerBase, public Observer<Type> {
Observable<Type> *from;
Target *target;
Cond c;
public:
Trigger(Observable<Type> *from, Target *target, Cond c) : from(from), target(target), c(c) {
from->addObserver(this);
}
~Trigger(){ from->removeObserver(this); }
void notify(const Type &t, Observable<Type> *o){
if(c(t)) target->notify(t, o);
}
};
Which allowed constructs like these:
class A : public SimpleTriggers {
struct B { void notify(Key k, Observable<Key> *o){
//B no longer cares about which key caused this function to be accessed.
bool shootLazor = key.down;
} } b;
public:
A(Observable<Key> *o) {
triggers.push_back(new Trigger<Key, B, EqCond<Key> >(o, &b, EqCond<Key>(Key('z'))));
}
};
Thus moving the conditions for an event to be executed into an external location.
However, this is unrelated to the actual observer pattern, and could just as easily be implemented with if, decorators, or some other type of condition handler, depending mostly on your own preferences.
So. This is as far as we're getting... Or is it?
There is still one small, insignificant issue that keeps plaguing us. How do we begin observing, where should the trigger be created? Should the class itself do it, requiring us to pass data the class doesn't actually need to know to the constructor, such as what to observe?
Should the parent class do it, creating another initialization requirement?
Should there be a magic mediator singleton that we send all our new classes to with some sort of identifier, and have that class figure out what to observe?
Personally, I use all of these solutions in some form or another. Normalizing to one is ideal, but sacrificed to development time. My personal favourite is the second, defining from where a class receives input outside a class to me seems cleaner than forcing the class to figure out where it should retrieve data, since either way the parent class must know what to pass in, giving it the additional responsibility of saying how it should works seems logical.
A class for picking in 3D shouldn't have to know what button needs to be pushed to initiate a pick, whilst a class setting it up may be interested in this.
Not to mention, there is always the Boost.Signals package; which provides a completely different observer implementation that may also be worth looking at. Boost.Signals is a very flexible solution that can do much of what we can, and in some instances more. The downside then, is that Boost.Signals is completely uncoupled from the rest of the code, meaning you have less access to modify it for your own needs. Having some knowledge of what the Observer pattern can do should help you find the solution you prefer.
Whichever way you pick, we are now as close as we will get to the core of the Observer pattern, at least for today. Hope you enjoyed, and perhaps also learned something, not only about the Observer pattern, but also about the mystery of templates.
Disclaimer: All of this is written with hand-eye compilation, any errors are a compiler bug on my end.
Linus