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

C/C++

C++ Notifiers


Dr. Dobb's Journal August 1998: C++ Notifiers

Dave is the owner of On Target Software, a C++/Windows consulting firm in Marshfield, Massachusetts. He can be contacted at [email protected].


Notifiers, also called events or messages, are used to pass information anonymously between objects. Although integral to Java, Taligent, and Smalltalk, they're absent from C++. Notifiers connect objects indirectly, replacing pointers and direct function calls. Because they're anonymous, notifiers reveal nothing about the implementation, interface, or even the existence of connected objects, leaving them independent of one another. By reducing dependencies, they reduce complexity. Most important, notifiers are easy to understand and to use, so they continue to be used effectively as new people replace the original developers. In this article, I'll show how notifiers can work in C++, and then demonstrate their use in a multithreaded application.

Although my own notifier implementation is a C++ Windows DLL, the source code could easily be ported to any platform and could be written in any object-oriented language. If you're a fan of design patterns, you'll recognize notifiers as an example of the Observer pattern, with some important differences, which are explained further on.

How Notifiers Work

Notifiers are the minimum interface necessary for communication. For A to send a notifier to B, it shouldn't need to include B's header or even know if B exists. That way, A and B depend only on the notifier interface, not on each other.

The notifier system has four parts:

  • Publisher, A.
  • Subscriber, B.
  • Notifier.
  • Dispatcher, mechanism to dispatch notifiers and register subscribers.

As Figure 1 shows, the publisher creates a notifier and tells the dispatcher to place it in the queue. Subsequently, the dispatcher calls the subscriber, passing it the notifier.

The dispatcher has the hardest job, maintaining the lists of notifiers and subscribers, but this is a module that can be written once and forgotten. The other three objects have minimal code.

At first, it may seem excessive to communicate by creating and posting a notifier instead of making a direct function call. If you already have a pointer to an object, calling a method of the object is faster and easier. But if you don't have that pointer to the object, notifiers are easier.

The Publisher

The publisher tells the system about a change. For example, a data module reports that a database value has changed, a real-time module reports a data acquisition event or a user-preferences module reports a font change.

The publisher can post the notifier or send it. Posting is asynchronous: The notifier is queued, and the publisher continues to execute without waiting for the notifier to be dispatched. Sending is synchronous: The notifier is dispatched to all its subscribers before the publisher continues.

The Subscriber

The subscriber is any object that inherits from the CSubscriber class in Listing Two Subscribers are kept in a list maintained by the dispatcher and are removed from the list automatically upon destruction.

Notifiers are dispatched to subscribers by calling a CSubscriber virtual method. For this reason, each notifier class has a corresponding method in the CSubscriber class. A notifier class that reports changes in temperature has a corresponding OnTemperatureNotifier method in the CSubscriber class. To receive that notifier, a subscriber would:

  • Subscribe to the temperature notifier.
  • Override OnTemperatureNotifier.

The Notifier

The notifier encapsulates information about a change and contains its own copy of the data. This is important because a notifier stays in the dispatch queue after posting, so it may outlive the publisher and the original data. For example, a real-time notifier might contain sensor data; a user-preferences notifier might contain fonts and colors.

The Dispatcher

The dispatcher, like the Wizard of Oz, stands invisibly behind a curtain. It maintains a list of all pending notifiers and all active subscribers. It receives notifiers from the publisher, sends the notifiers to the appropriate subscribers, then destroys the notifiers. Only the subscriber and notifier base classes are aware of the dispatcher.

In the Windows version presented here, I triggered the dispatcher periodically with a timer message, so it always runs in the application thread.

An Example

I used notifiers as the basis for a Windows application that looks like a household thermometer. To show how notifiers help during the evolution of a product, I've developed the example through several hypothetical customer releases. (The source code, executables, and related files are available electronically; see "Resource Center," page 3, and at http://www.targetsoft.com/.)

Release 1: A Simple Thermometer

The product begins its life cycle as the thermometer in Figure 2. The MFC AppWizard generated the shell of the application, and to this I added:

  • A temperature sensor running in its own thread.
  • A text "degrees" window at the top to display the temperature digitally.
  • A thermometer window containing a scale and a bar of mercury.
  • A Celsius button to toggle between Fahrenheit and Celsius.

As for the notifiers, I included:

  • A temperature notifier sent by the sensor when the temperature changes. The text display and the graphical thermometer both respond to it.
  • A units notifier sent by the Celsius button to toggle between Fahrenheit and Celsius.

Figure 3 shows the object relationships. The objects are unaware of each other, depending only on the notifiers represented by the pink boxes. Arrows show whether the notifiers are sent or received.

When the temperature changes, the sensor posts a PostTemperatureNotifier. The CThermometer class receives the notifier by subscribing to it during initialization and overriding the OnTemperatureNotifier method inherited from the CSubscriber base class; see Listing Three.

As Listing One shows, the notifier itself is a simple object with one data member. The header includes two inline convenience functions -- one for posting the notifier and one for sending it. Without notifiers, the temperature sensor and the thermometer would be forced to exchange pointers. The thermometer would call the sensor to get the temperature during initialization, and the sensor would call the thermometer whenever the temperature changed. They would also need to tell each other when they were deleted, so neither would be left with a dangling pointer.

