The Trouble with Observer

文章探讨了使用观察者模式在业务应用框架中的问题,特别是针对大量对象和链接带来的冗余问题,并提出了一种基于访问者模式的替代方案。

Pattern Hatching

The Trouble with Observer

John Vlissides

C++ Report, September 1996

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.

THE PROBLEM AND THE STANDARD SOLUTION

Suppose you're building a framework for business applications. These applications manipulate primitive data such as dollar amounts, names, addresses, percentages, and the like. They present this data through one or more user interfaces: fixed text for unchanging alphanumeric data; a text entry field for editable data; buttons, sliders, or pop-up menus for more constrained inputs; visual presentations such as pie charts, bar graphs, and plots of different sorts---you get the idea.

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:

Each piece of data is encapsulated in a subject object (corresponding to a model in MVC).

 

Each distinct user interface to a subject is encapsulated in an observer object (corresponding to a view in MVC).

 

A subject can have multiple observers at once.

 

Whenever the subject changes, it notifies its observer(s) of the change.

 

In turn, the observers query their subject for information that impacts their appearance; then they update themselves accordingly.

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.



Figure 1

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.



Figure 2





Figure 3

This design has several nice properties:

You can define, modify, and extend subjects independently of observers and vice versa---a boon for maintenance and enhancement.

 

Your application can include only the functionality it needs. This is particularly important when the framework offers lots of functionality. If for example an application doesn't need to present application data graphically, then it needn't include observers that present pie charts or bar graphs.

 

You can attach any number of observers to the same subject. The text entry field and the up-down buttons would likely be implemented as separate observers. (I didn't show that in Figure 3 to keep the diagram simple.) You can even have nonvisual observers. For example, you can define an object that logs changes to a subject's data without modifying the subject's implementation.

 

You can implement new subjects and observers in terms of existing ones, promoting reuse. Sounds wonderful, and it is. But alas, there is a dark side....

THE NEW AND IMPROVED PROBLEM

Figure 4 shows another way to look at these object compositions: as containment hierarchies. The loan subject contains its primitive constituents, and the observer subjects contain the corresponding primitive observers. Note the abundance of objects (labeled ovals) and object references (lines). There are links between not only the loan subject and observer but also between each primitive subject and its observer. In short, the Observer pattern has produced a lot of redundancy at run-time. If you were coding the loan subject and observer from scratch rather than reusing existing primitives, you could easily eliminate most of these links, not to mention many of the objects themselves. They're all included in the price we pay for reuse.



Figure 4

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

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?

STORE OR COMPUTE?

A major cost of the subject-observer split is the memory overhead of collateral objects and links. Parallel object hierarchies require double the number of intra-hierarchy links in addition to links between the hierarchies. That's a lot of information to keep lying around all the time. In fact, we should think hard about how necessary all this is. Do we really access the information so very often? It's one thing to separate user interface from application data, but must we constantly maintain scads of links between the two?

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:

When the subject changes.

 

When there's user input.

 

When (part of) the user interface must be (re)drawn. These circumstances constitute the times observer code would execute. If we eliminate the observer objects, then these are the times we'll have to figure out what to do on the fly.

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.

SUBJECT-SPECIFIC PRESENTATION

"Hey, how the heck do we know what to do at each step in the traversal, and where is that knowledge implemented?"

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.

VISITOR REVISITED

We've had a problem like this before, a year ago to be exact. I described it in my September '95 column "Visiting Rights"**, when we were in the thick of designing a file system. There were lots of different things we wanted file system objects (e.g., files and directories) to be able to do, but we didn't want to keep adding operations to Node, the abstract base class for file system objects. Each new operation would require surgery to existing code, heightening the risk of KSS (Kitchen-Sink Syndrome) in the Node interface.

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?



Figure 6

VISITOR'S ACHILLES' HEEL

Visitor aficionados know all too well how the pattern falls down when the class structure you visit isn't stable. This bodes ill for our business application framework. Ideally, our repertoire of Subject subclasses would be comprehensive enough that programmers would never have to define their own subclasses. But our world is far from ideal, and it must be possible (if not easy) to define presentations for new Subject subclasses without changing the framework. Specifically, we don't want to have to add new 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.

OTHER PROBLEMS

There are still some problems with this Visitor-based alternative to Observer. I'll have to address them briefly for now, since I've rambled on long enough this month. But I'm sure I'll have occasion to revisit (defend?) this approach in the near future.

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

PUTTING ONE'S MONEY WHERE ONE'S MOUTH IS

Okay, maybe all this sounds a little hare-brained, and I'm not promising it isn't. The nice thing about columns is that they don't have to compile and run and ultimately put bread on the table. But if there's even a germ of a useful idea here, please let me know. If it turns out there isn't---or worse---well, that's what disclaimers are for!

Acknowledgments

Many thanks this month to Bard Bloom, Jim Coplien, Wim De Pauw, Erich Gamma, Ralph Johnson, and John Lakos for their helpful comments and insights.

Footnotes

* The implementation should be hidden behind a set/get interface, so the actual amount of storage may vary.

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

posted on 2006-04-06 23:29 horily 阅读( ...) 评论( ...) 编辑 收藏

转载于:https://www.cnblogs.com/horily/archive/2006/04/06/368816.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值