Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

An easy to use Observer Pattern Implementation (no inheritance required)

0.00/5 (No votes)
26 Jan 2004 2  
Observer Pattern implemented in a nice template model, easy to use as it does not require the classical inheritance and can easly decouple Subject and Observer.

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 );
    // event definition

};

implies one of the following Observer implementations:

class CObserver1{
public:
    void OnEvent1( int param1, int param2, bool param3 );
    // event definition

};
class CObserver2{
public:
    void OnEvent1WithSubject( CSubject1 *pObject, 
       int param1, int param2, bool param3 ); 
    // event definition with reference to the

    // Subject that generated the event

};

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();
    // event definition

    void Event1( int param1, int param2, bool param3 );
    // the Events manager

    SubjectEvents<CSubject1> Events;
};
CSubject1::CSubject1()
// required to initialize the Events with the "this" pointer

: Events(this)    
{ }

void main()
{
    CSubject1 s1;
    CObserver1 o1;
    CObserver2 o2;
    // Connect s1 event to o1, o2


    // First Connect the Event1 to CObserver1 o1. Type checking

    // is automatically done on both ends

    // thus the event implementation in the Observer has

    // to be correct or the code will not compile

    // ClientObserver is used when the event is implemented

    // without requiring the CSubject1 as parameter

    s1.Events ( CSubject1::Event1 ) += 
         ClientObserver ( &o1, CObserver1::OnEvent1 );
    // connect o2. SubjectObserver is used when the event

    // implementation has the CSubject1 as the first parameter

    s1.Events ( CSubject1::Event1 ) += 
         SubjectObserver ( &o1, CObserver1::OnEvent1WithSubject );

    // both event handlers are not connected to the event generator.

    // Now, generate an event:

    s1.Events ( CSubject1::Event1 ).Notify ( 1, 2, true ); 
    // type checking is done again assuring

    // the validity of the parameters !!!


    // if you like more explicit/shorter function

    // calls you can call the event like:

    s1.Events ( CSubject1::Event1 ) ( 1, 2, true );
    // or

    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 );
    // type checking is done again assuring

    // the validity of the parameters !!!

}

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) )
{    // SubjectObserver requires function

     // that contain the "_Subject" as the first parameter

    return TClientObserver<_Observer,_Subject> ( pObserver, _FuncPoint );
}
template <typename _Observer>
TClientObserver<_Observer,NullType> 
  ClientObserver( _Observer* pObserver, void (_Observer::*_FuncPoint)() )
{    // ClientObserver requires functions

     // that do not _Subject as the first parameter

    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:

  1. Number of valid parameters of the Observer's function based on ParamCount<0>
  2. 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

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