The software industry is notorious for its disclaimers. Developers can pretty much disavow any and all responsibility for their creations. So in the spirit of equal access,
Warning: This article contains speculative designs that are provided on an "as-is" basis. The author and publisher make no warranty of any kind, expressed or implied, regarding these designs. But feel free to bet your company on them anyway.What I'm doing here is thinking aloud about a design problem that's nettled me for almost a decade now, because the common cure is often worse than the disease.
You decide that it's important to keep changes to the user interface from interfering with application functionality and vice versa. So you separate the user interface from the underlying application data. In fact, you consider a full-blown Smalltalk Model-View-Controller (MVC)-like partitioning between the two. MVC not only separates the application data from the user interface but also allows multiple user interfaces to the same data.
Thumbing through Design Patterns [Gamma+95], you spy the Observer pattern, which tells you how to achieve this partitioning. Observer captures the relationship between the primitive data and its potentially numerous presentations:
The subject stores the definitive information, and observers get updated whenever the subject's information changes. When the user saves his or her work, it is the subject that gets saved; the observers needn't be saved because the information they display comes from their subject.
Here's an example. To let a user change a numerical value such as an interest rate, an application might provide a text entry field and a pair of up-down buttons (see Figure 1). When the subject changes (say, because a user increased the interest rate a notch by pressing the up button), the subject that stores the interest rate notifies its observer, the text entry field. In response, the text entry field redisplays itself to reflect the new interest rate.
Now in addition to primitive data, the applications that use our framework need higher-order abstractions, such as loans, contracts, business partners, and products. To maximize reuse, you've defined fine-grained subjects and observers that you compose to create higher-order subjects and observers for these abstractions.
Take a look at Figure 2, which shows a user interface for entering loan information. The interface is implemented as an observer of a subject. Figure 3 shows that this observer is actually a composition of primitive observers, and the subject is a composition of the corresponding primitive subjects.
This design has several nice properties:
But wait---there's more. Run-time redundancy is only part of the problem; we've got static redundancy, too. Consider the classes that implement these object structures. The Observer pattern prescribes separate Subject and Observer class hierarchies, wherein the abstract base classes define the notification protocol and the interface for attaching and detaching observers. ConcreteSubject subclasses implement specific subjects, adding whatever interface their concrete observers need to figure out what changed. Meanwhile ConcreteObserver subclasses define distinct presentations of their subjects, with their Update operation specifying how they update themselves.
Figure 5 summarizes these static relationships. Pretty complicated, huh? Parallel hierarchies tend to get that way. Beyond the mechanical overhead of coding and maintaining these hierarchies, there's also conceptual overhead. Programmers have to understand twice as many classes, twice as many interfaces, and twice as many subclassing issues.
Wouldn't it be more wonderful if we could get away with just one class hierarchy and one instance hierarchy? Trouble is, we don't want to give up the clean partitioning of application data and user interface that the separate class and object hierarchies buy us. What to do?
Assuming the answer is no, then what's an alternative? If we want to limit ourselves to one object hierarchy, then we've got to find a way to present subject information without maintaining a parallel observer hierarchy and without simply lumping the hierarchies together.
Since memory is at issue here, let's contemplate a classic trade-off between space and time. Instead of storing the information, how about computing it on the fly? We don't have to store information that we can recreate whenever we need it---provided we don't have to recreate it very often. How often is "very often"? Often enough to have a unacceptable impact on performance.
Fortunately, the number of situations in which observers actually do anything is pretty small, at least in our application. Basically they spring to life in three circumstances:
Now don't get me wrong. I'm not saying we won't use any objects to do the observers' work. We'll use objects to encapsulate the presentation machinery all right. We just want to use substantially fewer objects than Observer calls for---hopefully a constant number, as opposed to a number proportional to the size of the subject hierarchy. And we don't want to store lots of links to subjects either; we'd like to compute the links rather than store them.
The three circumstances I've described usually precipitate traversal(s) of the observer structure or the subject structure (and often both). A change to a subject and the resulting change to its observer may necessitate a traversal of the entire observer structure, for example, to redraw affected user interface elements. Similarly, the computation that determines which user interface element a user clicked on involves at least a partial traversal of the observer structure. Ditto for redrawing.
Since we're likely to do a traversal under any of these circumstances, traversal might be a splendid time to compute what we would have otherwise stored. As a matter of fact, traversal can provide enough context to do things that would be impossible for subjects to do unilaterally.
For example, we could update a modified subject's appearance by traversing the subject structure and redrawing the user interface in its entirety. No doubt such a simple-minded approach is less efficient than we'd like, because presumably only a small part of the user interface needs to change. Fortunately, the remedy is equally simple-minded: just have subjects maintain a "dirty bit"* that indicates whether they've changed. Their dirty bits get reset as a side-effect of traversal. Hence we can ignore all but the dirty subjects during traversal, bringing the efficiency of this approach more in line with Observer's.
Glad you asked. When we had Observer objects, each one knew how to draw its piece of the presentation. The code for presenting a particular ConcreteSubject lived in a corresponding ConcreteObserver class. The subject ended up delegating its presentation to its observer(s). It's this delegation that led to lots of additional objects and references.
Having rid ourselves of observers, we need a new place to put the presentation code for a subject. We must assume the presentation is drawn incrementally during the traversal, and we must vary the presentation according to the type of subject. The code that gets executed at each point in the traversal depends on two things: the kind of subject, and the kind of presentation. If all we have is a subject hierarchy, how do we tell the subjects apart, and how does the correct code get executed?
Ambiguities like these result from removing presentation functionality from the subject. But we don't want to go back to a lumped subject and observer, and we don't want to resort to unseemly run-time type tests if we can help it.
That's when I introduced the Visitor pattern. New functionality got implemented in separate visitor objects, obviating the need to change the base class. The key thing about visitors is that they recover type information from the objects they visit. For example, we can define a Visitor class called "Presenter" that does everything needed to present a given subject, including drawing, input handling, and so forth. Its interface might look something like this:
class Presenter {
public:
Presenter();
virtual void Visit(LoanSubject*);
virtual void Visit(AmountSubject*);
virtual void Visit(RateSubject*);
virtual void Visit(PeriodSubject*);
// Visit operations for other ConcreteSubjects
virtual void Init(Subject*);
virtual void Draw(Window*, Subject*);
virtual void Redraw(Window*, Subject*);
virtual void Handle(Event*, Subject*);
// other operations involving traversal
};
To work, the Visitor pattern requires an Accept operation
of every kind of object it can visit. They're all implemented the
same way. For example:
void LoanSubject::Accept (Presenter& p) {
p.Visit(this);
}
Thus to generate a presentation of a given subject, each stage of the
traversal calls
subject->Accept(p);
where subject is of type Subject*,
and p is an instance of
Presenter. Herein lies the magic of Visitor: the call back on the
presenter resolves statically to the correct subclass-specific
Presenter operation, effectively identifying the concrete subject to
the presenter---run-time type tests need not apply.
If you're wondering who carries out the traversal, look again at
Presenter's interface: it includes Init,
Draw, Redraw, and Handle,
operations over and above those prescribed by the Visitor pattern.
These operations carry out one or more traversals in response to a
stimulus such as a user input, a change in subject state, or any other
traversal-prompting circumstance I described earlier. These
operations give clients a simple interface for keeping the
presentation alive and up to date. Figure 6 depicts the traversal
process graphically. Compare the number of objects and links (solid
lines) to that of Figure 4. A substantial reduction, no?
Visit
operations to the Presenter class
in support of new Subject subclasses.
Back in "Visiting Rights" I introduced a catch-all Visit
operation
into the Visitor interface as a place to implement default behavior.
That would entail adding a
virtual void Visit(Subject*);
operation to the Presenter interface. If there is default behavior
that all Visit operations should implement, you can put it in
Visit(Subject*) and have the other Visit
operations call it by
default. That way you avoid reimplementing the default functionality
in each Visit operation.
But this catch-all operation offers more than just occasional reuse. It provides a trap door through which to visit unforeseen Subject subclasses.
Suppose I'm an application programmer, and I've just defined a new
RebateSubject subclass of Subject. I've dutifully defined
its Accept operation like all the others:
void RebateSubject::Accept (Presenter& p) {
p.Visit(this);
}
If you didn't understand why the catch-all operation is important,
then this should make it clear. When
RebateSubject::Accept
calls Visit with itself as an argument, the compiler must find a
corresponding operation in the Presenter interface. If there were no
Presenter::Visit(Subject*)
as a catch-all, the compiler would throw up
its hands and spit out an error message. Not so if we have a
catch-all. The compiler is smart enough to know that a RebateSubject
is a Subject, and everything's hunky-dory.
But while we've sated the compiler, we haven't accomplished much.
Presenter::Visit(Subject*) was implemented before there was a
RebateSubject class, so it can't do anything beyond the default
behavior it implements. What now?
Remember what we're trying to avoid: the need to change the Visitor (that is, Presenter) interface. Why? Because the application programmer cannot change an interface defined by the framework. But nothing prevents the programmer from subclassing Presenter. That's exactly how we'll add code for presenting RebateSubjects.
Let's define a NewPresenter subclasses. Beyond the Presenter functionality it inherits, it adds functionality for presenting RebateSubjects by overriding the catch-all operation***:
void NewPresenter::Visit (Subject* s) {
RebateSubject* rs = dynamic_cast(s);
if (rs) {
// present the RebateSubject
} else {
Presenter::Visit(s); // carry out default behavior
}
}
Now you see the dirty little secret of this approach: the run-time
type test to ensure that the subject we're visiting is in fact a
RebateSubject. If we were absolutely sure that
NewPresenter::Visit(Subject*)
could be called only by visiting a
RebateSubject, then we could replace the dynamic cast with a simple
cast. That's probably a dicey thing to do nevertheless. Besides,
you'll have to do the dynamic cast if there's more than one new
subclass of Subject to present.
Obviously this is meant to work around a drawback of the Visitor pattern. If we're constantly adding new subclasses, then the whole Visitor approach degrades into a tag-and-case-statement style of programming. But if applications define just a few new subclasses, as should be the case in a design that favors composition, then most of the benefits of the Visitor pattern are retained.
The first problem has to do with the size of our Presenter class.
Effectively, we've lumped the functionality of several
ConcreteObserver classes into this one visitor. We don't want the
result to be a huge monolith. At some point we should start
decomposing Presenter into smaller visitors or apply other patterns to
reduce its size. For example, we could apply the Strategy pattern
[Gamma+95] to let Visit
operations delegate their work to strategy
objects. Of course, the reason we went with the Visitor approach in
the first place was to reduce the number of objects we use. Putting
more objects into the visitor reduces the pattern's benefit, though
it's unlikely we'll end up with as many objects (and links) as
Observer required.
The second problem involves observer state. Nominally, the Visitor approach replaces lots of observers with one visitor. What if each observer stores its own distinct state, not all of which is computable on the fly---where does that state end up? Since we've presumed that we can compute observer state rather than store it, this shouldn't be a problem. Worse comes to worst, if the uncomputable state really varies on a per-object basis, then the visitor can keep it in a private associative store (e.g., a hash table) keyed by subject. The difference in run-time overhead between the hash table and the Observer implementation should be negligible (he says).
** Vlissides, J. Pattern Hatching, C++ Report, Sept. 1995.
*** A C++ quirk: Because I've overloaded the Visit
operations, we must override all of them in the NewPresenter
subclass to head off complaints from the compiler. To avoid this
problem, forgo overloading and embed the concrete subject's name in
the Visit operation. I discuss this problem more
fully in "Visiting Rights."
References
[Gamma+95] Gamma, E., R. Helm, R. Johnson, J. Vlissides. Design
Patterns: Elements of Reusable Object-Oriented Software,
Addison-Wesley, Reading, MA, 1995.
This page and its contents ©1996 by John Vlissides. All Rights Reserved.