Pattern Hatching

Multicast

John Vlissides

C++ Report, September 1997

Multicast is a pattern in progress, assuming it's a pattern at all. Still, I thought it might be amusing to present it here in all its half-baked and disshelved glory. I've even included some of our recent exchanges on it, which should be even more amusing. Maybe too amusing. After all, people will realize we're no more prescient than anyone else. Those who attribute extraordinary powers to the Gang of Four will be appalled by our generally chaotic process of pattern development. I might burst a bubble or two---which would be a good thing come to think of it. Anyhow, my real goal is to get others in on the fun. Who knows, a kind and insightful soul out there could finally set us straight on this beasty.

I'll hold off on the pivotal Intent section, always a bone of contention, and skip to the Motivation. It'll be familiar to you if you've been with us for the past two columns.1,2 The scenario here is similar but not identical to the vending machine example I've been using.

Motivation

A program is event-driven if its flow of control is governed by external stimuli called events. Event-driven designs are common in real-time control applications. A key design challenge lies in making these systems extensible and type-safe at the same time.

Consider a modern, digitally controlled vending machine. It has several subsystems, including a snack dispenser, a coin changer, a keypad for selecting products, an alphanumeric display, and a "black box"---a simple computer that controls it all. The interactions between these subsystems and the customer get quite complicated. You can manage the complexity by modeling both the subsystems and the interactions between them as objects.

When a customer deposits a coin, for example, the CoinChanger object (which monitors the coin changer subsystem) produces a CoinInsertedEvent object. This object records the details of the event, including the time of the deposit and its amount in cents. Additional classes model other events of interest. A KeyPressEvent denotes the press of a key on the keypad. An instance of CoinReleaseEvent records a customer's request for his or her money back. ProductDispensedEvent and ProductRemovedEvent objects mark the final stages of a snack's consignment. The number of event classes may be large and potentially open-ended: adding a bill changer and its associated events (e.g., BillInsertedEvent) should require minimal changes to existing code.



What happens after an event gets created? Which object(s) will use the event, and how does it get to them? The answers depend on the kind of event. CoinChanger will be interested in any CoinReleaseEvents that occur after a coin has been deposited. But a CoinChanger won't want to receive the CoinInsertedEvents it creates for other objects. Similarly, a Dispenser object will generate instances of ProductDispensedEvent but won't be interested in receiving them. It will be keenly interested in KeyPressEvent objects, however, since they determine which snack to dispense. The interests of different subsystem objects will vary, perhaps even dynamically.

What's developing here is a rat's nest of dependencies between event objects, their producers, and their consumers. Complex dependencies are undesirable because they make the system harder to understand, maintain, and modify. It should be easy to change an object's event interest not just statically but at run-time as well.

A common solution uses an event registry to keep track of these dependencies. Clients register interest in a particular event with the registry. After creating an event, an object passes it to the registry, which delivers it to the interested objects. This approach requires two standard interfaces, one for events and another for the objects interested in handling them:



When an instance of a Handler subclass such as Dispenser receives an event through its handle operation (which implements Dispenser's handling of the event), the event's concrete type is not known statically. That's significant because the concrete type will define operations beyond those in the Event interface. Different kinds of events record different information; no single Event interface can anticipate the needs of every subclass.

Therefore each Event subclass extends the basic Event interface with subclass-specific operations. To get at these operations, Dispenser must attempt to downcast the event to a type it can handle:

    void Dispenser::handle (Event* e) {
        CoinReleaseEvent* cre;
        ProductDispensedEvent* pde;
        // similar declarations for other events of interest

        if (cre = dynamic_cast<CoinReleaseEvent*>(e)) {
            // handle a CoinReleaseEvent

        } else if (pde = dynamic_cast<ProductDispensedEvent*>(e)) {
            // handle a ProductDispensedEvent

        } else if (...) {
            // etc.
        }
    }
One problem with this approach is that it's not type-safe. Getting at the subclass-specific operations requires dynamic_casts, the results of which cannot be checked at compile-time. That means some type-related programming errors may remain hidden until run-time. This code has all the drawbacks of tag-and-switch-style programming as well: it's clumsy, it's hard to extend, and it's inefficient.

The MULTICAST pattern shows how to deliver information to interested objects in an extensible and statically type-safe manner. The pattern doesn't require singly-rooted Event or Handler hierarchies. Instead, you define an abstract handler class for each concrete event class---for example, a CoinReleaseHandler for CoinReleaseEvent. Any class of objects that wants to handle CoinReleaseEvents must inherit from a CoinReleaseHandler class. The same goes for handling other types of events: interested parties must inherit from the corresponding handler classes.



Here, CoinChanger inherits from both CoinReleaseHandler and ProductDispensedHandler because it's interested in both CoinReleaseEvents and ProductDispensedEvents---it might have to dispense coins in either event. As before, each handler class defines a handle operation in which subclasses implement their handling of the event. But unlike the original registry approach, handle's argument provides the precise concrete type of event, so there's no need for downcasting---the operation is statically type-safe.

But how do events get delivered to interested objects---that is, who calls handle? You could define a Registry class with register and notify operations as before, except now there are numerous classes of handlers that aren't related by inheritance. Consequently, not one but several register operations are needed, one for each kind of handler. And you'll have to add a register operation to the Registry class whenever you define a new kind of event. In other words, you'll have to change existing code.

Decentralization provides a way around this problem. Instead of registering with one big registry, clients can register interest directly with the objects that create events. For example, if a client is interested in CoinInsertedEvents, it registers that interest with CoinChanger, the class that instantiates those events:



Whenever CoinChanger generates a CoinInsertedEvent, it calls its notify operation to deliver the event to all registered CoinInsertedHandlers. The compiler guarantees that the object they'll receive is precisely the kind they're interested in: CoinInsertedEvent.

Similarly, a client interested in ProductDispensedEvents registers itself with the Dispenser. In general, you register interest in a class of events with the instantiator(s) of that class. Decentralizing registration like this improves extensibility. When you define a new kind of event, the code you change is limited to the class(es) that create the events, whereas the centralized approach requires changing the registry's interface and implementation as well.

* * *

The main difference between this design and that of previous columns is that here, clients register event interest with the class that instantiates the event---CoinChanger in this case. The original design had clients registering with the event classes themselves. Actually, my first cut at the Motivation did that too, but Erich objected:

In my view, you register interest not with the event but with the sender of the event. In this case, the vending machine should have operations like addCoinReleasehandler, addCoinInserthandler, etc.
I on the other hand feel it's important to motivate the case for putting the registration interface into the event classes. The impetus is ease of extension. You want to avoid upheaval when you define new kinds of events. If the registration mechanism is in an existing class, you'll have to change it to incorporate new registration operations. Putting the registration interface into the event itself makes it easier to accomodate new events.

From a modeling perspective, however, Erich is right, because registering with an event class may well seem unnatural. Even though we're talking about a static operation on a class and not an instance, it still looks as though you're registering an event with itself!

Since Erich's preference constitutes a more general case, we agreed it should be the representative design. Hence the Structure, Participants, and Collaborations sections put the registration interface in the sender. Had I insisted on my preference, we'd end up confusing people by advocating a particular design in the Motivation and a slightly different one thereafter.

Structure



Participants

Message (ProductDispensedEvent)

Sender (Dispenser)

AbstractReceiver (ProductDispensedHandler)

Receiver (CoinChanger)

Collaborations

* * *

The Structure, Participants, and Collaboration sections thus illustrate the general case. My earlier examples merely lump Sender and Message classes together to form the Event classes. This remains a viable option, incidentally, so we relegated it to the Implementation section.

The four of us pretty much agree on the content so far, but seeds of contention crop up in the Applicability section.

Applicability

Use MULTICAST when all of the following are true:
While these points haven't elicited negative reactions by themselves, the last one may hold the crux of our differences. But let me explain our biases first. Erich, Richard, and I were weaned on C++, which is relatively strongly typed. Ralph is a Smalltalker---Smalltalk having no notion of static type checking. It's probably no coincidence then that while the rest of us think Multicast is worthy of design pattern status, Ralph thinks it's just a variation on the Observer pattern. As such he believes it should be written up as part of a new and expanded Observer.

There are clear similarities between Observer and Multicast as it's currently conceived. Both maintain dependencies between objects, both communicate information between the objects, both emphasize extensibility, among other things. Yet most of us feel there's an essential difference. Erich expressed this sentiment early on:

Multicast is very close to Observer, but the distinction is subtle.
Subtle indeed. I wrestled with the distinction and came up a strawman:
In Observer, you're talking about a one-to-many dependency. Before you apply the pattern, the subject and its observers would have likely been lumped into one object. Observer partitions that object to give you flexibility, etc. Observer worries little about the information that passes between them or the extensibility thereof---it focuses on notification and subject-observer consistency.

Multicast focuses on the information that's passed between sender and receiver: its extensibility and type-safety. Moreover, the sender and receiver are usually unrelated in what they model, and the connections between them are much more unpredictable, and probably more dynamic too.

Ralph, bless his heart, was not convinced:
Once you implement Observer, you will definitely get very interested in what passes back and forth! The pattern in the book doesn't talk enough about this. It gives hints about push versus pull and the like, but it is vague and uninformative. It needs to be much more concrete and discuss the problems that can arise when you use Observer. In my opinion, the topics you discuss are all ones that anybody who uses Observer for a large project will need to think about.

I don't believe [the bit about Multicast's sender and the receiver being unrelated in what they model]. Maybe I am missing something. But in [the VisualWorks Smalltalk environment], senders and receivers tend to be unrelated, and connections can be very dynamic. Since VisualComponents (the typical Observer) and ValueModels (the typical Subject) are all extremely reusable and are usually mixed-and-matched, connections between them are pretty unpredictable as well.

