Implementing the Java Delegation Event Model in C++

Revision as of 07:41, 18 April 2009 by Clarman (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

First published in C++ Report, April 1999

by Craig Larman


Introduction

This article examines the design of the delegation event model (DEM) used in Java, and how it may be implemented in C++. In addition, the design and C++ implementations of JavaBeans-style bound and constrained properties are presented. DEM experts will note that some minor modifications have been made, to take advantage of multiple implementation inheritance in C++, and to make some simplifications. Why bother to learn about an implementation of the DEM in C++? Three reasons: 1.The DEM is a relatively elegant and efficient publish-subscribe style design, worthy of emulation in C++. 2.Java—and thus the use of the DEM—is widespread, and many C++ developers need to work in both languages. Thus it is advantageous to use similar publish-subscribe architectures across languages, in order to increase the congruence in designs, improve porting opportunities, and reduce the cross-language learning curve. 3.To promote examples of interface-based design in C++, which is a Good Thing.


First, Interfaces in C++ and the UML

A definition of interfaces and interface implementation in the context of C++ is required, since Java supports interfaces, which are used in the DEM. Readers who understand this topic may wish to skip this section. Informally an interface specifies a set of one or more operation signatures; for example, the ILowAltitudeListener interface defines the operation signature onLowAltitude (Figure 1). It is a relatively common convention to name interface classes starting with the capital letter “I”. In the Unified Modeling Language (UML) the interface name is stereotyped with «interface» to indicate it as such.


Article-dem-fig1.gif

Figure 1. An interface in the UML notation.


In C++ interfaces are defined by pure abstract base classes that only contain pure virtual function definitions, such as:

 
class /* INTERFACE */ ILowAltitudeListener 
{
public:
	virtual void onLowAltitude( LowAltitudeEvent* evt ) = 0;
};

In C++ interface implementation is achieved by publicly inheriting from the interface, and implementing the abstract operations. In addition, it is common that multiple inheritance is present; the derived class often inherits the interface specification from the interface and implementation details from another superclass (Figure 2). In the UML, interface implementation is indicated with a dashed-line version of the generalization-specialization association, as opposed to the regular solid-line version.


Article-dem-fig2.gif

Figure 2. Interface implementation and subclass extenions in the UML.


The derived C++ class which implements an interface must of course provide a member function body for the abstract operations it inherits:

 
class AirTrafficControl : 
public Organization, 
public ILowAltitudeListener
{ 
public:
	virtual void onLowAltitude( LowAltitudeEvent* evt )
	{
		cout << "warning: low altitude plane";
	}

// ...
};


The Java Delegation Event Model

The DEM is an implementation of the Observer or Publish-Subscribe pattern [GHJV95]. Publishers are capable of generating and sending (publishing) events; subscribers register interest (subscribe) in the events of particular publishers. When a publisher has an event, it notifies all the subscribers interested in that event. A basic design goal of Publish-Subscribe (e.g., the DEM) is to provide a form of loosely coupled signaling from a publisher to subscribers and a means to dynamically register/de-register subscribers with a publisher. A common implementation technique to achieve low coupling from publisher to subscriber is to design in terms of interfaces, rather than regular classes. This will be illustrated in this following sample implementation. In DEM nomenclature, the publisher is called the event source, and the subscriber is the event listener. Events are represented as classes of objects, and the publishing of an event is implemented as a regular message send (synchronous member function invocation). Most of the concepts presented in the first part of this article are summarized in Figure 3, which illustrates a sample use of the DEM with an Airplane which is a source LowAltitudeEvents, and AirTrafficeControl, a listener for these events. The DEM is quite simple, so this diagram might provide enough insight to proceed directly to the second major section on JavaBeans-style bound and constrained properties.


Article-dem-fig3.gif

Figure 3. An example use of the DEM.


Event Objects

Events in the DEM are represented as objects, and all derive from class EventObject, which is responsible for knowing the source object of the event (Figure 4). It is common, though not required, that subclasses of EventObject add additional properties pertaining to the event. For example, a KeyboardEvent could know the key pressed, and so forth. A sample definition of EventObject is:

template <class SOURCE_TYPE>
class EventObject  
{
public:
	EventObject( SOURCE_TYPE* source ) 
		: m_source( source ) {}

	virtual ~EventObject() {}

	SOURCE_TYPE* getSource() { return m_source; }

private:
	SOURCE_TYPE* m_source;
};

The most common difference between the Java and this C++ implementation of EventObject and other classes in the DEM is the inclusion of parameterized types in the C++ version (for example, for the event source class). In contrast, the Java version declares the event source as of class Object—the root of all Java classes—and requires down-casting from class Object for most usage. Of course, the template classes in C++ eliminate the need to cast.


Article-dem-fig4.gif

Figure 4. Events are represented as objects, and know their source.


Event Listeners (Subscribers)

Event listener classes (i.e., subscribers) are responsible for responding to event notification when an event source sends them a message. In effect, the event source says to the listener, “Hey! I’m having an event” and the listener must react to that notification. A concrete event listener must implement a particular interface for an event. By convention, the interface names all end with the term Listener, such as ILowAltitudeListener. The name of the listener operation(s) ideally suggests the signaling of the event, such as onLowAltitude or alarmRaised. It is required that all event handling operations must receive the EventObject (or a subclass) as a parameter, such onLowAltitude(LowAltitudeEvent*) (Figure 5). This gives the listener potential visibility back to the source object of the event, since an EventObject knows its source.


Article-dem-fig5.gif

Figure 5. Examples of Listener interface names and operation signatures.


In the C++ implementation, the listener interface requires a template parameter for the event source type, because the event object parameter definition requires it. An example is:

template <class SOURCE_TYPE>
class /* INTERFACE */ ILowAltitudeListener 
{
public:
	virtual void onLowAltitude( 
		LowAltitudeEvent<SOURCE_TYPE>* evt ) = 0;
};

Concrete event listener classes that implement the listener interface are responsible for responding to event notification when an event source sends them a message. To repeat a previous example, the AirTrafficControl class implements the ILowAltitudeListener interface (Figure 2), and is thus ready to subscribe to listen for LowAltitudeEvents. When an event source sends an onLowAltitude message to an AirTrafficControl instance, it reacts to the “event” as it wishes, such as displaying a message.

class AirTrafficControl : 
public Organization, 
public ILowAltitudeListener<Airplane>
{
public:
	// this event handling method is called when a 
	// source “has an event” and sends this message.
 
void onLowAltitude( LowAltitudeEvent<Airplane>* evt )
	{
		// in contrast to the Java DEM, the parameterized event 
		// class avoids the need to down-cast the event source

		Airplane* plane = evt->getSource();

		cout << "warning: low altitude plane";
	}

// snip ...
};


Event Sources (Publishers)

An event source (i.e., publisher) has two responsibilities:

1.Register listener objects. 2.Notify listener objects when an event happens by sending them a message.

Any number of classes can be a source of the same event. For example, both Airplane and Bird can be a source of the LowAltitudeEvent.

The event source class must implement methods to registeradd and removelisteners. By convention, the signature pattern for the registration methods will usually be:

void addFooListener( FooListener<SOURCE_TYPE>* )

void removeFooListener( FooListener<SOURCE_TYPE>* )

Why addFooListener rather than a more generic method name such as simply addListener, overloaded by parameter type? The latter is not sufficient because the class of the actual parameter can implement multiple listener interfaces; thus overloading on parameter type is not a sufficient discriminator. For example, if Airplane is a source of LowAltitudeEvents, it must be able to register ILowAltitudeListener instances. The group of registered listeners will probably by saved in an STL container, such as a vector. To illustrate:

class Airplane
{ 
public:
   void addLowAltitudeListener( ILowAltitudeListener<Airplane>* lis )
	{
		// add lis to a container
	}

   void removeLowAltitudeListener(ILowAltitudeListener<Airplane>* lis )
	{
		// remove lis from a container
	}

 // ...
};


Broadcasting Events from a Source to Listeners

When an event source “has an event” it must notify all registered listeners by broadcasting an event notification message to them. In the DEM, this is called event multicasting. The event source class requires a method, such as broadcastLowAltitude, to perform this. By convention, this method takes an already existing event object as a parameter, so that the event object can be created in some other method and passed along. For example:

class Airplane
{ 
public:
	// broadcast the event to all listeners

void broadcastLowAltitude( LowAltitudeEvent<Airplane>* evt )
	{
	for each listener lis in the container,
		lis->onLowAltitude( evt )
	}

	// modify the altitude, and possibly signal
	// event multicasting

void setAltitude( int alt ) 
	{ 
	m_altitude = alt;

	// if necessary, broadcast the LowAltitudeEvent

	if ( alt < getAltitudeThreshold() )
		{
			LowAltitudeEvent<Airplane>* evt =
				new LowAltitudeEvent<Airplane>( this ) ;

			broadcastLowAltitude( evt );

			delete evt ;
		}
	}
// snip ...
};


The Abstract Client Pattern

Not only does the DEM illustrate the Publish-Subscribe pattern, but also the Abstract Client pattern [Martin96]. When a server object (such as an event source) needs to register callbacks on client objects, and send messages to clients (such as event listeners), lower coupling is achieved by requiring that the client objects implement a client interface. The server object only knows about the type of the client in terms of its client interface type. The concrete clients inherit from—or implement—an abstract client interface; hence the pattern name Abstract Client. In the DEM, the listener interface definition is the Abstract Client definition, and the event sources are the servers that register callbacks on the event listener clients. The event handling method that a listener implements is the callback method.


Adding a Common EventSourceMulticaster superclass

The above implementation of the event source class Airplane can be improved upon in C++. Since the responsibilities of listener management and broadcasting are invariably the same for all event sources, it is desirable to factor the listener management and broadcasting code into a common abstract base class such as EventSourceMulticaster (the Java DEM calls such classes multicasters). The only points of variation are the source type, which can be specified with a parameterized type, and the specific listener method to invoke, which can be specified with a function pointer. Please see Listing 1 for an example implementation.


template < class LISTENER_TYPE, class EVENT_TYPE >
class EventSourceMulticaster
{
public:
	EventSourceMulticaster() {}
	virtual ~EventSourceMulticaster() {}


	// design note: i considered using a function object of type
	// std::mem_fun1_t as the parm, but it requires functions with
	// a non-void return type -- something i don't want

	EventSourceMulticaster(  void ( LISTENER_TYPE :: *fp )( EVENT_TYPE* ) )
		{ m_eventFunctionMember = fp; }

	void addListener( LISTENER_TYPE* lis ) 
		{ m_listeners.push_back( lis ); }

	void removeListener( LISTENER_TYPE* lis ) 
		{ m_listeners.remove( lis ); }

	void broadcastEvent( EVENT_TYPE* evt )
	  {
	    list<LISTENER_TYPE*>::iterator iter;
          for( iter = m_listeners.begin(); 
               iter != m_listeners.end(); iter++ )
		 ( (*iter)->*m_eventFunctionMember )( evt );
	  }

private:
	list<LISTENER_TYPE*> m_listeners;
	void ( LISTENER_TYPE :: * m_eventFunctionMember )( EVENT_TYPE* );
};

Listing 1. An EventSourceMulticaster implements common event source behavior


The base EventSourceMulticaster can be further specialized into specific derived event source multicaster subclasses, which fill in the details required by the very generic EventSourceMulticaster class. For example, a LowAltitudeEventSource class definition is shown in Listing 2. Note that the derived multicaster inherits privately from the base multicaster, since we want to ensure that none of the base class behavior will ever encroach on the ultimate concrete derived class, such as Airplane.


template <class SOURCE_TYPE>
class LowAltitudeEventSource : 
	private EventSourceMulticaster
		<
		ILowAltitudeListener<SOURCE_TYPE> , 
		LowAltitudeEvent<SOURCE_TYPE> 
		>
{
public:

	LowAltitudeEventSource() 
		: EventSourceMulticaster<
			ILowAltitudeListener<SOURCE_TYPE>, 
			LowAltitudeEvent<SOURCE_TYPE> >
		  ( &ILowAltitudeListener<SOURCE_TYPE>::onLowAltitude ) {}

	void addLowAltitudeListener
		( ILowAltitudeListener<SOURCE_TYPE>* lis ) 
			{ addListener( lis ) ; }

	void removeLowAltitudeListener
		( ILowAltitudeListener<SOURCE_TYPE>* lis ) 
			{ removeListener( lis ) ; }

	void broadcastLowAltitude
			( LowAltitudeEvent<SOURCE_TYPE>* evt ) 
				{ broadcastEvent( evt ) ; }

};
Listing 2. A LowAltitudeEventSource specializes from EventSourceMulticaster.

Once these helper abstract superclasses are defined, we can finally derive our concrete event source classes from them, and inherit the correct behavior for being an event source.

class Airplane :
	public LowAltitudeEventSource<Airplane>
{
// snip ...
};

For example, the Airplane class is a source of LowAltitudeEvents. It will inherit the appropriate broadcastLowAltitude, addLowAltitudListener and removeLowAltitudListener methods. Figure 6 illustrates the design using these helper classes.

Article-dem-fig6.gif

Figure 6. Example implementation of the DEM, with helper multicaster classes.


Putting It All Together

We have defined the following components:

  • AirTrafficControl—a LowAltitudeListener.
  • Airplane—a source of LowAltitudeEvents.
  • Various supporting components (ILowAltitudeListener, LowAltitudeEvent, and so on).

With the pieces in place, we can register a listener with a source, and cause an event to be signaled. For example:

// Airplane is a source of LowAltitudeEvents.

Airplane plane;
	
// ATC (AirTrafficControl) is a listener
// for LowAltitudeEvents

ATC atc;

// register the ATC as a listener with 
// the source.

plane.addLowAltitudeListener( &atc );

// change the plane's altitude so that it emits
// a LowAltitudeEvent. the ATC::onLowAltitude
// method will be invoked.

plane.setAltitude( 50 );


JavaBeans-Style Bound and Constrained Properties

The simple Publish-Subscribe model provided by the DEM can be used in myriad ways to support loosely coupled signaling, and the dynamic registering of listeners on sources. One example is to implement support for what in JavaBeans are called bound and constrained properties. Bound Properties A bound property is a logical attribute of a class that emits a PropertyChangeEvent when the property changes. A PropertyChangeEvent knows the property name, and old and new values. For example:

template <class SOURCE_TYPE, class VAL_TYPE>
class PropertyChangeEvent : 
	public EventObject<SOURCE_TYPE> 
{
public:
// snip ...

	VAL_TYPE*	getNewValue() { return m_newValue; }
	VAL_TYPE*	getOldValue() { return m_oldValue; }
	string	getPropertyName() { return m_propertyName; }

// snip ...
};

To illustrate, if destination is a bound property in Flight then the setDestination mutator will create and broadcast a PropertyChangeEvent, using a helper multicaster base class named PropertyChangeEventSource, as follows:

class Flight : 
	public PropertyChangeEventSource<Flight, string> 
{
public:
	void setDestination( string dest ) 
	{ 
		string old = m_destination;
		m_destination = dest; 

		PropertyChangeEvent<Flight, string>* evt =
			new PropertyChangeEvent<Flight, string>
				(this, "destination", &old, &dest) ; 

		broadcastPropertyChange( evt ) ; 

		delete evt ;
	}

	// snip ...
};

The IPropertyChangeListener interface and an example are illustrated in Figure 7.

What are bound properties good for? One common use is when data widgets in a window need to refresh when the data they are associated with changes. For an example in Java, a TextField in a FlightApplet (a kind of graphical panel displayable in web browsers) may display the destination property of a Flight instance. The FlightApplet can be an IPropertyChangeListener on a Flight instance. When the Flight destination changes, the Flight can emit a PropertyChangeEvent to the FlightApplet, which then refreshes the TextField widget with the new value. To generalize this case: bound properties are useful to communicate state changes in a domain layer of objects up to a presentation layer of windows that are visualizing domain state, while maintaining low coupling from the domain to presentation layer (which is a desirable design goal). This technique can also be applied in C++ to the Document-View framework in Microsoft’s MFC.

Article-dem-fig7.gif

Figure 7. Bound properties and IPropertyChangeListeners.


Constrained Properties

A constrained property is a logical attribute of a class that emits a VetoableChangeEvent when the property is attempting to change. Like PropertyChangeEvents, a VetoableChangeEvent knows the property name, and old and new values. The key idea is that the listener may veto the potential property change, and the change will be aborted. The “vetoing” of a change request occurs with C++ exceptions. The exception may be thrown before the line of code that would actually cause the data member to be assigned. To illustrate, if altitude is a constrained property in ConstrainedAirplane then the setAltitude mutator will create and broadcast a VetoableChangeEvent, using a helper multicaster base class named VetoableChangeEventSource. Note that the AirTrafficControl listener may throw an exception, thus causing an exit from the setAltitude method before the data member change can occur. First, here is the ConstrainedAirplane class that has the constrained property:

class ConstrainedAirplane :  
	public VetoableChangeEventSource<ConstrainedAirplane, int> 
{
public:

	void setAltitude( int alt ) throw (PropertyVetoException) 
	{ 

		cout << endl << "BEFORE veto " << alt << endl ;

		VetoableChangeEvent<ConstrainedAirplane, int>* evt =
			new VetoableChangeEvent<ConstrainedAirplane, int>
				(this, "altitude", &m_altitude, &alt) ; 

		// ask my listeners if they want to veto the change.
// a PropertyVetoException can occur here

		try { broadcastVetoableChange( evt ) ; }
		catch (...) 
		{ 
			delete( evt ) ;
			throw ;
		}
	
		// only if we get passed the above attempt
		// do we change the property value

		cout << "Success. No veto " << endl ;

		delete( evt ) ;

		m_altitude = alt ;
	}

	// snip ...
};

Second, here is the AirTrafficControl class that is an IVetoableChangeListener, and which vetoes the change if the new altitude is less than some threshold.

Presumably at some point an AirTrafficControl instance has registered as an IVetoableChangeListener with the ConstrainedAirplane. The vetoableChange method is the event handling callback method that the listener must implement, and which does the source object invoke when it is attempting to change a constrained property, such as the altitude property in the ConstrainedAirplane.

class AirTrafficControl : 
	public IVetoableChangeListener<ConstrainedAirplane, int>
{
public:

	void vetoableChange( VetoableChangeEvent<ConstrainedAirplane, int>* evt ) 
		throw (PropertyVetoException)
	{ 
		int altitude = *( evt->getNewValue() );

		if ( altitude < getAltitudeThreshold() )
		{
			cout << endl << "vetoing the change" << endl;
			
			throw PropertyVetoException();
		}
	}

// snip ...

};


What are constrained properties good for? In Java, they are rarely used; bound properties are much more common than constrained. One possible use is for delegated data validation. An IVetoableChangeListener can be a data validator, which examines potential new values, and may reject some. If you use them this way, you may wish to set a meaningful message in the PropertyVetoException, so that the exception handler can discern, and perhaps display, details about the rejected data.


Conclusion

In this article we examined a C++ implementation of the Java DEM. It deviates from the Java implementation to take advantage of multiple implementation inheritance for common event source responsibilities. In addition, we explored the use of the DEM to support bound and constrained properties—an idiosyncratic use of the DEM for notification to listeners when object properties in a source object change.


References

[GHJV95] Gamma, E., Helm, R., Johnson, R., and Vlissides, J. 1995. Design Patterns. Reading, MA: Addison-Wesley.
[Martin96] Martin, R. 1996. Pattern Languages of Programming Vol 1. NJ: Prentice-Hall.