Release 2: Add a Temperature Graph

Figure 4 shows the addition of a graph to track temperature over time. The graph object responds to changes in temperature and the toggling of Celsius/Fahrenheit. Since these notifiers already exist, all I had to do was write the code for the graph and modify the parent window to instantiate it. None of the other objects changed at all. In fact, none of the other objects are even aware of the graph.

Release 3: Add a Thermostat Setpoint

Release 3 changes the thermometer to a thermostat by adding a setpoint, in Figure 5. The slider changes the setpoint, which is reflected in a text display above it. The graph tracks the setpoint as well as the temperature, and the sensor simulates climate control by adjusting the temperature to the setpoint.

To implement this release, I had to:

  • Add the two new windows and the setpoint notifier.
  • Change the graph to respond to setpoint changes.
  • Change the temperature sensor to simulate changes as if a furnace was responding to the setpoint.

As Figure 6 illustrates, the number of objects increased and the interaction between those objects increased even more, but only one new notifier was added.

This is the profound advantage of notifiers, and this is why I believe they're an essential tool of the object-oriented developer. Notifiers constrain the design of an object, limiting its complexity in a fundamental way, without limiting its operational capability.

Pointers versus Notifiers

If I'd built the example program using pointers to connect objects, I could have gotten the same results, and the code would have been faster and more efficient, but the object relationships would have been more complex, as in Figure 7.

The complexity comes from these requirements:

  • View objects request data when they initialize, and data objects tell view objects when they change, so each needs a pointer to the other.
  • Each object has to tell its clients when it is destroyed, so pointers are not left dangling.
  • Each pointer is associated with a full header file.
  • Clients make direct calls to an object's interface, which means an object can't be changed without considering all its clients.
  • Additional objects can cause exponential growth in object relationships.

Complex pointer exchanges aren't necessary with small noninteractive systems, so with those, I might not bother with notifiers. On the other hand, I wouldn't consider writing a user interface of any size without notifiers. User interfaces tend to have many objects, each of which needs to know some aspect of the system state. Furthermore, user interfaces are hardest hit by changing requirements so they have the greatest need for design flexibility.

Requesting Information

One problem with notifiers is getting information on demand. Let's say the user opens a new graph to track the setpoint. The graph needs the current setpoint, but won't receive a notifier until the setpoint changes. Since the graph isn't directly connected to the setpoint control, it can't call it for the current value.

I solved this problem by creating a special notifier just for requesting other notifiers. To get the current setpoint, the graph calls RequestNotifier. The slider responds by posting the setpoint notifier. The slider doesn't know who sent the request and the graph doesn't know who responded. If I decide later to eliminate the slider and have a different object maintain the setpoint, the graph won't change at all.

This technique has another important use. It's especially tricky to initialize a system containing mutually dependent objects. Adding new objects to the start-up sequence can break old code.

You can use notifiers to decouple objects during startup:

  • In the constructor for each object, initialize all data to safe values. The object can run with these values, although it won't do anything useful.
  • Each object posts a RequestNotifier for the data it needs.
  • After all objects are created, the RequestNotifiers are dispatched, and the publishers respond by posting their current values.
  • The subscribers receive their first updates and begin normal operation.

Since objects don't exchange pointers, startup and shutdown aren't sensitive to the order of object creation and deletion.

Notifiers and the Observer Pattern

Notifiers are similar to the Observer pattern described in Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1995), by Erich Gamma et al. The Observer pattern defines two abstractions: a subject (publisher) and an observer (subscriber). Observers hold pointers to subjects. When a subject changes state, it triggers a notification that is sent to its observers. The observers use their pointer to the subject to call for an update.

The advantages of the Observer pattern are that there is no overhead of a separate notifier object, and the notifications are much faster. The disadvantages are that an Observer has a pointer to the subject (which becomes invalid if the subject is deleted) and it is dependent on the subject's interface.

I use the notifier object to abstract the changed information, which becomes the only element shared between the publisher and the subscriber. I've found this abstraction to be the greatest advantage of using notifiers.

Frequently Asked Questions

Why not use COM events and connection points? One problem is that COM connections are direct. A client creates a sink which holds a pointer to a connectable object. The client can't subscribe unless the connectable object exists. The connectable object can't be deleted until the client releases it. In contrast, notifiers keep subscribers and publishers separate, connected only to the dispatcher. This independence makes implementing both objects simpler.

COM is a powerful mechanism for communicating with outside objects. Notifiers can help by distributing external COM events to internal C++ objects. For example, on a font-change event sent to an ActiveX control, a notifier could propagate the new font to all the views in the control.

Why not use Windows messages? Windows messages can also be sent or posted. Why invent a whole new mechanism? Unfortunately, only a window can receive a Windows message. In addition, using a dispatcher mechanism allows the messages to be objects, to carry their own data, and to be deleted after dispatching.

