Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Callbacks Made Easy with the Observer/Mediator Design Patterns


February 2001/Callbacks Made Easy with the Observer/Mediator Design Patterns


Introduction

In almost every non-trivial computing system, you can find uses of many different event notification techniques, such as are commonly employed in client/server and distributed architectures. One of the most useful is the callback. It implements a convenient event-based communication bridge between data instances, applications, or systems. This article shows how to blend two basic patterns — Observer and Mediator [1] — to implement a non-intrusive callback library. Compared with callback implementations based on the standard Observer pattern, this composite OM (Observer-Mediator) design offers far greater programming freedom and flexibility during initial development and, importantly, during later evolutionary phases. The OM design allows developers to introduce and further extend callback functionality at very late stages of software development. This encourages developers to focus on developing applications, not infrastructure.

The Callback Library Design

A callback involves an association between at least two participants. Following the terminology used in Design Patterns [1], I refer to the callback initiator as the Subject; the callback receiver is the Observer. Despite the many variations in possible Observer pattern implementations (see [2] and [3] for a couple of examples), the Observer design by itself cannot solve a central problem with callback frameworks: the callback infrastructure is embedded in the Subject; there is no true decoupling between Subject and Observer instances.

Figure 1 illustrates this situation. To enable callback functionality, the ConcreteSubject class must accommodate additional callback-related infrastructure. In its simplest form that infrastructure is encapsulated in a base class (Subject). All application classes capable of generating callbacks must derive from this class.

A similar requirement is imposed on callback recipients — ConcreteObservers. Suppose that in the middle of the development of a payroll management system the programmers are told that an employee should report to the payroll office every time she enters and leaves the office. Then the programmers will have to modify the Employee and PayrollOffice classes they have already developed, to accommodate the additional functionality. Consequently, the implementation will have to change to something similar to:

class Employee : public HumanBeing, 
   public Subject { ... };
class PayrollOffice : public Observer 
{ ... }:

Such modifications substantially distort an application-focused class hierarchy; they change the sizes of corresponding objects; and they introduce implicit coupling. (Subject and Observer have to know about each other.)

As an orthogonal approach to managing associations, I introduce a separate Callback Manager entity (see Figure 2). The Manager takes over all the responsibilities for managing Subject-Observer associations. The Manager accommodates the necessary infrastructure, and it channels and plays a mediator role in callback-related communications between the Subject and the Observer.

The major advantage of this design is that the Subject is free of callback infrastructure and responsibilities unrelated to its original purpose. The Subject does not manage and is not even aware of the existence of Observers. Therefore, if callback functionality is later added as in the case of the Employee-PayrollOffice example, it will not affect the application class hierarchy or object sizes.

The application developer provides Subject (Employee) and Observer (PayrollOffice) components of a callback association. The library discussed in this article adds the only missing link — the Mediator (called the Signal class here).

Usage and Implementation Highlights

Listing 1 shows an example use of the OM design. In lines 66 and 67, callback associations between instances of Employee and PayrollOffice are created. When the Subject (john) changes its state, as in line 72, it reports the change to the Callback Manager (Signal) by generating an appropriate event, as shown in lines 14, 20, and 26. The Callback Manager executes the registered callback functions (employee_enters and employee_leaves). Finally, the code in lines 74 and 75 destroys the callback associations created in lines 66 and 67.

Callback initiation is event-driven. However, callback registration (lines 66, 67) does not require an explicit event specification — the callback signature uniquely identifies the event that the callback will be associated with and initiated by:

class PayrollOffice
{  ...
  // Associated with Employee::EnterEvent.
  void employee_enters(
    const Employee::EnterEvent&, Employee&);
  // Associated with Employee::LeaveEvent.
  void employee_leaves(
    const Employee::LeaveEvent&, Employee&);
};

As shown in lines 2, 8, and 9 of Listing 1, every callback event is a distinct class derived from Signal::Event. The Signal::Event class forms the basis for an extendible collection of callback event types.

To avoid pollution of the global namespace, Subjects introduce their own sets of events (EnterEvent, LeaveEvent) or alternatively, use already existing events (DestroyEvent, Signal::Event).

If callback functionality is added later, it does not upset already-written code. Equally important, it does not change existing data layout. This ensures that the software is capable of evolving:

class Employee
{  ...
  // Event added. No existing code broken.
  class NameChangedEvent : public Signal::Event {...};

  void change_name()
  { ...
    Signal::emit(*this, NameChangedEvent());
  }
};

class PayrollOffice
{ ...
  // Callback added.
  void name_changed(
    const Employee::NameChangedEvent&,
    Employee&);
};