Given the lack of any notion of multiple interface inheritance in Smalltalk, it's no wonder Ralph thinks Multicast is little more than a wart on Observer, or maybe a mutant Observer. Do you suppose Multicast is more of an idiom for strongly typed languages than a design pattern?

In a later message, Ralph posits why the rest of us think Multicast is a worthy design pattern in its own right.

I think one of the unspoken reasons you think Multicast is important is because of static type checking. You are trying to avoid type casting. You have two different ways of avoiding it. One is to pass an Event that you can dispatch on. The other is to make separate Handler hierarchies. It seems to me that if you did the first, you would not need to do the second, so I am not sure why Multicast needs both. Maybe it is because you want to put the behavior in the Observer (i.e., the Handler), not the Event. But it is clear to me that Observer needs to talk about how static type checking makes things complicated.

In summary, Observer defines a rich space of design variations. I don't see any reason why Multicast is much different from the other variations. That is why Observer is a pattern and not a reusable mechanism. It gets changed each time it is used. When you are talking about Multicast, you are really talking about some common and useful variations of Observer. But you are packaging a particular set of variations, while other sets of variations are just as interesting and you are ignoring them. I believe it would be better to explore the entire space.

I have to agree with a lot of this, and judging from the silence, so does everyone else. But you can't keep dumping stuff into a pattern indefinitely. So rather than rebut these points, Erich asked a new and different question:
Is it that bad to promote a significant refinement and variation of a pattern as a separate pattern? Instead of having 20 implementation bullets I prefer to have a separate pattern.
Herein lies an important issue we've been skirting, that of pattern scalability. Could it be the crux within the crux? I thought it might be:
There's fertile new territory in making our patterns more scalable. For each of our patterns I've got a file for comments, feedback, and any new insights we've had. Many of these files have gotten pretty big. If we were to incorporate everything we've learned about, say, Singleton, into the Singleton pattern, the result would be ungainly. Some of our patterns---Observer and Composite come to mind---are arguably too long as it is.

