Abstract
This article describes how it is possible to put a reusable design idea into a really reusable code with an example of the observer pattern. After a short repetition about patterns in general and the observer pattern I will show you the typical disadvantages of applying it to a specific problem because of inheritance dependencies. I will provide some general techniques to reduce such dependencies and eventually present a reusable implementation of the observer pattern that can be easily applied to your projects. You should be familiar with the idea of patterns in general, the observer pattern, concepts of OOA/OOD and the C++ template mechanism.
Introduction
Reusable design...
"A design pattern names, abstracts, and identifies the key aspects of a common design structure that makes it useful for creating a reusable object-oriented design" [Gamma et al in [1]]
As Gamma stated out, a design pattern addresses the common problem in creating object-oriented software designs. It helps you to communicate with other developers about your design at a high level of abstraction, which is really great. However, someday you will have to put the design into a concrete implementation, meaning you have to pour it into some code.
... versus reusable code
At this point you will find yourself doing the same coding again and again. Wouldn't it be nice to have some reusable code for the reused design? The rest of this article will show you how it is possible to achieve this with an example of the observer pattern.
The observer pattern
The observer pattern is one of the best and most usable patterns in practice. This comes from its simplicity and the fact that it addresses a very common problem: The problem of propagating changes in an object's state to dependent objects. There is an excellent article by T. Kulathu Sarma about the observer pattern, here at CodeProject [2], so I do not want to go in detail. The following diagram (modified slightly from [1]) shows the structure of the observer pattern:
The problem
Our goal is to create a reusable implementation of the observer pattern. That looks simple, just put the above structure straight into a class Subject
and an abstract interface IObserver
and use them whenever you need to implement the observer pattern. But you will find that it is difficult to reuse your code in real world projects. The main problem is that it involves structural dependencies on the project's architecture. And these dependencies come from the usage of inheritance.
Why inheritance is bad
Some of you may wonder: "Come on, what's so bad about inheritance? It's inheritance that helps us build reusable components!". Of course, you are right. Inheritance is a very powerful - if not the most powerful - concept in OOP. But in general the usability of inheritance ends beyond the kind-of relationship. For the observer pattern it sounds quite strange to call a real-world ConcreteSubject
, like a temperature sensor used in weather forecasts, a "kind of subject". It is more common to think of the possibility to notify other objects on state changes as a feature of our temperature sensor.
The structural impacts of inheritance
1. Modification of the inheritance tree
Consider the following class structure of an automatic weather forecast system with classes TempSens
(a temperature sensor) and HurrDet
(a hurricane warning detector) which are derived from some base classes. We don't want to make any assumptions about these base classes, so we just call them A
and B
:
Now we want HurrDet
to act as an observer of TempSens
. To reuse our implementation TempSens
has to be derived from Subject
and HurrDet
from the IObserver
interface. This is a bit tricky, because we have to:
- change the existing inheritance tree or
- use multiple inheritance
Solution a) involves changes to the inheritance tree which are often not acceptable. Solution b) uses multiple inheritance to add the necessary functionality into the class TempSens
. The Subject
class is used as a mixing class that mixes the functionality into our concrete class. This is a very common approach. However, multiple inheritance on polymorph classes can be tricky and most people tend to avoid it. At the end most developers would not reuse our class Subject
, and build their own implementation of the observer pattern for this special case. In other words: They would reuse the design - but not the code:
2. Serving and observing more than one kind of object
Now we add a new class Strategy
that describes the algorithms to use for all weather forecast components. These algorithms are adopted from time to time by some meteorologist to implement new research results into the system. We want HurrDet
to act as an observer for both the TempSens
and the Strategy
objects. This means that HurrDet
now acts as two single observers.
At the same time we add a new feature to our TempSens
. Because the reliability of the temperature sensor is crucial for the system, it should be able to monitor its physical state and report it to the technical staff. As you might already guess, this is another excellent application for the observer pattern. Our class TempSens
now is a subject in two very different manners. It acts as two single subjects, one propagating temperature information and the other propagating physical state information.
At this point the reusage-by-inheritance approach fails. We cannot derive our class TempSens
more than once from Subject
or derive HurrDet
more than once from IObserver
. Indeed, we have to create a lot of IObserver
-like interfaces and again here we are reusing only the design - and not the code:
Conclusion
The above example has shown that reusing the code of a generic component like our observer implementation tends to be difficult in a real world project. This is because we make too many assumptions about the structure of the project:
- It must be possible to add the functionality by inheritance.
- Every class needs the functionality at most once.
The solution
It is important to note that both the assumptions are a drawback of the usage of inheritance. Having understood this, it is clear that we have to find a way to build our components in a way that does not depend on inheritance. We can achieve this by replacing inheritance with aggregation. Instead of deriving from a class with some functionality, we include it as a data member (aggregate it). However, data members are usually declared private
and are not accessible from other classes. So we need to add member functions to the outer class to export the interface of the aggregate. These member functions are simple one-liners, they just call the corresponding members of the aggregate, which is often referred to as delegation. Delegation is a very popular technique in COM development because COM lacks implementation inheritance and delegation is the only possible way to reuse an implementation.
Implementing the subject without inheritance
We start with implementing the subject, because this is the easiest part to solve. Instead of deriving our concrete subject TempSens
from Subject
we now aggregate it and use delegation. This solves all our problems, because by using aggregation it becomes easy to include it twice into TempSens
. As described above, we have to (partially) export Subjects
interface from TempSens
because other objects must still be able to attach or detach themselves as observer:
Replacing inheritance by aggregation in our TempSens
is quite easy. This is because of the fact that we don't need the abstract interface of Subject
. Indeed the observers are semantically connected to the concrete subject TempSens
and need to know its interface. So it is not a problem for them that the Attach()
and Detach()
methods are now part of the TempSens
interface and are not inherited from some abstract ISubject
interface.
Implementing the observer without inheritance
However, on the observer side, things are quite different. At runtime, the subject may be connected to any number of observers, which may be instances of different classes. On a state change the subject calls Update()
for every observer object, so all observers have to be derived from the abstract IObserver
interface. In other words: Here we need Update()
to be a part of an abstract interface, it is not possible to move it to the concrete observer classes.
To come around this we again use delegation, but now in another direction. We create a helper class ObserverAdapter
that implements the IObserver
interface. We aggregate this into our HurrDet
class. The ObserverAdapter::Update()
implementation just calls a member function of the outer HurrDet
class - it delegates the call to the outer class:
Because we don't want to write such a special helper class for each observer we generalize our ObserverAdapter
implementation: We parameterize it by the outer class and use a member callback into the outer class for delegation. To use member functions as callbacks is not that common. Maybe this is because of the confusing syntax of pointer-to-member constructions in C++. However, if you look at the code of class ObserverAdapter
below, you will find that it is not really difficult to use a member function as callback.
A reusable implementation of the observer pattern
Now we have all the elements that we need to create the reusable implementation of the observer pattern. Besides I have added some more extensions to the original structure of the observer pattern which makes it more applicable to the real world projects:
- I declared
IObserver
as a subclass of ISubject
. This makes sense because of the close relationship between a subject and the interface of its observers. In other words: The subject defines the interface of its observers.
- The
Notify()
call accepts a single parameter and returns a value. The types for parameter and return value are passed as template arguments to ISubject
. It's up to you what to do with these parameters; I usually use them to pass information about the changes in the subject's state. This technique is known as push-variation of the observer pattern.
- Another template argument is used to store an observer-related attribute in the subject's list of observers. The semantics of this attribute is in the responsibility of the subject implementer. For example, it can be used as event mask or threshold to pass observers only those events they are really interested in.
- All the classes and interfaces are declared in the namespace
tool
.
The above extensions are useful to avoid unnecessary method invocations, which is especially important for distributed computing scenarios.
The ISubject and IObserver interfaces
template<class OBSERVER_INFO = int,
class RETURN = int, class ARG1 = int>
struct ISubject
{
typedef typename OBSERVER_INFO observer_info_t;
typedef typename RETURN return_t;
typedef typename ARG1 arg1_t;
struct IObserver
{
typedef typename RETURN return_t;
typedef typename ARG1 arg1_t;
virtual return_t Update( arg1_t ) = 0;
};
virtual bool AttachObserver(IObserver* pObserver,
observer_info_t Info) = 0;
virtual bool DetachObserver(IObserver* pObserver) = 0;
};
As said above, the subject interface ISubject
gets two template parameters to parameterize the signature of ISubject::IObserver::Update()
. A third template parameter is used to declare the attribute type for the information to be stored with each observer. Nothing fancy so far. Now let's have a look at the reusable ISubject
implementation:
The SimpleSubject class
template<class OBSERVER_INFO = int,
class RETURN = int, class ARG1 = int>
class SimpleSubject : public ISubject<OBSERVER_INFO,
RETURN, ARG1>
{
public:
virtual bool AttachObserver(IObserver* pObserver,
observer_info_t Info = observer_info_t() )
{
m_aObservers.push_back(observer_entry_t(pObserver,
Info));
return true;
}
virtual bool DetachObserver( IObserver* pObserver )
{
clients_t::iterator i = m_aObservers.begin();
for( ; i != m_aObservers.end(); ++i )
if( i->pObserver == pObserver ) {
m_aObservers.erase( i );
return true;
}
return false;
}
virtual void NotifyObserver( arg1_t arg1 = arg1_t() )
{
for( clients_t::iterator i = m_aObservers.begin();
i != m_aObservers.end(); ++i )
i->pObserver->Update( arg1 );
}
protected:
struct observer_entry_t
{
observer_entry_t( IObserver* o, observer_info_t i )
: pObserver( o ), Info( i ) {}
IObserver* pObserver;
observer_info_t Info;
};
typedef std::vector< observer_entry_t > clients_t;
clients_t m_aObservers;
};
The SimpleSubject
class provides an easy implementation of the IObserver
interface and uses a std::vector
to store observer_entry_t
elements. Each element represents an attached observer with its attribute. The sample application shows how you can use the attribute by overriding SimpleObserver::NotifyObserver
.
template<class OUTER, class SUBJECT>
struct ObserverAdapter : public SUBJECT::IObserver
{
typedef typename OUTER outer_t;
typedef typename SUBJECT::return_t return_t;
typedef typename SUBJECT::arg1_t arg1_t;
typedef return_t (OUTER::*mfunc_t)(arg1_t);
ObserverAdapter( outer_t* pObj = NULL,
mfunc_t pmfUpdate = NULL )
: m_pObj( pObj ),
m_pmfUpdate( pmfUpdate ) {}
virtual return_t Update( arg1_t arg1 = arg1_t() )
{
return (m_pObj->*m_pmfUpdate)( arg1 );
}
virtual void SetUpdate( outer_t* pObj,
mfunc_t pmfUpdate )
{
m_pObj = pObj;
m_pmfUpdate = pmfUpdate;
}
outer_t* m_pObj;
mfunc_t m_pmfUpdate;
};
This is obviously the most interesting class that contains only a few lines of code. It gets two template parameters: The type of the outer class (OUTER
) and the subject interface (SUBJECT
). The first is needed to declare the pointer-to-member-function type mfunc_t
. The latter is used to derive from the actual IObserver
interface and to get the appropriate return_t
and arg1_t
types. The callback function to use can be passed to the constructor, or can be changed later by the SetUpdate()
method. Both get two parameters: The instance of an object you want to process the callback and the address of a member function to call. The member function's prototype has to be of the form [virtual] return_t func(arg1_t)
. (It does not matter if the member function has been declared virtual or not, virtual is not part of a member function's signature.) Whenever the subject calls Update()
, the real work is just delegated to the callback. The stored member function's address m_pmfUpdate
is called in the context of the stored object m_pObj
and passed the given parameter arg1
.
The sample application
The sample application is a simple dialog-based MFC app. It implements the example of a weather forecasting system using the above subject and observer classes. It also shows how to use the template parameters to utilize the per-observer attribute by overriding SimpleSubject::NotifyObserver()
. The example classes of our weather forecasting are declared and implemented in the file weather_forecast.h. The apps dialog can be used to simulate events like "temperature change" or "power failure" and show that they are propagated to the observing objects.
Summary
Design patterns are great for developers and it prevents them from reinventing the wheel and helps them to look at the design on a high level of abstraction. However, even if we do not have to reinvent the wheel, we have to rebuild it each time, because of the existing gap between the reusable design and reusable code. It has been shown that naive implementations usually involve structural dependencies which inhibit their application in real world projects. These (bad) structural dependencies are related to the usage of inheritance. We can solve this problem by replacing inheritance with aggregation and using the delegation technique. In combination with callbacks to member functions this is a very flexible approach that allows us to build pluggable components and generic features without making any assumptions about the environment they are used in.
References
- [1] Erich Gamma, Richard Helm, Ralph Johnson, Jon Vlissides : "Design Patterns - Elements of Reusable Object-Oriented Software". Addison Wesley, 1995.
- [2] T. Kulathu Sarma : "Applying Observer Pattern in C++ Applications", 2001.