Where were we? Ah yes, a real-time control framework for things like vending machines and cruise missiles. At issue was the abstract base class Event [Vlis97]. It's interface is minimal, with just two operations:
virtual long timestamp() = 0; // precise time of the event
virtual const char* rep() = 0; // low-level representation
The framework deals exclusively in terms of this interface, while
application-defined subclasses extend it to model domain-specific
events.
The problem I left unresolved last time concerns how the framework supplies events to application code. In the standard approach, the framework defines a virtual Event* nextEvent() operation that the application calls whenever and wherever it wants to handle an event. The unhappy consequence is that the return value invariably must be downcast to an application-defined type. In the vending machine case, this precipitated the following code:
Event* e = nextEvent();
CoinInsertedEvent* ie;
CoinReleaseEvent* re;
// similar declarations for other kinds of events
if (ie = dynamic_cast<CoinInsertedEvent*>(e)) {
// call CoinInsertedEvent-specific operations on ie
} else if (re = dynamic_cast<CoinReleaseEvent*>(e)) {
// call CoinReleaseEvent-specific operations on re
} else if (...) {
// ...you get the idea
}
The framework knows only about the base class, Event. So
whenever the framework deals with an event, it effectively "launders
out" all type information beyond what Event
declares---including all vestiges of subclass-defined extensions. Type
information gets lost as a result, and we have to work hard to recover
it.
In situations where interface extensions are the norm, this is no small problem. The event handling code is not type-safe. The results of dynamic_casts cannot be checked at compile-time, leaving discovery of type-related programming errors to crash-time. Then there are the classic drawbacks of tag-and-switch-style programming: the code is clumsy, it's hard to extend, and it's inefficient.
After an application of the Visitor pattern [GHJV95] failed to remedy the situation, I promised I'd devote my next column to a radically different approach. Here's the beef.
The yin to this pull model yang is called, not surprisingly, the push model. There, the consumer passively awaits notification of an event's arrival. Because the producer must push information to arbitrarily many consumers, the model requires a priori registration of consumers with the producer(s) that should notify them.**
As far as we're concerned, the question of push versus pull boils down to establishing the locus of control in this framework. The push model tends to simplify the consumer at the producer's expense, while the pull model does the reverse. So an important thing to consider is the relative abundance of producer and consumer code. If you have few producers and lots of consumers, then the push model arguably makes more sense. This isn't a hard-and-fast rule, mind you. There may be considerations that favor the pull model independent of the number of producing or consuming code sites. But in our real-time control framework, it's reasonable to assume there will be more loci of code for consuming events than for producing them. With counterarguments lacking, push is the model of choice.
Indubitably, but first things first. If an extensible and type-safe event delivery mechanism is our ideal (it should be), then the interface that delivers events to application code---or at least the interface for delivering application-specific events---cannot reside in the framework at all, lest we end up in dynamic_cast purgatory as before. We have to deliver events to application code in a type-safe way and let applications define new kinds of events without changing existing code, be it code in the framework or the application. That, together with the switch from pull to push, calls for the demise of nextEvent as the sole interface for delivering events.
The granule of change, therefore, is the kind of event. This is significant because matching the granule of change to the granule of extension is key to minimizing upheaval in the face of extension. A given change in functionality should call for a commensurate change in implementation. Clearly, you don't want a small functional change to provoke massive code modifications. But what's so bad about the converse? Why shouldn't a major change in functionality to require only small changes to code?
That may seem desirable, but in fact it's utopian. Achieving it usually means one of two things: either the system is functionally unstable and hence bug-prone, or, more likely, changes are expressed not in the system per se but in another, usually interpreted specification---a scripting language versus C++, for example. In the latter case the system is unlikely to need modification only because "the system" refers to the interpreter. And rightly so: if adding functionality means changing the interpreter, then someone somewhere has failed spectacularly.
So if this matching principle is valid, what are its implications for our design? We model each granule of change---the kind of event---explicitly as a class. A class defines both an implementation and an interface. By the matching principle, then, the code for extended functionality should comprise both implementation code and any specialized interfaces that clients need for type-safe access. In other words, a new kind of event should give rise to one new class, period. No other code should be added or changed.
To recap, the new design should (1) deliver events by pushing them to their consumers, and (2) it should require at most one new class per application-specific event, with no changes to existing code. Tall order, I know, but we'll very nearly achieve it.
You may have noticed that you can't register just any old object with an event class; it has to be of a specific type. If you're confused, look at the argument of each register operation. For CoinInsertedEvents the registrant must be of type CoinInsertedHandler, for CoinReleaseEvents it's CoinReleaseHandler, and so on. These types are defined in separate mixin classes that exist solely to define an event-handling interface:
Extension is easy too. Say a new model of vending machine comes with a bill changer. This calls for integrating a new BillAcceptedEvent into the control software. All it entails is defining that class plus a corresponding BillAcceptedHandler mixin. Then any subsystem that cares about the new event has to
Having just read your Pattern Hatching article [titled] "Type Laundering," I have one query. Under the heading "Event's Loss is Cursor's Gain" you mention using a pure virtual destructor to force subclasses to define their own destructor. My understanding of C++ is that all destructors are called. In the environment I work in (Win32 on PCs), calling a destructor at address zero causes a page fault. Is this just a problem in this environment or should one never have a pure virtual destructor?The usual way I make a class abstract in C++ is by protecting its constructor, as opposed to making at least one member function pure virtual. In the "Type Laundering" article I was trying to demonstrate the virtues of my approach. Here again is the class in question:
class Cursor {
public:
virtual ~Cursor () { }
protected:
Cursor () { }
};
The only other way to make Cursor abstract is by making the
destructor pure virtual. But what the heck does a pure virtual
destructor mean? Destructors aren't inherited per se;
all of them get called sequentially. Therefore a pure virtual
destructor must be defined. Is that possible?
Surely. Consider the following passage from the draft standard of December 2, 1996:
Section 10.4, paragraph 2: "A pure virtual function need be defined only if explicitly called with the qualified_id syntax ... Note: a function declaration cannot provide both a pure_specifier and a definition."Since the pure virtual destructor will in fact be called explicitly during destruction, this section indicates that it must be defined---not in the declaration, but in a separate definition:
class Cursor {
public:
virtual ~Cursor () = 0;
};
Cursor::~Cursor () { }
I suppose there isn't a whole lot of difference between the two
approaches after all. In one, you have to protect all
constructors; in the other, you have to define a pure virtual
destructor. Pick your nauseant.
** The push model exemplifies "The Hollywood Principle," or "Don't call
us; we'll call you" [Vlis96]. For an in-depth
treatment of both models, see the excellent article by Schmidt and
Vinoski [SV97].
[SV97] Schmidt, D. and Vinoski, S. "The OMG Event Object Service,"
C++ Report, February, 1997.
[Vlis96] Vlissides, J. "Pattern Hatching," C++ Report,
February 1996.
[Vlis97] Vlissides, J. "Pattern Hatching," C++ Report,
February 1997.
References
[GHJV95] Gamma, E., R. Helm, R. Johnson, J. Vlissides. Design
Patterns: Elements of Reusable Object-Oriented Software,
Addison-Wesley, Reading, MA, 1995.
©1997 by John Vlissides. All Rights Reserved.