Why not have the dispatcher run in its own thread? If the dispatcher runs in its own thread, subscribers have to lock the data referenced in their OnNotifier methods, and they have to lock it everywhere else it is referenced. It's better to run the dispatcher in the main application thread. If a notifier will trigger a long-running operation, that one operation can be isolated in its own thread, which bounds the synchronization problem.

As the system grows, do notifier classes proliferate? Not necessarily. A notifier class defines a set of related data associated with a set of subscribers. If I need to send a new kind of update, I look at the existing notifier classes to see if one exists with a similar purpose targeted to the same subscribers. Table 1 shows how data might be assigned to notifier classes in a factory automation system. I've found that even large systems can get by with a small working set of notifiers.

What about performance? Notifiers are slower than pointers. If a notifier is posted, it must be allocated and queued, and each subscriber to that notifier must be called. If a notifier is sent, there is no allocation or queuing, but the subscribers are still called.

In addition, the presence of the OnNotifier functions in the v-table of each subscriber increases the memory footprint proportional to the number of notifier classes and the number of different subscriber subclasses. When used appropriately, however, notifiers have little impact on performance. In commercial systems with nearly a hundred notifiers (too many!) and dozens of subscribers, the dispatcher has never been a bottleneck. Other problems, like database access time, were much bigger culprits.

DDJ

Listing One

class CTemperatureNotifier : public CNotifier{
public:
   CTemperatureNotifier(void) : CNotifier(TEMPERATURE_NOTIFIER)
      { m_temperature = 0.0; }
public:
   float m_temperature;   // temperature is in degrees celsius
};
// convenience function to send the notifier immediately
inline HRESULT SendTemperatureNotifier(float temperature)
{
   HRESULT hr = E_OUTOFMEMORY;
   CTemperatureNotifier *pNotifier = new CTemperatureNotifier();
   if (pNotifier)
   {
      pNotifier->m_temperature = temperature;
      pNotifier->Send();
   }
   return hr;
}
// convenience function to queue the notifier for dispatching
inline HRESULT PostTemperatureNotifier(float temperature, 
    CNotifier::PRIORITY nPriority = CNotifier::PRIORITY_NORMAL)
{
   HRESULT hr = E_OUTOFMEMORY;
   CTemperatureNotifier *pNotifier = new CTemperatureNotifier();
   if (pNotifier)
   {
      pNotifier->m_temperature = temperature;
      pNotifier->Post(nPriority);
   }
   return hr;
}

Back to Article

Listing Two

class CSubscriber{
public:
   enum SUBSCRIBER_PRIORITY
   {
      PRIORITY_HIGH,
      PRIORITY_NORMAL,
      PRIORITY_LOW
   };
public:     
// Constructors and destructors
   CSubscriber(void);
   CSubscriber(const CSubscriber &src);
   virtual ~CSubscriber(void);
   CSubscriber &operator=(const CSubscriber &src);
// Subscription Operations
      // GetSubscriberPriority: return relative priority of subscriber
   SUBSCRIBER_PRIORITY GetSubscriberPriority(void);
      // SetSubscriberPriority:  move this subscriber ahead or behind other
      //                         subscribers in the list
   void SetSubscriberPriority(const SUBSCRIBER_PRIORITY nPriority);
      // IsSubscribed: return true if a subscriber will be sent this notifier 
   bool IsSubscribed(UINT nSubscription);
      // Subscribe: add subscriber to the list that receives this notifier
   HRESULT Subscribe(UINT nSubscription);
      // Unsubscribe: no longer send this notifier type to the subscriber
   HRESULT Unsubscribe(UINT nSubscription);
// Notifier response functions, called by dispatcher
      // called when the temperature changes
   virtual void OnTemperatureNotifier(CTemperatureNotifier *pNotifier);
      // called when temperature units change
   virtual void OnUnitsNotifier(CUnitsNotifier *pNotifier);
      // called when thermostat setpoint changes
   virtual void OnSetPointNotifier(CSetPointNotifier *pNotifier);
      // called when another subscriber wants a notifier
   virtual void OnRequestNotifier(CRequestNotifier *pNotifier);
protected:  // Implementation
      // SetDispatcher: Called by CDispatcher
   static void SetDispatcher(CDispatcher *pDispatcher);
      // GetDispatcher: Get pointer to dispatcher
   static CDispatcher * GetDispatcher(void);
private:  // Data members
   static CDispatcher *m_pDispatcher;           // see SetDispatcher
   SUBSCRIBER_PRIORITY m_nSubscriberPriority;
   friend class CDispatcher;
};

Back to Article

Listing Three

HRESULT CThermometer::Init(void){
   ... other initialization ...
   Subscribe(TEMPERATURE_NOTIFIER);
   return result;
}
void CThermometer::OnTemperatureNotifier(CTemperatureNotifier *pNotifier)
{
   if ((GetSafeHwnd() != 0) && 
       (m_temperature != pNotifier->m_temperature))
   {
      m_temperature = pNotifier->m_temperature;
      RedrawThermometer();
   }
}

Back to Article


Copyright © 1998, Dr. Dobb's Journal

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.