This chapter presents a case study in the design of a “What-You-See-Is-What-You-Get” (or “WYSIWYG”) document editor called Lexi.[1] We’ll see how design patterns capture solutions to design problems in Lexi and applications like it. By the end of this chapter you will have gained experience with eight patterns, learning them by example.
Design Problems
- Document structure. The choice of internal representation for the document affects nearly aspect of Lexi's design. All editing, formatting, displaying, and textual analysis will require traversing the representation. The way we organize this information will impact the design of the rest of the application.
- Formatting. How does Lexi actually arrange text and graphics into lines and columns? What objects are responsible for carrying out different formatting policies? How does these polices interact with the document's internal representation?
-
Embellishing the user interface. Lexi's user interface includes scroll bars, borders, and drop shadows that embellish the WYSIWYG document interface.
-
Supporting multiple look-and-feel standards. Lexi should adapt easily to different look-and-feel standards such as Motif and Presentation Manager (PM) without major modification.
-
Supporting multiple window systems. Lexi's design should be as independent of the window system as possible.
-
User operations. Users control Lexi through various user interfaces, including buttons and pull-down menus. The functionality behind these interfaces is shattered throughout the objects in the application.
-
Spelling checking and hyphenation. How does Lexi support analytical operations such as checking for misspelled words and determining hyphenation points?
Document Structure
A document is ultimately just an arrangement of basic graphical elements such as characters, lines, polygons and other shapes. These elements capture the total information content of the document. Yet an author often vies these elements not in graphical terms but in terms of the document's physical structure — lines, columns, figures, tables and other substructures. To give Lexi's implementation similar qualities, we'll choose an internal representation that matches the document's physical structure.
In particular, the internal representation should support the following:
- Maintaining the document's physical structure, that is, the arrangement of text and graphics into lines, columns, tables, etc.
- Generating and presenting the document visually.
- Mapping positions on the display to elements in the internal representation. This lets Lexi determine what the user is referring to when he points to something in the visual representation.
In addition to these goals are some constraints. First, we should treat text and graphics uniformly. The application's interface lets the user embed text within graphics freely and vice versa. We should treating graphics as a special case of text or text as a special case of graphics; otherwise we'll end up with redundant formatting and manipulation mechanisms. One set of mechanisms should suffice for both text and graphics.
Second, our implementation should't have to distinguish between single element and groups of elements in the internal representation. …
Opposing the second constraint, however , is the need to analyze the text for such things as spelling errors and potential hyphenation points.
Recursive Composition
A common way to represent hierarchically structured information is through a technique called recursive composition, which entails building increasingly complex elements out of simpler ones. Recursive composition gives us a way to compose a document out of simple graphical elements.
We can represent this physical structure by devoting an object to each important element. That includes not just the visible laments like the characters and graphics but the invisibles structural elements as well — the lines and the column.
By using an object for each character and graphical element in the document, we promote flexibility at the finest levels of Lexi's design. We can treat text and graphics uniformly with respect to how they are drawn, formatted, and embedded with each other. We can extend Lexi to support new character sets without disturbing other functionality. Lexi's object new character sets without disturbing other functionality. Lexi's object structure mimics the document's physical structure.
This approach has two important implications. The first is obvious: The objects need corresponding classes. The second implication, which maybe less obvious, is that these classes must have compatible interfaces, because we want to treat the objects uniformly. The way to make interfaces compatible in a language like C++ is to relate the classes through inheritance.
Glyphs
We'll define a Glyph abstract class for all objects that can appear in a document structure. Its subclasses define both primitive graphical elements (like characters and images) and structural elements (like rows and columns).
Glyphs have three basic responsibilities. They know
(1) how t draw themselves,
(2) what space they occupy, and
(3) their children and parent.
Glyph subclasses redefine the Dr56aw
operation to render themselves onto a window. They are passed a reference to a Window
object in the call to Draw
. The Window class defines graphics operations for rendering text and basic shapes in a window on the screen. A Rectangle subclass of Glyph might redefine Draw
as follows:
void Rectangle::Draw (Window* w) {
w->DrawRect (_x0, _y0, _x1, _y1)
}
Where _x0
, _y0
, x_1
and _y1
are data members of Rectangle
that define two opposing corners of the rectangle. DrawRect
is the Window operation that makes the rectangle appear on the screen.
A parent glyph often needs to know how much space a child glyph occupies, for example, to arrange it and other glyphs in a line so that none overlaps. The Bounds
operation returns the rectangular area that the glyph occupies. It returns the opposite corners of the smiles rectangle that contains the glyph. Glyph subclasses redefine this operation to return the rectangular area in which they draw.
The Intersects
operation returns whether a specified point intersects the glyph. Whenever the user clicks somewhere in the document, Lexi called this operation to determine which glyph or glyph structure is under the mouse. The Rectangle class redefines this operation to compute the intersection of the rectangle and the given point.
Because glyphs can have children, we need a common interface to add, remove, and access those children. For example, a Row's children are the glyphs it arranges into a row. The Insert
operation inserts a glyph at a position specified by an integer index. The Remove
operation removes a specified glyph if it is indeed a child.
The Child
operation returns the child (if any) at the given index. Glyphs like Row that can have children should use Child
internally instead of accessing the child data structure directly. That way you won't have to modify operations like Draw
that iterate through the children when you change the data structure from, say, an array to a link list. Similarly, Parent
provides a standard interface to the glyph's parent, if any. Glyphs in Lexi store a reference to their parent, and their Parent
operation simply returned this reference.
Composite Pattern
Recursive composition is good for more than just documents. We can use it to represent any potentially complex, hierarchical structure. The Composite pattern captures the essence of recursive composition in object-oriented terms. Now would a good time to turn to that pattern and study it, referring back to this scenario as needed.
Formatting
We've settled on a way to represent the document's physical structure. Next, we need to figure out how to construct a particular physical structure, one that corresponds to a properly formatted document. Representation and formatting are distinct: The ability to capture the document's physical structure doesn't tell us how to arrive at a particular structure. This responsiblity rests mostly on Lexi. It must break text into lines, lines into columns, and so on, taking into account the user's higher-level desires.
By the way, we’ll restrict “formatting” to mean breaking a collection of glyphs into lines. In fact, we’ll use the terms “formatting” and “linebreaking” interchangeably. The techniques we’ll discuss apply equally well to breaking lines into columns and to breaking columns into pages.
Encapsulating the Formatting Algorithm
The formatting process, with all its constraints and details, isn’t easy to automate. There are many approaches to the problem, and people have come up with a variety of formatting algorithms with different strengths and weaknesses. Because Lexi is a WYSIWYG editor, an important trade-off to consider is the balance between formatting quality and formatting speed. We want generally good response from the editor without sacrificing how good the document looks. This trade-off is subject to many factors, not all of which can be ascertained at compile-time. For example, the user might tolerate slightly slower response in exchange for better formatting. That trade-off might make an entirely different formatting algorithm more appropriate than the current one. Another, more implementation-driven trade-off balances formatting speed and storage requirements: It may be possible to decrease formatting time by caching more information.
Because formatting algorithms tend to be complex, it’s also desirable to keep them well-contained or—better yet—completely independent of the document structure. Ideally we could add a new kind of Glyph subclass without regard to the formatting algorithm. Conversely, adding a new formatting algorithm shouldn’t require modifying existing glyphs.
These characteristics suggest we should design Lexi so that it’s easy to change the formatting algorithm at least at compile-time, if not at run-time as well. We can isolate the algorithm and make it easily replaceable at the same time by encapsulating it in an object. More specifically, we’ll define a separate class hierarchy for objects that encapsulate formatting algorithms. The root of the hierarchy will define an interface that supports a wide range of formatting algorithms, and each subclass will implement the interface to carry out a particular algorithm. Then we can introduce a Glyph subclass that will structure its children automatically using a given algorithm object.
Compositor and Composition
We'll define a Compositor class for objects that can encapsulate a formatting algorithm. The interface lets the compositor know what glyphs to format and when to do the formatting. The glyphs it formats are the children of a special Glyph subclass clawed Composition. A composition gets an instance of a Compositor subclass (specialized for a particular line breaking algorithm) when it is created, and it tells the compositor to Compose
its glyphs when necessary, for example, when the user changes a document. Figure 2.5 depicts the relationships between the Composition and Compositor classes.
An unformatted Composition object contains only the visible glyphs that make up the document's basic content. It doesn't contain glyphs that determine the document's physical structure, such as Row and Column. The composition is in this state just after it's created and initialized with glyphs it should format.
When the composition needs formatting, it calls its compositor's Compose
operation. The compositor in turn iterates through the composition's children and inserts new Row and Column glyphs according to its line breaking algorithm. Figure 2.6 shows the resulting object structure. Glyphs that the compositor created and inserted into the object structure appear with gray backgrounds in the figure.
Each Compositor subclass and implement a different line breaking algorithm.Each Compositor subclass can implement a different linebreaking algorithm. For example, a SimpleCompositor might do a quick pass without regard for such esoterica as the document’s “color.” Good color means having an even distribution of text and whitespace. A TeXCompositor would implement the full TEX algorithm [Knu84], which takes things like color into account in exchange for longer formatting times.
The Compositor-Composition class split ensures a strong separation between code that supports the document’s physical structure and the code for different formatting algorithms. We can add new Compositor subclasses without touching the glyph classes, and vice versa. In fact, we can change the linebreaking algorithm at run-time by adding a single SetCompositor
operation to Composition’s basic glyph interface.
Strategy Pattern
Encapsulating an algorithm in an object is the intent of the Strategy (315) pattern. The key participants in the pattern are Strategy objects (which encapsulate different algorithms) and the context in which they operate. Compositors are strategies; they encapsulate different formatting algorithms. A composition is the context for a compositor strategy.
The key to applying the Strategy pattern is designing interfaces for the strategy and its context that are general enough to support a range of algorithms. You shouldn’t have to change the strategy or context interface to support a new algorithm. In our example, the basic Glyph interface’s support for child access, insertion, and removal is general enough to let Compositor subclasses change the document’s physical structure, regardless of the algorithm they use to do it. Likewise, the Compositor interface gives compositions whatever they need to initiate formatting.
Embellishing the User Interface
We consider two embellishment in Lexi's per interface. The first adds a border around the text editing area to demarcate the page of text. The second adds scroll bars that let the user view different parts of the page. To make it easy to add and remove these embellishments (especially at run-time), we shouldn’t use inheritance to add them to the user interface. We achieve the most flexibility if other user interface objects don’t even know the embellishments are there. That will let us add and remove the embellishments without changing other classes.
Transparent Enclosure
From a programming point of view, embellishing the user interface involves extending existing code. Using inheritance to do such extension precludes rearranging embellishments at run-time, but an equally serious problem is the explosion of classes that can result from an inheritance-based approach.
We could add a border to Composition by subclassing it to yield a BorderedComposition class. Or we could add a scrolling interface in the same way to yield a ScrollableComposition. If we want both scroll bars and a border, we might produce a Bordered-ScrollableComposition, and so forth. In the extreme, we end up with a class for every possible combination of embellishments, a solution that quickly becomes unworkable as the variety of embellishments grows.
Object composition offers a potentially more workable and flexible extension mechanism. But what objects do we compose? Since we know we’re embellishing an existing glyph, we could make the embellishment itself an object (say, an instance of class Border). That gives us two candidates for composition, the glyph and the border.The next step is to decide who composes whom. We could have the border contain the glyph, which makes sense given that the border will surround the glyph on the screen. Or we could do the opposite—put the border into the glyph—but then we must make modifications to the corresponding Glyph subclass to make it aware of the border. Our first choice, composing the glyph in the border, keeps the border-drawing code entirely in the Border class, leaving other classes alone.
What does the border class look like? The fact that borders have an appearance suggests they should actually be glyphs; that is, Border should be a subclass os Glyph. But there's a more compelling reasons for doing this: Clients shouldn't care whether glyphs have borders or not. They should treat glyphs uniformly. When clients tell a plain, unbordered glyph to draw itself, it should do so without embellishment. If that glyph is composed in a border, clients shouldn’t have to treat the border containing the glyph any differently; they just tell it to draw itself as they told the plain glyph before. This implies that the Border interface matches the Glyph interface. We subclass Border from Glyph to guarantee this relationship.
All this leads us to the concept of transparent enclosure, which combines the notions of (1) single-child (or single-component) composition and (2) compatible interfaces. Clients generally can’t tell whether they’re dealing with the component or its enclosure(i.e., the child’s parent), especially if the enclosure simply delegates all its operations to its component. But the enclosure can also augment the component’s behavior by doing work of its own before and/or after delegating an operation. The enclosure can also effectively add state to the component. We’ll see how next.
Monoglyph
We can apply the concept of transparent enclosure to all glyphs that embellish other glyphs. To make this concept concrete, we’ll define a subclass of Glyph called MonoGlyph to serve as an abstract class for “embellishment glyphs,” like Border (see Figure 2.7). MonoGlyph stores a reference to a component and forwards all requests to it.
That makes MonoGlyph totally transparent to clients by default. For example, MonoGlyph implements the Draw
operation like this:
void MonoGlyph::Draw (Window* w) {
_component->Draw(w);
}
MonoGlyph subclasses reimplement at least one of these forwarding operations. Border::Draw
, for instance, first invokes the parent class operation MonoGlyph::Draw
on the component to let the component do its part—that is, draw everything but the border. Then Border::Draw
draws the border by calling a private operation called DrawBorder
, the details of which we’ll omit:
void Border::Draw (Window* w) {
MonoGlyph::Draw(w);
DrawBorder(w);
}
Another MonoGlyph subclass appears in Figure 2.7. Scroller is a MonoGlyph that draws its component in different locations based on the positions of two scroll bars, which it adds as embellishments. When Scroller draws its component, it tells the graphics system to clip to its bounds. Clipping parts of the component that are scrolled out of view keeps them from appearing on the screen.
Now we have all the pieces we need to add a border and a scrolling interface to Lexi’s text editing area. We compose the existing Composition instance in a Scroller instance to add the scrolling interface, and we compose that in a Border instance. The resulting object structure appears in Figure 2.8.
Note that we can reverse the order of composition, putting the bordered composition into the Scroller instance. In that case the border would be scrolled along with the text, which may or may not be desirable. The point is, transparent enclosure makes it easy to experiment with different alternatives, and it keeps clients free of embellishment code.
Note also how the border composes one glyph, not two or more. This is unlike compositions we’ve defined so far, in which parent objects were allowed to have arbitrarily many children. Here, putting a border around something implies that “something” is singular. We could assign a meaning to embellishing more than one object at a time, but then we’d have to mix many kinds of composition in with the notion of embellishment: row embellishment, column embellishment, and so forth. That won’t help us, since we already have classes to do those kinds of compositions. So it’s better to use existing classes for composition and add new classes to embellish the result. Keeping embellishment independent of other kinds of composition both simplifies the embellishment classes and reduces their number. It also keeps us from replicating existing composition functionality.
Decorator Pattern
The Decorator (175) pattern captures class and object relationships that support embellishment by transparent enclosure. The term “embellishment” actually has broader meaning than what we’ve considered here. In the Decorator pattern, embellishment refers to anything that adds responsibilities to an object. We can think for example of embellishing an abstract syntax tree with semantic actions, a finite state automaton with new transitions, or a network of persistent objects with attribute tags. Decorator generalizes the approach we’ve used in Lexi to make it more widely applicable.
Supporting Multiple Look-and-Feel Standards
Achieving portability across hardware and software platforms is a major problem in system design. Retargeting Lexi to a new platform shouldn’t require a major overhaul, or it wouldn’t be worth retargeting. We should make porting as easy as possible.
One obstacle to portability is the diversity of look-and-feel standards, which are intended to enforce uniformity between applications. These standards define guidelines for how applications appear and react to the user. While existing standards aren’t that different from each other, people certainly won’t confuse one for the other—Motif applications don’t look and feel exactly like their counterparts on other platforms, and vice versa. An application that runs on more than one platform must conform to the user interface style guide on each platform.
Our design goals are to make Lexi conform to multiple existing look-and-feel standards and to make it easy to add support for new standards as they (invariably) emerge. We also want our design to support the ultimate in flexibility: changing Lexi’s look and feel at run-time.
Abstracting Object Creation
Everything we see an interact with in Lexi's user interface is a glyph composed in other, invisible glyphs like Row and Column. The invisible glyphs compose visible ones like Button and Character and lay them out properly.
We'll assume we have two sets of widget glyph classes with which to implement multiple look-and-feel standards:
- A set of abstract Glyph subclasses for each category of widget glyph.
- A set of concrete subclasses for each abstract subclass that implement different look-and-feel standards.
Lexi needs a way to determine the look-and-feel standard that’s being targeted in order to create the appropriate widgets. Not only must we avoid making explicit constructor calls; we must also be able to replace an entire widget set easily. We can achieve both by abstracting the process of object creation. An example will illustrate what we mean.
Factories and Product Classes
Normally we might create an instance of a Motif scroll bar glyph with the following C++ code:
ScrollBar* sb = new MotifScrollBar;
This is the kind of code to avoid if you want to minimize Lexi’s look-and-feel dependencies. But suppose we initialize sb as follows:
ScrollBar* sb = guiFactory->CreateScrollBar();
where guiFactory
is an instance of a MotifFactory class. CreateScrollBar
returns a new instance of the proper ScrollBar subclass for the look and feel desired, Motif in this case.As far as clients are concerned, the effect is the same as calling the MotifScrollBar constructor directly. But there’s a crucial difference: There’s no longer anything in the code that mentions Motif by name. The guiFactory
object abstracts the process of creating not just Motif scroll bars but scroll bars for any look-and-feel standard. And guiFactory
isn’t limited to producing scroll bars. It can manufacture a full range of widget glyphs, including scroll bars, buttons, entry fields, menus, and so forth.
All this is possible because MotifFactory is a subclass of GUIFactory, an abstract class that defines a general interface for creating widget glyphs. It includes operations like CreateScrollBar
and CreateButton
for instantiating different kinds of widget glyphs. Subclasses of GUIFactory implement these operations to return glyphs such as MotifScrollBar and PMButton that implement a particular look and feel. Figure 2.9 shows the resulting class hierarchy for guiFactory
objects.
Moreover, the products that a factory produces are related to one another; in this case, the products are all widgets for the same look and feel.
The last question we have to answer is, Where does the GUIFactory
instance come from? The answer is, Anywhere that’s convenient. The variable guiFactory
could be a global, a static member of a well-known class, or even a local variable if the entire user interface is created within one class or function. There’s even a design pattern, Singleton (127), for managing well-known, one-of-a-kind objects like this. The important thing, though, is to initialize guiFactory
at a point in the program before it’s ever used to create widgets but after it’s clear which look and feel is desired.
If the look and feel is known at compile-time, then guiFactory
can be initialized with a simple assignment of a new factory instance at the beginning of the program:
GUIFactory* guiFactory = new MotiFactory;
If the user can specify the look and feel with a string name at startup time, then the code to create the factory might be
GUIFactory* guiFactory;
const char* styleName = getenv("LOOK_AND_FEEL"); // user or environment supplies this at startup
if (strcmp(styleName, "Motif") == 0) {
guiFactory = new MotifFactory;
} else if (strcmp(styleName, "Presentation_Manager") == 0) {
guiFactory = new PMFactory;
} else {
guiFactory = new DefaultGUIFactory;
}
There are more sophisticated ways to select the factory at run-time. For example, you could maintain a registry that maps strings to factory objects. That lets you register instances of new factory subclasses without modifying existing code, as the preceding approach requires. And you don’t have to link all platform-specific factories into the application. That’s important, because it might not be possible to link a MotifFactory on a platform that doesn’t support Motif.
But the point is that once we’ve configured the application with the right factory object, its look and feel is set from then on. If we change our minds, we can reinitialize guiFactory
with a factory for a different look and feel and then reconstruct the interface. Regardless of how and when we decide to initialize guiFactory
, we know that once we do, the application can create the appropriate look and feel without modification.
Abstract Factory Pattern
Factories and products are the key participants in the Abstract Factory pattern. This pattern captures how to create families of related product objects without instantiating classes directly. It's most appropriate when the number and general kinds of product objects stay constant and there are differences in specific product families. We choose between families by instantiating a particular concrete factory and using it consistently to create products thereafter. We can also swap entire families of products by replacing the concrete factory with an instance of a different one. The Abstract Factory pattern's emphasis on families of products distinguishes it from other creational patterns, which involve only one kind of product object.
Supporting Multiple Window Systems
Look and feel is just one of many portability issues. Another is the windowing environment in which Lexi runs. A platform’s window system creates the illusion of multiple overlapping windows on a bitmapped display. It manages screen space for windows and routes input to them from the keyboard and mouse. Several important and largely incompatible window systems exist today (e.g., Macintosh, Presentation Manager, Windows, X). We’d like Lexi to run on as many of them as possible for exactly the same reasons we support multiple look-and-feel standards.
Can We Use an Abstract Factory?
In applying the Abstract Factory pattern, we assumed we would define the concrete widget glyph classes for each look-and-feel standard. That meant we could derive each concrete product for a particular standard (e.g., MotifScrollBar and MacScrollBar) from an abstract product class (e.g., ScrollBar). But suppose we already have several class hierarchies from different vendors, one for each look-and-feel standard. Of course, it’s highly unlikely these hierarchies are compatible in any way. Hence we won’t have a common abstract product class for each kind of widget (ScrollBar, Button, Menu, etc.)—and the Abstract Factory pattern won’t work without those crucial classes. We have to make the different widget hierarchies adhere to a common set of abstract product interfaces. Only then could we declare the Create...
operations properly in our abstract factory’s interface.
We solved this problem for widgets by developing our own abstract and concrete product classes. Now we’re faced with a similar problem when we try to make Lexi work on existing window systems; namely, different window systems have incompatible programming interfaces. Things are a bit tougher this time, though, because we can’t afford to implement our own nonstandard window system.
But there’s a saving grace. Like look-and-feel standards, window system interfaces aren’t radically different from one another, because all window systems do generally the same thing. We need a uniform set of windowing abstractions that lets us take different window system implementations and slide any one of them under a common interface.
Encapsulating Implementation Dependencies
In Section 2.2 we introduced a Window class for displaying a glyph or glyph structure on the display. The Window class encapsulates the things windows tend to do across window systems:
- They provide operations for drawing basic geometric shapes.
- They can iconify and de-iconify themselves.
- They can resize themselves.
The Window class must span the functionality of windows from different window systems. Let’s consider two extreme philosophies:
Intersection of functionality. The Window class interface provides only functionality that’s common to all window systems. The problem with this approach is that our Window interface winds up being only as powerful as the least capable window system. We can’t take advantage of more advanced features even if most (but not all) window systems support them.
Union of functionality. Create an interface that incorporates the capabilities of all existing systems. The trouble here is that the resulting interface may well be huge and incoherent. Besides, we’ll have to change it (and Lexi, which depends on it) anytime a vendor revises its window system interface.
Neither extreme is a viable solution, so our design will fall somewhere between the two. The Window class will provide a convenient interface that supports the most popular windowing features. Because Lexi will deal with this class directly, the Window class must also support the things Lexi knows about, namely, glyphs. That means Window’s interface must include a basic set of graphics operations that lets glyphs draw themselves in the window. Table 2.3 gives a sampling of the operations in the Window class interface.
Window is an abstract class. Concrete subclasses of Window support the different kinds of windows that users deal with. For example, application windows, icons, and warning dialogs are all windows, but they have somewhat different behaviors. So we can define subclasses like ApplicationWindow, Icon Window, and DialogWindow to capture these differences. The resulting class hierarchy gives applications like Lexi a uniform and intuitive windowing abstraction, one that doesn’t depend on any particular vendor’s window system:
Now that we’ve defined a window interface for Lexi to work with, where does the real platform-specific window come in? If we’re not implementing our own window system, then at some point our window abstraction must be implemented in terms of what the target window system provides. So where does that implementation live?
One approach is to implement multiple versions of the Window class and its subclasses, one version for each windowing platform. We’d have to choose the version to use when we build Lexi for a given platform. But imagine the maintenance headaches we’d have keeping track of multiple classes, all named “Window” but each implemented on a different window system. Alternatively, we could create implementation-specific subclasses of each class in the Window hierarchy—and end up with another subclass explosion problem like the one we had trying to add embellishments. Both of these alternatives have another drawback: Neither gives us the flexibility to change the window system we use after we’ve compiled the program. So we’ll have to keep several different executables around as well.
Neither alternative is very appealing, but what else can we do? The same thing we did for formatting and embellishment, namely, encapsulate the concept that varies. What varies in this case is the window system implementation. If we encapsulate a window system’s functionality in an object, then we can implement our Window class and subclasses in terms of that object’s interface. Moreover, if that interface can serve all the window systems we’re interested in, then we won’t have to change Window or any of its subclasses to support different window systems. We can configure window objects to the window system we want simply by passing them the right window system-encapsulating object. We can even configure the window at run-time.
Window
and WindowImp
We’ll define a separate WindowImp
class hierarchy in which to hide different window system implementations. WindowImp
is an abstract class for objects that encapsulate window system-dependent code. To make Lexi work on a particular window system, we configure each window object with an instance of a WindowImp
subclass for that system. The following diagram shows the relationship between the Window
and WindowImp
hierarchies:
By hiding the implementations in WindowImp
classes, we avoid polluting the Window
classes with window system dependencies, which keeps the Window
class hierarchy comparatively small and stable. Meanwhile we can easily extend the implementation hierarchy to support new window systems.
WindowImp
subclasses
Subclasses of WindowImp
convert requests into window system-specific operations. Consider the example we used in Section 2.2. We defined the Rectangle::Draw
in terms of the DrawRect
operation on the Window instance:
void Rectangle::Draw (Window* w) {
w->DrawRect(_x0, _y0, _x1, _y1);
}
The default implementation of DrawRect uses the abstract operation for drawing rectangles declared by WindowImp
:
void Window::DrawRect (
Coord x0, Coord y0, Coord x1, Coord y1
) {
_imp->DeviceRect(x0, y0, x1, y1);
}
where _imp
is a member variable of Window that stores the WindowImp
with which the Window is configured. The window implementation is defined by the instance of the WindowImp
subclass that _imp
points to. For an XWindowImp
(that is, a WindowImp subclass for the X Window System), the DeviceRect’s implementation might look like
void XWindowImp::DeviceRect (
Coord x0, Coord y0, Coord x1, Coord y1
) {
int x = round(min(x0, x1));
int y = round(min(y0, y1));
int w = round(abs(x0 - x1));
int h = round(abs(y0 - y1));
XDrawRectangle(_dpy, _winid, _gc, x, y, w, h);
}
DeviceRect
is defined like this because XDrawRectangle
(the X interface for drawing a rectangle) defines a rectangle in terms of its lower left corner, its width, and its height. DeviceRect must compute these values from those supplied. First it ascertains the lower left corner (since (x0, y0) might be any one of the rectangle’s four corners) and then calculates the width and height.
PMWindowImp
(a subclass of WindowImp for Presentation Manager) would define DeviceRect
differently:
void PMWindowImp::DeviceRect (
Coord x0, Coord y0, Coord x1, Coord y1
) {
Coord left = min(x0, x1);
Coord right = max(x0, x1);
Coord bottom = min(y0, y1);
Coord top = max(y0, y1);
PPOINTL point[4];
point[0].x = left; point[0].y = top;
point[1].x = right; point[1].y = top;
point[2].x = right; point[2].y = bottom;
point[3].x = left; point[3].y = bottom;
if (
(GpiBeginPath(_hps, 1L) == false) ||
(GpiSetCurrentPosition(_hps, &point[3]) == false) ||
(GpiPolyLine(_hps, 4L, point) == GPI_ERROR) ||
(GpiEndPath(_hps) == false)
) {
// report error
} else {
GpiStrokePath(_hps, 1L, 0L);
}
}
Why is this so different from the X version? Well, PM doesn’t have an operation for drawing rectangles explicitly as X does. Instead, PM has a more general interface for specifying vertices of multisegment shapes (called a path) and for outlining or filling the area they enclose.
PM’s implementation of DeviceRect is obviously quite different from X’s, but that doesn’t matter. WindowImp hides variations in window system interfaces behind a potentially large but stable interface. That lets Window subclass writers focus on the window abstraction and not on window system details. It also lets us add support for new window systems without disturbing the Window classes.
Configure Window
s with WindowImp
s
A key issue we haven’t addressed is how a window gets configured with the proper WindowImp subclass in the first place. Stated another way, when does _imp get initialized, and who knows what window system (and consequently which WindowImp subclass) is in use? The window will need some kind of WindowImp before it can do anything interesting.
There are several possibilities, but we’ll focus on one that uses the Abstract Factory (87) pattern. We can define an abstract factory class WindowSystemFactory that provides an interface for creating different kinds of window system-dependent implementation objects:
class WindowSystemFactory {
public:
virtual WindowImp* CreateWindowImp() = 0;
virtual ColorImp* CreateColorImp() = 0;
virtual FontImp* CreateFontImp() = 0;
// a "Create..." operation for all window system resources
};
Now we can define a concrete factory for each window system:
class PMWindowSystemFactory : public WindowSystemFactory {
virtual WindowImp* CreateWindowImp()
{ return new PMWindowImp; }
// ...
};
class XWindowSystemFactory : public WindowSystemFactory {
virtual WindowImp* CreateWindowImp()
{ return new XWindowImp; }
// ...
};
The Window base class constructor can use the WindowSystemFactory
interface to initialize the _imp
member with the WindowImp
that's right for the window system:
Window::Window () {
_imp = windowSystemFactory->CreateWindowImp();
}
The windowSystemFactory
variable is well-known instance of a WindowSystemFactory
subclass, akin to the well-known guiFactory
variable defining the look and feel. The windowSystemFactory
variable can be initialized in the same way.
Bridge Pattern
The WindowImp
class defines an interface to common window system facilities, but its design is driven by different constraints than Window
's interface. Application programmers won't deal with WindowImp
's interface directly; they only deal with Window objects. So WindowImp's interface needn't match the application programmer's view of the world, as was our concern in the design of the Window class hierarchy and interface.
The important thing to realize is that Window's interface caters to the applications programmer, while WindowImp caters to window systems.
The relationship between Window
and WindowImp
is an example of the Bridge pattern. The intent behind Bridge is to allow separate class hierarchies to work together even as they evolve independently. Our design criteria led us to create two separate class hierarchies, one that supports the logical notion of windows, and another for capturing different implementations of windows. The Bridge pattern lets us maintain and enhance our logical windowing abstractions without touching window system-dependent code, and vice versa.
Using Operations
Some of Lexi's functionality is available through the document's WYSIWYG representation.
Lexi provides different user interfaces for these operations.
Furthermore, these operations are implemented in many different classes.
To further complicate matters, we want Lexi to support undo and redo of most but not all its functionality.
Encapsulating a Request
Let's assume that these work-performing glyphs are instances of a Glyph subclass called MenuItem
that they do their work in response to a request from a client. Carrying out the request might involve an operation on one object, or many operations on many objects, or something in between.
We don't need a subclass of MenuItem for each request any more than we need a subclass for each text string in a pull-down menu.
To illustrate, suppose you could advance to the last page in the document both through a MenuItem in a pull-down menu and by pressing a page icon at the bottom of Lexi's interface.
What's missing is a mechanism that lets us parameterize menu items by the request they should fulfill. That way we avoid a proliferation of subclasses and allow for greater flexibility at run-time.
We could parameterize MenuItem with a function to call, but that's not a complete solution for at least three reasons:
- It doesn't address the undo/redo problem.
- It's hard to associate state with a function.
- Functions are hard to extend, and it's hard to reuse parts of them.
These reasons suggest that we should parameterize MenuItems with an object, not a function. Then we can use inheritance to extend and reuse the request's implementation. We also have a place to store state and implement undo/redo functionality.
Command Class and Subclasses
First, we define a Command
abstract class to provide an interface for issuing a request. The basic interface consists of a single abstract operation called "Execute". Subclasses of Command
implement Execute
in different ways to fulfill different requests. Some subclasses may delegate part of all of the work to other objects. To the requester, however, a Command object is a Command object — they are treated uniformly.
Now MenuItem
can store a Command
object that encapsulates a request. We give each menu item object an instance of the Command
subclass that's suitable for that menu item, just as we specify the text to appear in the menu item. When a user chooses a particular menu item, the MenuItem simply calls Execute
on its Command
object to carry out the request. Note that buttons and other widgets can use commands in the same way menu items do.
Undoability
Undo/redo is an important capability in interactive applications. To undo and redo commands, we add an Unexecute
operations to Command's interface.
Sometimes undoability must be determined at run-time.
So to determine if a command is undoable, we add an abstract Reversible operation to the Command interface. Reversible returns a Boolean value. Subclasses can redefine this operation to return true or false based on run-time criteria.
Command History
The final step in supporting arbitrary-level undo and redo is to define a command history, or list of commands that have been executed (or unexecuted, if some commands have been undone).
To undo the last command, we simply call Unexecute on the most recent command:
The number of levels is limited only by the length of the command history.
To redo a command that's just been undone, we do the same thing in reverse. Commands to the right of the present line are commands that may be redone in the future. To redo the last undone command, we call Execute on the command to the right of the present line:
Then we advance the present line so that a subsequent redo will call redo on the following command in the future.
Command Pattern
Lexi's commands are an application of the Command pattern, which describes how to encapsulate a request. The Command pattern prescribes a uniform interface for issuing requests that lets you configure clients to handle different requests. The interface shields clients from the request's implementation. This is perfect for applications like Lexi that must provide centralized access to functionality scattered throughout the application. The pattern also discusses undo and redo mechanisms built on the basic Command interface.
Spelling Checking and Hyphenation
The last design problem involves textual analysis, specifically checking for misspelling and introducing hyphenation points where needed for good formatting.
The constraints here are similar to those we had for the formatting design problem in Section 2.3.
We also want to avoid wiring this functionality into the document structure. This goal is even more important here than it was in the formatting case, because spelling checking and hyphenation are just two of potentially many kinds of analyses we may want Lexi to support.
There are actually two pieces to this puzzle: (1) accessing the information to be analyzed, which we have scattered over the glyphs in the document structure, and (2) doing the analysis.
Accessing Scattered Information
Many kinds of analysis requiring examining the text character by character.
Encapsulating Access and Traversal
Right now our glyph interface uses an integer index to let clients refer to children. Although that might be reasonable for glyph classes that store their children in an array, it may be inefficient for glyphs that use a linked list.
An important role of the glyph abstraction is to hide the data structure in which children are stored. That way we can change the data structure a glyph class uses without affecting other classes.
Therefore only the glyph can know the data structure it uses. A corollary is that the glyph interface shouldn't be biased toward one data structure or another. It should be better suited to arrays than to linked lists, for example, as it is now.
We can solve this problem and support several different kinds of traversals at the same time. We can put multiple access and traversal capabilities directly in the glyph classes and provide a way to choose among them, perhaps by supplying an enumerated constant as a parameter.
We might add the following abstract operations to Glyph's interface to support this approach:
void First(Traversal kind)
void Next()
bool IsDone()
Glyph* GetCurrent()
void Insert(Glyph*)
Operations First
, Next
, and IsDone
control the traversal.
An analysis would use the following C++ code to do a preorder traversal of a glyph structure rooted at g
:
Glyph* g;
for (g->First(PREORDER); !g->IsDone(); g->Next()) {
Glyph* current = g->GetCurrent();
// do some analysis
}
But this approach still has problems. For one thing, it can't support new traversals without either extending the set of enumerated values or adding new operations. Say we wanted to have a variation on preorder traversal that automatically skips non-textual glyphs. We'd have to change the Traversal
enumeration to include something like TEXTUAL_PREORDER
.
We'd like to avoid changing existing declarations.
Once again, a better solution is to encapsulate the concept that varies, in this case the access and traversal mechanisms. We can introduce a class of objects called iterators whose sole purpose is to define different sets of these mechanisms. We can use inheritance to let us access different data structures uniformly and support new kinds of traversals as well.
Iterator Class and Subclasses
We'll use an abstract class called Iterator
to define a general interface for access and traversal.
- Concrete subclasses like
ArrayIterator
andListIterator
implement the interface to provide access to arrays and lists, whilePreorderIterator
,PostorderIterator
, and the like implement different traversals on specific structure. - Each iterator subclass has a reference to the structure it traverses. Subclass instances are initialized with this reference when they are created.
Now we can access the children of a glyph structure without knowing its representation:
Glyph* g;
Iterator<Glyph*>* i = g->CreateIterator();
for (i->First(); i->isDone(); i->Next()) {
Glyph* child = i->CurrentItem();
// do something with current child
}
CreateIterator
returns a NullIterator instance by default. A NullIterator
is a degenerate iterator for glyphs that have no children, that is, leaf glyphs. NullIterator
's IsDone
operation always return true.
A glyph subclass that has children will override CreateIterator to return an instance of a different Iterator subclass. Which subclass depends on the structure that stores the children.
Iterator<Glyph*>* Row::CreateIterator () {
return new ListIterator<Glyph*>(_children);
}
CurrentItem
would simply call CurrentItem
on the iterator at the top on the stack:
Glyph* PreorderIterator::CurrentItem () const {
return
_iterators.Size() > 0 ?
_iterators.Top()->CurrentItem() : 0;
}
The Next
operation gets the top iterator on the stack and asks its current item to create an iterator, in an effort to descent the glyph structure as far as possible.
void PreorderIterator::Next () {
Iterator<Glyph*>* i = _iterators.Top()->CurrentItem()->CreateIterator();
i->First();
_iterators.Push(i);
while (
_iterators.Size() > 0 && _iterators.Top()->isDone()
) {
delete _iterators.Pop();
_iterators.Top()->Next();
}
}
Notice how the Iterator
class hierarchy lets us add new kinds of traversals without modifying glyph classes — we simply subclass Iterator
and add a new traversal as we have with PreorderIterator
.
Glyph
subclasses use the same interface to give clients access to their children without revealing the underlying data structure they use to store them.
Iterator Pattern
The Iterator
pattern captures these techniques for supporting access and traversal over object structures. It's applicable not only to composite structures but to collections as well. It abstracts the traversal algorithm and shields clients from the internal structure of the objects they traverse.
The Iterator
pattern illustrates once more how encapsulating this concept that varies helps us gain flexibility and reusability. Even so, the problem of iteration has surprising depth, and the Iterator pattern covers many more nuances and trade-offs than we've considered here.
Traversal versus Traversal Actions
First we have to decide where to put the responsibility for analysis. An obvious approach is to put the analytical capability into the glyph classes themselves.
Encapsulating the Analysis
We could use an instance of this class in conjunction with an appropriate iterator.
The fundamental question with this approach is how the analysis object distinguishes different kinds of glyphs without resorting to type tests or downcasts. We don't want a SpellingChecker
class to include code like
void SpellingCheck::check (Glyph* glyph) {
character* c;
Row* r;
Image* i;
if (c = dynamic_cast<Character*>(glyph) {
// analyze the character
} else if (r = dynamic_cast<Row*>(glyph)) {
// prepare to analyze r's children
} else if (i = dynamic_cast<Image*>(glyph)) {
// do nothing
}
}
This code is pretty ugly. It relies on fairly esoteric capabilities like-safe casts.
We want to avoid such a brute-force approach, but how? Let's consider what happens when we add the following abstract operation to the Glyph class:
void CheckMe(SpellingChecker&)
We define CheckMe
in every Glyph subclass as follows:
void GlyphSubclass::checkMe (Spellingchecker& checker) {
checker.checkGlyphSubclass(this);
}
where GlyphSubclass
would be replaced by the name of the glyph subclass. Note that when CheckMe
is called, the specific Glyph subclass is known — after all, we're in one of its operations. In turn, the SpellingChecker
class interface includes an operation like CheckGlyphSubclass
for every Glyph subclass:
class SpellingChecker {
public:
SpellingChecker();
virtual void CheckCharacter(Character*);
virtual void CheckRow(Row*);
virtual void Checklmage(Image*);
// ... and so forth
List<char*>& GetMisspellings();
protected:
virtual bool IsMisspelled(const char*);
private:
char _currentWord[MAX_WORD_SIZE];
List<char*> _misspellings;
};
SpellingChecker’s checking operation for Character glyphs might look something like this:
void SpellingChecker::CheckCharacter (Character* c) {
const char ch = c->GetCharCode();
if (isalpha(ch)) {
// append alphabetic character to _currentWord
} else {
// we hit a nonalphabetic character
if (IsMisspelled(_currentWord)) {
// add _currentWord to _misspellings
_misspellings.Append(strdup(_currentWord));
}
_currentWord[0] = '\0';
// reset _currentWord to check next word
}
}
Notice we’ve defined a special GetCharCode
operation on just the Character
class. The spelling checker can deal with subclass-specific operations without resorting to type tests or casts—it lets us treat objects specially.
CheckCharacter
accumulates alphabetic characters into the _currentWord
buffer. When it encounters a nonalphabetic character, such as an underscore, it uses the IsMisspelled operation to check the spelling of the word in _currentWord
. If the word is misspelled, then CheckCharacter
adds the word to the list of misspelled words. Then it must clear out the _currentWord
buffer to ready it for the next word. When the traversal is over, you can retrieve the list of misspelled words with the GetMisspellings
operation.
Now we can traverse the glyph structure, calling CheckMe
on each glyph with the spelling checker as an argument. This effectively identifies each glyph to the SpellingChecker
and prompts the checker to do the next increment in the spelling check.
SpellingChecker SpellingChecker;
Composition* c;
// ...
Glyph* g;
PreorderIterator i(c);
for (i.First(); !i.IsDone(); i.Next()) {
g = i.CurrentItem();
g->CheckMe(spellingChecker);
}
The following interaction diagram illustrates how Character
glyphs and the SpellingChecker
object work together:
This approach works for finding spelling errors, but how does it help us support multiple kinds of analysis? It looks like we have to add an operation like CheckMe(SpellingChecker&) to Glyph and its subclasses whenever we add a new kind of analysis. That’s true if we insist on an independent class for every analysis. But there’s no reason why we can’t give all analysis classes the same interface. Doing so lets us use them polymorphically. That means we can replace analysis-specific operations like CheckMe(SpellingChecker&)
with an analysis-independent operation that takes a more general parameter.
Visitor Class and Subclasses
We’ll use the term visitor to refer generally to classes of objects that “visit” other objects during a traversal and do something appropriate. In this case we can define a Visitor
class that defines an abstract interface for visiting glyphs in a structure.
class Visitor {
public:
virtual void VisitCharacter(Character*) { }
virtual void VisitRow(Row*) { }
virtual void VisitImage(Image*) { }
// ... and so forth
};
Concrete subclasses of Visitor
perform different analyses. For example, we could have a SpellingCheckingVisitor
subclass for checking spelling, and a HyphenationVisitor
subclass for hyphenation. SpellingCheckingVisitor
would be implemented exactly as we implemented SpellingChecker
above, except the operation names would reflect the more general Visitor
interface. For example, CheckCharacter would be called VisitCharacter
.
Since CheckMe
isn’t appropriate for visitors that don’t check anything, we’ll give it a more general name: Accept
. Its argument must also change to take a Visitor&
, reflecting the fact that it can accept any visitor. Now adding a new analysis requires just defining a new subclass of visitor—we don’t have to touch any of the glyph classes. We support all future analyses by adding this one operation to Glyph
and its subclasses.
We’ve already seen how spelling checking works. We use a similar approach in HyphenationVisitor to accumulate text. But once HyphenationVisitor’s VisitCharacter operation has assembled an entire word, it works a little differently. Instead of checking the word for misspelling, it applies a hyphenation algorithm to determine the potential hyphenation points in the word, if any. Then at each hyphenation point, it inserts a discretionary glyph into the composition. Discretionary glyphs are instances of Discretionary, a subclass of Glyph.
A discretionary glyph has one of two possible appearances depending on whether or not it is the last character on a line. If it’s the last character, then the discretionary looks like a hyphen; if it’s not at the end of a line, then the discretionary has no appearance whatsoever. The discretionary checks its parent (a Row object) to see if it is the last child. The discretionary makes this check whenever it’s called on to draw itself or calculate its boundaries. The formatting strategy treats discretionaries the same as whitespace, making them candidates for ending a line. The following diagram shows how an embedded discretionary can appear.
Visitor Pattern
What we’ve described here is an application of the Visitor (331) pattern. The Visitor class and its subclasses described earlier are the key participants in the pattern. The Visitor pattern captures the technique we’ve used to allow an open-ended number of analyses of glyph structures without having to change the glyph classes themselves. Another nice feature of visitors is that they can be applied not just to composites like our glyph structures but to any object structure. That includes sets, lists, even directed-acyclic graphs. Furthermore, the classes that a visitor can visit needn’t be related to each other through a common parent class. That means visitors can work across class hierarchies.
An important question to ask yourself before applying the Visitor pattern is, Which class hierarchies change most often? The pattern is most suitable when you want to be able to do a variety of different things to objects that have a stable class structure. Adding a new kind of visitor requires no change to that class structure, which is especially important when the class structure is large. But whenever you add a subclass to the structure, you’ll also have to update all your visitor interfaces to include a Visit… operation for that subclass. In our example that means adding a new Glyph subclass called Foo will require changing Visitor and all its subclasses to include a VisitFoo operation. But given our design constraints, we’re much more likely to add a new kind of analysis to Lexi than a new kind of Glyph. So the Visitor pattern is well-suited to our needs.