How do we promote "scalability"? Making each pattern into a pattern language is one possibility. But I confess to being underwhelmed by [much of what's been] written by that name. It would be a real win to come up with some kind of superstructure that encompasses the current patterns while making room for new insights, extensions, and variations. If there's one thing I'm learning from Multicast, it's that our patterns can't go on growing forever.

I also felt a need to come up for air and ask a few fundamental questions:
  1. How are [Multicast and Observer] related?
  2. Are they dependent? If so, must we roll them into one pattern?
  3. Is the one-to-many dependency of Observer useful by itself?
  4. Should every application of Observer also be an application of Multicast?
Ralph's thought on #1 is that Multicast is either a "special-case/extension" of Observer, or it is a "composite" pattern that contains Observer.* Both imply that the patterns are dependent, at least to a degree. His answers to the last two questions reinforce this impression:
[Is the one-to-many dependency of Observer useful by itself?] It is in simple systems. When life gets complicated, there are various ways to cope. Some of them are simpler than Multicast. For example, ValueModels eliminate a lot of case statements without making events be objects.

[Should every application of Observer also be an application of Multicast?] I think the real question is, "If you are going to use Multicast, is it worthwhile using Observer without Multicast?" I can see arguing "no," because the advantages of using a simpler mechanism in part of your system are probably overwhelmed by the complexity of having two mechanisms that do more or less the same thing, and so designers will have to choose which one to use. Just use one, and they won't have to decide. Life will be easier.

If that is true, then maybe a GUI framework that is supposed to be scalable should use Multicast, since large applications will need it. This would be an argument to use Multicast instead of Observer everywhere.

The main counterargument is that Observer works just fine for simple systems, and lots of systems are built successfully from it.

Clearly Ralph believes Observer and Multicast are distinct, related, and dependent in some sense. Aha! thought I, isn't there a similar relationship between Abstract Factory and Factory Method? Abstract Factory uses Factory Method, and yet these are separate patterns. But he demurs:
There are lots of ways for patterns to depend on each other. It is possible to make an Abstract Factory that does not use Factory Method, but I claim that it isn't possible to have a Multicast that does not use Observer. I'd be happy to be proven wrong.
Well, I can imagine applying Multicast to implement a many-to-one dependency, which is the opposite of Observer's stated intent (a one-to-many dependency). We haven't duked this one out yet, but I predict he'd claim that this too is an example of Observer. A many-to-one dependency can be viewed as multiple degenerate applications of the pattern; that is, many-to-one is just a collection of subject-observer pairs all sharing the same observer. Voilà!

Phooey, I say.

There's another way around my counterexample, which is to claim that Observer's intent is just plain wrong---that one-to-many isn't a valid constraint on Observer. Well of course. We can prove anything we want about a pattern if we can change it. Not that I think our patterns are sacrosanct or anything; I simply prefer to change only one variable at a time, and at present that variable is called "Multicast," not "Observer."

Which brings us to a whole 'nother can of worms, that of Multicast's intent.

Intent

Deliver information to interested objects at arbitrary times through an extensible interface.
Erich and I thought this summed up the pattern's intent rather well. Ralph however thinks it says virtually nothing. He argues there should be no difference in intent between Multicast and Observer, which is perhaps the best argument for making them one and the same pattern.
I claim that Multicast and Observer are used for the exact same purpose. In other words, they have the same intent. I think that the current intent for Multicast is misleading because it hides this fact. On the other hand, Factory Method and Abstract Factory have different intents.

In my opinion, Multicast is a special case of Observer, a more complicated case. This is not a proof that there shouldn't be two patterns in the catalog, just something to consider when you are deciding what should be there.

And so it goes. I'll have to defer the rest of the story to next time. Meanwhile let me encourage you to submit your thoughts on these issues. Send them to me at vlis@watson.ibm.com, and I'll forward them to the rest of the Gang for their perusal. I'll include the most interesting fallout in future installments. Let the flames begin!

ACKNOWLEDGMENTS

Thanks to Erich and Ralph for letting me publish stuff they never intended for public consumption.

Footnotes

* "Composite" in this context refers to a combination of patterns, not the GOF Composite pattern.

References

1 Vlissides, J. "Pattern Hatching," C++ Report, February 1997.

2 Vlissides, J. "Pattern Hatching," C++ Report, June 1997.

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