Pattern Hatching

Latter-Day Events

John Vlissides

C++ Report, June 1997

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.

DIFFERENCE #1: TELL, DON'T ASK

Part of what makes the new approach different is how events get delivered. Currently, nextEvent is the way an application gets events. Application code calls this operation when it's ready to process the next event. If it happens to call nextEvent when no event is pending, one of two things will happen: the calling thread will block, or nextEvent will return a null value, probably resulting in a busy-wait. It's up to the framework designer to decide which will be the case.* Either way, the consumer of the event initiates its handling. This is the pull model of event-driven programming, because the event consumer (e.g., the application) is the active entity, "pulling" information from the event producer (the framework in this case).

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.

DIFFERENCE #2: DECENTRALIZED DISPATCH

The other key difference concerns centralization of the source of events---or rather the avoidance thereof. Currently, nextEvent centralizes the event delivery mechanism in the framework. That in a nutshell makes it the type-laundering bottleneck it is. So if centralization is the problem, isn't some form of decentralization the obvious solution?

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.

MATCHING GRANULES

Now we have to figure out where to transfer that responsibility. Since extensibility is a concern, sooner or later we must consider what will change when one extends the system. Let's do it sooner: The change that concerns applications is the definition of new events. The framework may predefine some general-purpose events, like maybe TimerEvent or ErrorEvent. But most applications will define their own events on a much higher level of abstraction, like the vending machine's CoinInsertedEvent and CoinReleaseEvent classes.

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.

THE NEW DESIGN

First, forget about a common base class for events. Subclass-specific interfaces are the norm. There's hardly any base class functionality. There's no longer a nextEvent operation requiring a polymorphic return value. In sum, a common base class is more trouble than it's worth. Instead, we define free-standing event classes whose interfaces are exactly what their clients need---no more, no less.



Next, each of these classes gets a unique registration interface:



There are two registration-related operations: register and notify. Both are static. Any instance that's interested in getting CoinInsertedEvents, for example, must register itself with the CoinInsertedEvent class. Any object can herald the arrival of a new CoinInsertedEvent instance by calling CoinInsertedEvent::notify with that instance as a parameter.

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:



A class that's interested in handling one or more of these events must implement the corresponding interfaces. For example, say a CoinChanger class controls the behavior of the coin changer subsystem in the vending machine. The coin changer wants to know when the user presses the coin release button so that it can dispense change, if need be. It also wants to know when a product gets dispensed successfully so that it can reset itself for the next consignment. CoinChanger must therefore implement both the CoinReleaseHandler and ProductDispensedEvent interfaces:



Finally, the coin changer is responsible for notifying other subsystems about coin insertions. When the underlying hardware senses a coin, CoinChanger responds by creating a CoinInsertedEvent instance (as the dashed arrowheaded line indicates). After initializing the event with requisite information, it calls CoinInsertedEvent::notify, passing the new instance as a parameter. notify in turn iterates through all the registered implementors of the CoinInsertedHandler interface---that is, all objects that have expressed interest in coin insertions---calling their handle operation and passing along the CoinInsertedEvent object. Meanwhile, the CoinChanger object is registered (probably during instantiation) with the CoinReleaseEvent and ProductDispensedEvent classes. Thus whenever other subsystems in the vending machine produce CoinReleaseEvents or ProductDispensedEvents, the CoinChanger instance will find out about it. No type tests, no downcasting, no switch statements, no kidding.

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

Sure, that's a bit short of the goal of defining only one new class and changing no existing code. We are introducing an extra interface (BillAcceptedHandler), but that doesn't amount to much work. And the changes to existing code are limited to the application, not the framework, which can content itself with a fixed set of predefined event classes and handler interfaces. Life has gotten better.

MULTICAST

What you've witnessed is an application of MULTICAST, a new pattern we've been tinkering with. I'll take a closer look at it next time.

MAILBAG

Michael McCosker writes,
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.

ACKNOWLEDGMENTS

Doug Schmidt and the GOF helped me clarify the event stuff. Rey Crisostomo pointed out the semantic nuances of pure virtual destructors. Gracias, muchachos!

Footnotes

* Many frameworks provide blocking and non-blocking analogs of nextEvent in support of both options. Some also allow blocking with a time-out.

** 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].

References

[GHJV95] Gamma, E., R. Helm, R. Johnson, J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, Reading, MA, 1995.

[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.


©1997 by John Vlissides. All Rights Reserved.