// Register callback.
Signal::connect(
  john, po, &PayrollOffice::name_changed);

Non-class-based callbacks are supported in a similar fashion:

void sweepstakes(
   const Employee::EnterEvent& ev,
   Employee* employee, PayrollOffice* po)
{ ...
  if (++counter == 1000)
  {
    po->prize_goes_to(employee);
  }
}

// Register callback.
Signal::connect(john, po, sweepstakes);

The Callback Manager

Listing 2 shows the Signal class. It may look heavy at first glance. However, its public interface effectively consists of only three functions — connect and disconnect to create and remove a callback association; and emit to report a Subject’s state change and to generate an event.

connect and disconnect are families of overloaded functions to support Observer-based member callbacks and non-member callbacks with the following basic signatures:

void (Observer::*)(const Event&,
   Subject&),
void (*)(const Event&, Subject&,
   Observer&).

For simplicity, Listing 2 shows only two basic functions in each family. The actual source code includes four more functions to properly handle const properties of the Subject and the Observer arguments.

The exact callback signature requirement ensures static type safety without sacrifice in functionality. If additional data needs to be passed along, that data needs to be part of a Subject, an Event, or an Observer. Which of the three is determined as follows:

  • If the date describes an event initiator (a Subject), it must be part of the Subject.
  • If the data describes a condition or event that initiated a particular callback (an Event) it must be part of the Event.
  • If the data describes any additional information that the application writer wants to associate with the callback, it must be part of the Observer.

In the following example, additional data (_old_name) is passed as part of a Subject-initiated event (NameChangedEvent):

class NameChangedEvent : public Signal::Event
{ ...
  NameChangedEvent(char* old_name)
  : _old_name(old_name) {}

  const char* old_name() const { return _old_name; }

  // Additional callback-related data.
  char* _old_name;
}

void Employee::change_name(char* new_name)
{  ...
  // Report name change.
  Signal::emit(*this, NameChangedEvent(old_name));
}

void PayrollOffice::name_changed(
  const Employee::NameChangedEvent& ev, Employee* emp)
{ ...
  Records* rec = find_records(ev.old_name());
  rec->update_name(emp->name());
}

The set of public interface functions (connect, disconnect, and emit) is backed up by a set of private functions (_connect, _disconnect, _emit). In fact, the public functions do nothing but strip down user-supplied Subject and Observer types, pack homogenized data into an internal storage parcel, and immediately invoke their private counterparts. The template-based public interface accomplishes the only but important task — it provides type safety without additional run-time overhead. The following snippet shows the implementation of function connect.

template<class Subject, class Observer, class UserEvent>
inline void
Signal::connect(Subject& subject, Observer& observer,
   void (Observer::* cb)(const UserEvent&, Subject*))
{
   Parcel parcel(new UserEvent, (Callee*) &observer,
                 (MemberCallback) cb);
   _signals()._connect(&subject, parcel);
}

Homogenized data (expressed in terms of internal Callee, MemberCallback, or LonelyCallback types) are packed in Parcels:

struct Parcel
{  ...
   Event*                   _event;
   Callee*               _receiver;
   MemberCallback _member_callback;            
   LonelyCallback _lonely_callback;            
};

and stored in the internal Signal::_collection. (See _connect, in the CUJ online source archives, www.cuj.com/code, for implementation details.) Later, when a Subject reports a state change by calling Signal::emit(*this, Event), the collection is searched with the Subject as a primary key. All the callbacks registered for the Subject-Event pair will be initiated. (See _emit, also in the online archives, for implementation details.)

To simplify Signal’s public interface the class is implemented as a singleton. Specific projects might need separate callback managers for different groups of events. Minor changes in Signal will accommodate such a requirement.

Using Base Class Callbacks with a Derived Class

This requirement is satisfied using a type-safe reference-based upcast. In the following example the Player implements the bulk of callback-related functionality. The derived FancyPlayer capitalizes on the functionality while adding additional features:

class Player
{  ...
   // Callbacks.
   void play(const Button::ClickEvent&, Button*);
   void stop(const Button::ClickEvent&, Button*);
};

class FancyPlayer : public Player
{  ...
   // Additional stuff.
   void screen_saver(...);
};

Button b;
Player p0;
FancyPlayer fp;
Player& p1 = fp; // Reference upcast.

Signal::connect(b, p0, &Player::play); // 1
Signal::connect(b, fp, &Player::play); // 2
Signal::connect(b, p1, &Player::play); // 3

