. |
The "Chain of Responsibility" pattern as described in Design Patterns by Gamma et al.1 can be a useful pattern, especially in projects that already have an established ownership composition hierarchy. However, "Chain of Responsibility" can introduce cyclic link-time dependencies, which are a serious problem in any medium-to-large project. After a brief overview of the functionality and usefulness of the "Chain of Responsibility" pattern, an innovative adaptation of the pattern called the EventHandler class will be introduced. The EventHandler class improves upon the functionality of the pattern by providing all the functionality of "Chain of the Responsibility" with none of the drawbacks associated with cyclic link-time dependencies.
COMPILE-TIME AND LINK-TIME DEPENDENCIES Compile and link-time dependencies are discussed at length in John Lakos' excellent book Large Scale C++ Software Design.2 Lakos devotes a large portion of the book to an in-depth discussion of compile-time and link-time dependencies between modules. Too many dependencies can lead to significant coupling of modules. Excessive compile-time dependencies lead to this situation: If one developer changes a line or two of code, everyone on the development team is suddenly forced into a lengthy project rebuild.
Excessive link-time dependencies lead to situations where one developer cannot work on a test driver for a seemingly trivial "leaf" class, without being required to link in a host of other files in the project just to test it. A leaf class, in the context of this discussion, is a class that is either at the top of the inheritance tree, or one that is at the ultimate periphery of the composition diagram. In other words, it might be contained by, but will not be a container of, any other class. Therefore, in a perfect world free from cyclic link dependencies, a leaf class is simply a class that shouldn't need any other class in order to link. And yet, unless a team strictly adheres to the Lakos guidelines during development, it is all too common to suddenly hear the plaintive cry from over the cube wall, "All I'm trying to do is test my CleverButTrivial class that has less than 50 lines of code, so why must I link in 53 other project source modules and wait forty-five minutes for a link?! Aaargh!!" Furthermore, if the cyclic link dependency exists in a library, using even a single class from the library could result in huge executable size bloat and skyrocketing link times as every class in the library is linked into the project.
More significantly, I have also discovered that cyclic link-time dependencies are often a sign of weakness in the implementation or software design. If the module containing class A needs the module containing class B in order to link, and simultaneously class B needs class A (a cyclic dependency) in order to link, that usually means that either:
1. Class A is doing the work (performing a function) that class B ought to be doing, because A is operating on B's data and using various B::GetXXX() and B::SetXXX() functions, or
2. The inverse is true, where class A has some data that really should belong to class B.
Both cases are instances when, in the development melee, somehow data and functions have managed to get separated among different classes. On the surface this situation seems like an absurdly obvious mistake, but in practice there are many times when the decision about where to put functionality (member functions) is not immediately obvious, because the classes might be closely related. It is also easy for a developer who is tasked with working on class A to lose sight of the big picture and put some bit of functionality into A that uses a bunch of B's data, when a bigger-picture look at things might indicate that the functionality really should belong to B.
THE COMPOSITION (OWNERSHIP) TREE One concept that is crucial at the outset of the project design is the Composition Hierarchy. This is particularly true for an application that will have a large number of objects that are dynamically managed, created and deleted on the fly at the user's request. This hierarchy is also sometimes known as the "ownership tree," because it determines which objects "own" other objects, or in other words, which objects are responsible for deleting other objects when they go out of scope.
It is common for a simple pattern I refer to as "Parent/Child" to emerge during the design of the ownership tree. This pattern is similar to "Composite" from Design Patterns, with the minor exception that the Parent and Child do not necessarily share a common base class as they do in Composite. A simple representation would be:class Parent
{
Container childContainer;
};
class Child
{
Parent *parent;
};
In other words, there is a One-to-Many relationship between Parent and Child objects. In this case, this relationship is implemented in the Parent with any available template class Container<> of Child pointers, and each Child has a pointer back to its Parent. We would expect to see various Parent member functions such as AddChild(), GetChild(), and DelChild(), along with Child member functions such as SetParent() and GetParent(). A typical AddChild() implementation might look like:void Parent::AddChild(Child *aChild)
{
aChild->SetParent(this);
childContainer.push_back(aChild);
}
This parent/child composition pattern makes it quite simple to implement the Chain of Responsibility pattern, but as we will discover, at a stiff price in terms of cyclic link dependencies.
REVIEW OF THE CHAIN OF RESPONSIBILITY PATTERN Chain of Responsibility involves allowing child objects to pass requests for information or messages the child cannot handle by itself up to parent objects for handling.
The example from Reference 1 is that of Child windows passing help requests that the Child cannot handle up to the Parent window for handling. Another example might be for a Child member function to return some common data that is actually stored in the Parent class. Consider an example in which we are modeling production runs of Automobiles (the Child) built at a ManufacturingSite (the Parent), and we would like the ability to get the factory name for each Automobile:class ManufacturingSite
{
Container carContainer;
char *siteName;
public:
const char* GetSiteName() const
{return siteName;}
};
class Automobile
{
ManufacturingSite *theSite;
public:
const char* GetSiteName() const
{return theSite->GetSiteName();}
};
This is an example of the Chain of Responsibility pattern in action, in which the Automobile class passes the responsibility for retrieving the actual name of the site on to the parent ManufacturingSite class. The request is passed on using the preexisting Parent pointer from the Parent/Child pattern.
Looks simple and straightforward, right? Be careful, because ...
CHAIN OF RESPONSIBILITY CAN MEAN CYCLIC LINK DEPENDENCIES In the above example, it was a safe bet that class ManufacturingSite was already link-time dependent on class Automobile, if for no other reason than the parent functions necessary to add and remove the children in the parent's child container, as well as the code in the ManufacturingSite destructor to walk the child container and delete each child Automobile.
But it was also likely that before GetSiteName() came along, class Automobile was not link-time dependent on ManufacturingSite. The Automobile class could be developed and tested (and reused!) completely in isolation from the ManufacturingSite class. However, as soon as the above GetSiteName() function was added, Automobile and ManufacturingSite became circularly link-time dependent. This realization leads to the rather distressing discovery that the Parent pointer in the various Child classes is of extremely limited utility, given that almost any use of the pointer from within a Child member function to invoke a Parent member function will incur a circular link dependency.
So what's a poor programmer to do? It is common for Child classes to need to communicate with the Parent classes. In a small project, you might just elect to bite the bullet and make the Parents and Children circularly dependent, but what about medium and large projects? How can Child classes communicate with Parent classes without incurring circular link dependencies?
THE EVENTHANDLER SOLUTION One solution is to communicate between parents and children using Events. Data is encapsulated inside an Event type, which is posted to a general Event queue for other classes to read. Graphics-based environments such as Windows NT provide a prebuilt messaging (event) system that can be extended and used for user-defined messages or events. For example, both Borland's OWL and Microsoft's MFC class frameworks offer this capability, whereby the programmer can define his own application-specific events and then direct the framework to respond to those events just like they respond to system-generated events. But what if that system isn't available for some reason, such as a Windows NT console application, or a Unix-based application? What if portability across platforms and operating systems is an issue?
The answer is to create an Event class and EventHandler class to provide simple messaging services across classes. The Event class will encapsulate the data we wish to communicate from the sender to the receiver. The EventHandler class will essentially use the Chain of Responsibility pattern to handle individual events, and to post unhandled events up the ownership tree for handling elsewhere.
If the EventHandler class will continue to use the parent pointer for communication up the ownership tree, how then have we avoided the circular link dependencies described above? The dependencies are avoided by designing the EventHandler class to be used as a mix-in base class of both Parent and Child, so that the parent pointer contained within each Child is actually a pointer to type EventHandler rather than to type Parent.
THE EVENT CLASS This class represents a bundle of data to be sent along the EventHandler tree. Refer to Listings 1 and 2 for the EventHandler.h and EventHandler.cpp files.
To use this class, inside any member function of a class derived from EventHandler, create an Event with the desired command value, thus:
Event ev(CM_DATAMODIFIED);
Optionally, the two public union'd data members may be set to whatever makes sense for this particular event:ev.infoInt1 = GetRow();
ev.infoInt2 = GetCol();
Then post the event to the EventHandler tree using:
EventHandler::PostEvent(ev);
The EventHandler* 'sender' field will be set automatically by the EventHandler::PostEvent() function, and the Event will then be dispatched through the EventHandler tree until every EventHandler has been visited or the passed event is cleared.
Events that have been cleared using Clear() are not sent to any further EventHandler objects. Events that have been marked local using SetLocal() are not promoted any 'higher' up the EventHandler ownership tree.
THE EVENTHANDLER CLASS This class is intended to be used as a mix-in base class, to provide basic event-handling functionality to any object.
It is intended to be used in the construction of a 'tree' of EventHandler objects, where each EventHandler object has a pointer to its owner as well as a container of child EventHandler pointers:
Events are initiated at any level of the tree, with a call to:
EventHandler::PostEvent(Event& ev);
Events will then be dispatched to the other EventHandler objects in the tree, by calling the virtual function:
EventHandler::HandleEvent(Event& ev);
on behalf of each EventHandler object, until (a) every EventHandler object has been visited once, or (b) the passed Event is marked as 'clear.'
Events are dispatched beginning at the current level of the tree, and all child EventHandler objects are visited before the Event is promoted up the ownership tree. This dispatching, initiated by the client using EventHandler::PostEvent(Event&), is handled internally by the private EventHandler member functions ProcessEvent() and PropagateEvent(). These two functions implement a scheme in which all child EventHandler objects are visited with the posted Event before it is promoted up the tree, in an effort to give child or sibling EventHandler objects a chance to handle and clear the event before it is posted all the way to the top. This is slightly more complicated to implement than simply passing all events to the top EventHandler and then recursively walking the tree from the top down, but is done for performance reasons to hopefully prevent too many Events from being posted throughout the entire EventHandler hierarchy.
If at any point during this traversal of the tree the event is marked as 'clear' by a particular HandleEvent() function, then it will stop traversing the tree. Also, if at any point an event is marked as 'local,' then it will continue to traverse the list of children, but will not be promoted any higher up the tree.
Classes derived from EventHandler should override the HandleEvent() function to respond appropriately to the Event commands of interest. If the class overriding the HandleEvent() function knows with certainty that a given event is handled completely, it should mark the passed event as clear using:
Event::Clear();
If the class overriding HandleEvent() knows that a given event needs to be sent to all its children but should not be sent further up the ownership tree, it should do this by marking the passed event as local using:
Event::SetLocal();
Note that by default, the child EventHandler pointers are not owned by the owner EventHandler object, and therefore will not be deleted when the owner EventHandler object goes out of scope or is deleted. This can be changed with the SetOwnsChildren() function call.
EXAMPLE OF USE Listing 3 shows a short example that demonstrates the use of the Event and EventHandler classes. Three classes are defined, called Parent, Child, and GrandChild. The names are not intended to convey any sort of inheritance relationship, but instead are intended to convey composition relationships. Instances of these classes are put together in the following composition hierarchy:
Each class overrides the virtual function HandleEvent(), and each has a simple member function called Go() that will actually post an event to the EventHandler hierarchy. Here is the output from c1.Go() :in Child::HandleEvent(), cmd = 2
in GrandChild::HandleEvent(), cmd = 2
in Parent::HandleEvent(), cmd = 2
in Child::HandleEvent(), cmd = 2
CONCLUSION The Chain of Responsibility pattern can be useful. It can be implemented easily in projects that already have an established ownership or composition hierarchy. However, using the established ownership tree to implement the pattern will almost certainly lead to cyclic link dependencies. The drawbacks and penalties associated with cyclic link dependencies include loss of reusability and excessive link times, costs that become excessive for any medium or large project.
The EventHandler class as presented in this article represents an alternative implementation of Chain of Responsibility that avoids cyclic link dependencies. The implementation is simple, powerful, and useful in its own right even when cyclic link dependencies are not a governing concern.
References
1. Gamma, E. et al. Design Patterns: Elements of Reusable Object-Oriented Software, Addison–Wesley, Reading, MA, 1995.
2. Lakos, J. Large Scale C++ Software Design, Addison–Wesley, Reading, MA, 1996.
Elliott Jackson is a nuclear engineer and senior software designer for Westinghouse Electric in Pittsburgh, PA. He is responsible for C++ development, mentoring, and advocacy, and is currently the technical lead on a complex C++ project. He can be contacted at ewj@westolcom.
|