Introduction
The Observer Pattern (Gamma) is part of lots of implementations and is one of the most used patterns. However, implementing a flexible Observer pattern is not an easy task and most of the time creates tight coupling between the Subject and the Observer.
The most popular implementations are:
- Using an interface that has to be inherited by the Observers
- Using function to pointers from the Subject to the Observers
Implementing any of those two models is both complex and error prone, thus time-consuming. More than this, the coupling that normally appears between the Subject and Observer is most of the time unnecessary, while the requirement of implementing an interface sometimes breaks the design, adds the multiple-inheritance costs or requires you to create Adaptor classes between the Subject and the Observer.
The proposed implementation tries to overcome those problems by using the power of templates and creating an easy way of implementing the Observer Pattern in existing or new code without requiring inheritance or function pointers and making it easy to decouple the Subject from the Observer.
Background
Observer Pattern (also called the "Publish-Subscribe" mechanism) as presented by Gamma defines a "one-to-many" so that when one object changes its state, the dependent objects will be informed about this.
The Observer Pattern is widely used in GUI applications (Model-View-Controller model) and other systems where specific objects (Observers) need to be informed when the Subject changes its state. (e.g.: function handler called when a button is pressed).
C# implements this model as Events:
Button
class defines the event as an event and the delegation of the event. public event ClickEventHandler Click(object sender, System.EventArgs e)
public delegate void ClickEventHandler(object sender, System.EventArgs e)
- Observer class (your form) subscribes for the event:
this.btnMyButton.Click += new System.EventHandler(this.btnMyButton_Click);
- Observer class expects the event to be received when the button is pressed and the Click event is generated in the function:
private void btnMyButton_Click(object sender, System.EventArgs e) {...}
Now, this model is simple and basic for C# without too many dependencies. This Observer implementation tries to bring this ease to C++ with maybe a little bit more flexibility.
Using the code
First things first: we have a Subject and more Observers. The Subject is the one that defines the Events and the Observers are the one that have to be subscribed to those events and receive them when they are fired. The only real requirement on any Observer implementation is that the Event defined by the Subject is defined in the same way in the Observer. E.g.:
class CSubject1{
public:
void Event1( int param1, int param2, bool param3 );
};
implies one of the following Observer implementations:
class CObserver1{
public:
void OnEvent1( int param1, int param2, bool param3 );
};
class CObserver2{
public:
void OnEvent1WithSubject( CSubject1 *pObject,
int param1, int param2, bool param3 );
};
Both event definitions are useful. In the first definition, we have the advantage that we don't need to know the class that generated the event, thus it can be any class, not only the CSubject1
. In the second definition, we have to know the CSubject1
class thus we can make specific operations based on the details of CSubject1
.
Now, to get to the subject, how do we connect those 2 classes and make the CSubject1::Event1
function behave as an Event and call the CObserver1::OnEvent1
or CObserver2::OnEvent1WithSubject
? The answer is quite simple. Add to the Subject, a member of SubjectEvents
and connect the Observers with the Subjects:
class CSubject1{
public:
CSubject1();
void Event1( int param1, int param2, bool param3 );
SubjectEvents<CSubject1> Events;
};
CSubject1::CSubject1()
: Events(this)
{ }
void main()
{
CSubject1 s1;
CObserver1 o1;
CObserver2 o2;
s1.Events ( CSubject1::Event1 ) +=
ClientObserver ( &o1, CObserver1::OnEvent1 );
s1.Events ( CSubject1::Event1 ) +=
SubjectObserver ( &o1, CObserver1::OnEvent1WithSubject );
s1.Events ( CSubject1::Event1 ).Notify ( 1, 2, true );
s1.Events ( CSubject1::Event1 ) ( 1, 2, true );
s1.Events.Event ( CSubject1::Event1 ).Notify ( 1, 2, true );
}
However, most of the times, you don't want to "generate" the event from outside of the class that defined the event, neither to write too often a line like:
s1.Events ( CSubject1::Event1 ) ( 1, 2, true );
to generate an event, and more than that, we also have the CSubject1::Event1(...)
function not implemented and not used, thus a small change will make our life easier.
void CSubject1::Event1( int param1, int param2, bool param3 )
{
Events ( CSubject1::Event1 )( param1, param2, param3 );
}
Right now, we have a complete Event generating and handling mechanism, completely type-safe and quite flexible. To trigger an event, we only have to call Event1 ( ... )
and the event is automatically generated and dispatched to all subscribed Observers. The main advantages are:
- No need to define an interface in order to make an Observer receive events generated from a Subject.
- No need for the observer to know the subject. (possible but not mandatory)
- No need for complex pointers to functions cast, or observer registration model.
- Easy to use: with 2 lines of code you have a publish-subscribe mechanism.
- Safe: all function definitions and calls are typed-checked
- Secure: you cannot connect two functions with different parameters
- Decouples Subject from Observers
The main "drawbacks" of the implementation are:
- return type has to be
void
- a maximum of 5 parameters is supported in the current implementation
- the overhead of an event generation is:
- the cost to find the event definition in a list +
- one virtual function call (after the code is optimized by the compiler)
Implementation details
The implementation uses pointers to functions and template's capabilities of auto-detection of parameters. When a function is registered as an event s1.Events ( CSubject1::Event1 )
, the type of the parameters of the function are stored and any function that is registered as an Observer function has to be compliant with the parameters defined by the Subject function.
The SubjectEvents
class maintains a list of registered events. Every time the Event(...)
or operator() ( ... )
is called, the function send as parameter is looked up and registered as an Event.
The Event(...)
and operator() ( ... )
have the same prototype(s), overloaded for different types of functions that can be registered, from:
template<typename _Subject2>
Blue::TSubjectEvent<_Subject2>& Event ( void (_Subject2::*_FuncPoint)() )
for functions without any parameter to:
template<typename _Subject2, typename _Param,
typename _Param2, typename _Param3, typename _Param4, typename _Param5>
Blue::TSubjectEvent<_Subject2,_Param, _Param2,_Param3,_Param4,_Param5>& Event
( void (_Subject2::*_FuncPoint)(_Param,_Param2,_Param3,_Param4,_Param5) )
for function with five parameters. Thus, every call will automatically detect the exact parameters that are passed and return the class TSubjectEvent
constructed with the specified parameters. The TSubjectEvent
has the operator+=
overloaded and accepts to add to its internal list of observers - TClientObserver
classes created on the same mode and having the same parameters. The TClientObserver
is returned from the call to the ClientObserver
or SubjectObserver
function and it's built in the same way as the Event(...)
function by detecting the parameters of the function:
template <typename _Observer, typename _Subject>
TClientObserver<_Observer,_Subject>
SubjectObserver( _Observer* pObserver,
void (_Observer::*_FuncPoint)(_Subject) )
{
return TClientObserver<_Observer,_Subject> ( pObserver, _FuncPoint );
}
template <typename _Observer>
TClientObserver<_Observer,NullType>
ClientObserver( _Observer* pObserver, void (_Observer::*_FuncPoint)() )
{
return TClientObserver<_Observer> ( pObserver, _FuncPoint );
}
If you are trying to add an Observer function using SubjectObserver
/CodeObserver
with different parameters than the ones defined in the TSubjectEvent
, the compiler will not compile the code as the parameters are different.
When trying to Fire an Event from a Subject, the same parameter type detection mechanism is used as in the case of function registration. The only noticeable implementation detail is the final function call that gets executed from a template function based on different details of the Observer and Observer's function:
_Call ( param, param2, param3, param4, param5,
ParamCount<ArgCount>(), Type2Type<_Subject>() );
In case of an Observer registered NOT to receive the Subject as a parameter, one of the following functions is automatically selected by the compiler:
template<typename T> void _Call( _Param param,
_Param2 param2, _Param3 param3, _Param4 param4, _Param5 param5,
ParamCount<0>, Type2Type<NullType> )
{
( ((_Observer*)m_pObserver)->*m_pFunction) ( );
}
[...]
template<typename T> void _Call( _Param param,
_Param2 param2, _Param3 param3, _Param4 param4,
_Param5 param5, ParamCount<5>, Type2Type<NullType> )
{
( ((_Observer*)m_pObserver)->*m_pFunction)
( param, param2, param3, param4, param5 );
}
or one of the followings if the Observer wants the Subject pointer:
template<typename T> void _Call( _Param param,
_Param2 param2, _Param3 param3, _Param4 param4,
_Param5 param5, ParamCount<0>, ... )
{
( ((_Observer*)m_pObserver)->*m_pFunction) ( (_Subject)m_pSubject );
}
[...]
template<typename T> void _Call( _Param param,
_Param2 param2, _Param3 param3, _Param4 param4,
_Param5 param5, ParamCount<5>, ... )
{
( ((_Observer*)m_pObserver)->*m_pFunction)
( (_Subject)m_pSubject, param, param2, param3, param4, param5 );
}
The two final parameters help the compiler detect the exact function to be compiled according to:
- Number of valid parameters of the Observer's function based on
ParamCount<0>
- Whenever to pass the Subject pointer or not based on
Type2Type<NullType>
If the Observer was registered NOT to receive the Subject, the Type2Type<_Subject>()
will resolve to Type2Type<NullType>
(as the _Subject
is by set to NullType
).
If the Observer is registered to receive the Subject, the function defined with the last parameter ...
will be selected by the compiler.
In the same way, the ParamCount
is used to select the number of parameters to be sent to the final function.
Conclusion
The Observer pattern can be implemented in multiple ways, most of them requiring relying on defining an interface to be implemented (in some way or another) by another class.
This implementation uses a non-intrusive way of implementing this pattern, allowing you to completely de-couple the Observer from the Subject.
History
- 25.Jan.2004: version 1.0
First released version.
Copyright
You can use these sources for absolutely free.
Related articles