The first call to connect goes through as expected. However, an attempt to use a base-class callback with an instance of a derived class fails -- the second connect does not compile! The template-based mechanism does not recognize base-class/derived-class relationships. However, the third connect compiles and achieves what was needed on the second line.

Possible Extensions

Some sophisticated object communications require more than a simple callback notification mechanism. An immediate example would be a negotiation algorithm that requests an Observer’s approval before the Subject undertakes a certain action.

The library discussed here can be extended effortlessly to cover such functionality without affecting participating data structures. Technically, the Signal class will include two separate managers to handle callbacks and requests. The Request Manager will be similar to the discussed Callback Manager, with its own request collection and public interface (connect_request, disconnect_request, emit_request, etc.). The difference will be in the signatures of user-provided request-processing functions (compared to callback functions) and in the emit function initiating a request:

// Signatures of request functions.
bool (Observer::*)(const Event&, Subject*),
bool (*)(const Event&, Subject*, Observer*). 

template<class Subject>
static bool emit_request(Subject&, const Event&);

The bool return type reflects that fact that request-processing functions report back whether they accept or reject (return true or false) a request. The following usage example better demonstrates the basics of the discussed algorithm:

void Button::press()
{
   // Request to process the event. The
   // request will be evaluated by 
   // user-provided functions and
   // accepted (returns true) or 
   // rejected (false).

   bool accept = 
   Signal::emit_request(*this, Press());

   if (accept)
   {
      // Accepted. Do event-related 
      // processing.
      ...
      // Changes have occurred. Notify.

      Signal::emit_callback(*this,
         Press());
   }
}

Development Environment

The callback library was developed and tested using gcc-2.95 on SPARC Solaris 2.6 and with minor adjustments on UnixWare 7 using the SCO native compiler. To overcome a weird linking problem in the gcc-2.95 library, I had to comment out the following line in the supplied iostream.h:

ostream& operator<<(__omanip func) { 
   return (*func)(*this); }

Summary of Advantages

By employing the composite OM pattern, I have produced what I consider to be a package of hard-to-beat advantages, rarely (if ever) available together:

  • Ease of use and integration. A few lines of code enable callback associations between Subjects and Observers (not necessarily class-based).
  • True non-coupling. The Subject is totally free from the Observer-related data and functionality.
  • Non-intrusiveness. The Subject is free from callback-related infrastructure. There are no special requirements or restrictions imposed on data structures that participate in callback associations.
  • Multicast (one-to-many) associations. An unlimited number of Observers may register with a Subject and many Subjects may sensibly use the same Observer, callback function, or Observer-Callback pair.
  • Flexibility and expandability. The Subject uses a flexible event-driven mechanism to report its state change and to initiate a callback. For example, it is reasonable for instances of a Button class to generate Press, Release, Double Click, Focus, and potentially many other events. The application writer is free to associate zero, one, or many callbacks with any Subject-supported event. The event set is easily expandable without upsetting already written application code.
  • Complete type safety based on the template mechanism.
  • Support for legacy data structures and callbacks. The library provides a way to use non-member callback functions that is consistent with the way class-member callbacks are used. This allows non-class-based data structures (lumps of raw data or third-party structures) to use the callback mechanism, providing an easy upgrade path for legacy code.
  • Low overhead. There is no built-in callback-associated overhead in Subject and Observer classes or their instances, regardless if they participate in callback associations or not.
  • Responsiveness. Conceptually, the Subject initiates but does not carry out the execution of potentially time-consuming callback functions. This allows callback-related functionality (event processing and callback initiation done by a separate Callback Manager entity) to be implemented in a separate thread.

Acknowledgments

The design and ultimately the implementation were inspired by the innovative "signal-slot" callback design used in the Qt library (the basis of the popular KDE environment) from Troll Tech AS [5].

References

[1] Erich Gamma et al. Design Patterns (Addison-Wesley, 1995). ISBN 0-201-63361-2.

[2] Paul Jakubik. "Callback Implementations in C++," “http://www.primenet.com/~jakubik/callback.html#Callback Implementations in C++”.

[3] Rich Hickey. "Callbacks in C++ Using Template Functors," C++ Report, 7(2) 1995.

[4] Bjarne Stroustrup. The Design and Evolution of C++ (Addison-Wesley, 1994). ISBN 0-201-543303.

[5] "Introduction to Signals and Slots," Qt Reference Documentation, http://www.troll.no/qt/metaobjects.html.

Vladimir Batov is a software engineer currently working for EMC Corporation, Massachusetts. He can be reached at [email protected].


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.