PATTERNS AND SOFTWARE DESIGN

Designing Objects for Extension

Richard Helm and Erich Gamma

Richard and Erich are coauthors of Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994). They can be reached at Richard .Helm@dmr.ca and Erich_Gamma@Taligent.com, respectively.


The key to creating reusable software lies in knowing people's needs and anticipating how those people might reuse your software to meet those needs. To accomplish this, you must consider how your system might change over its lifetime and be aware of typical causes of redesign. Among the many causes for changes are the evolving requirements of current users and the needs of new users. Other causes might be intrinsic to the business environment in which your software is used, or stem from changes in technology or platforms.

If places where such modifications occur are not isolated or decoupled from the rest of the system, the resulting changes will cause modifications throughout the software and risk the introduction of reuse errors. The design issue then is to find and separate the potentially changing parts of an application from its more stable parts. This idea is not new, and analogies can be found in other domains--building and architecture, for instance.

In his wonderful and thoughtful book How Buildings Learn (Viking, 1994), Stewart Brand considers buildings as consisting of six layers: site (its physical space on ground), structure (exterior and load-bearing walls), skin (exterior brickwork, cladding), services (electrical, plumbing), space plan (interior walls, windows, ceilings), and stuff (furniture). Brand notes that these layers evolve at different rates during the life of a building. Sites of buildings often exist for hundreds of years, whereas the furniture tends to move frequently. Brand also notes that these layers shear against each other as they change at different rates. The slow-moving site and structure systems dominate the fast-moving space-plan and stuff systems. He observes that "an adaptive building has to allow slippage between the differently paced systems. If not, the slow systems block the fast ones, and the fast ones tear up the slow." If you lay your electrical and plumbing services within the concrete slab of your house, you'll have trouble adding new wiring or fixing blocked drains.

The same ideas apply to software. There is typically a relatively stable, core application architecture (event driven, client/ server, blackboard, or whatever), around which exist less stable layers, from slow-moving subsystem structures, to fast-moving details of the user interface. Just as in buildings, the key to developing an extensible software system is for these layers to be able to slip and shear against each other. When they cannot, you often have a reuse error. There are multiple causes of reuse errors:

Algorithmic dependencies. Algorithms are often extended, optimized, and replaced during development and reuse. Objects that depend on an algorithm (both its behavior and data structures) will have to change when the algorithm changes. Therefore, algorithms likely to change should be isolated from the rest of the application.

Tight coupling. Classes that are tightly coupled are hard to reuse in isolation, since they depend on each other. Tight coupling leads to monolithic systems, where you can't change or remove a class without understanding and changing many other classes. The system becomes a dense, brittle mass that's hard to learn, port, and maintain. Techniques such as abstract coupling and layering help create loosely coupled systems. Abstract coupling means that an object talks to another object by using an interface defined by an abstract class. This enables the object to communicate with objects of different concrete subclasses.

Creating an object by specifying a class explicitly. Specifying a class name when you create an object commits you to a particular implementation instead of a particular interface. This commitment can complicate future changes if the implementations change. One way to avoid such commitment is to make requests to instantiate classes through a third-party factory object.

Dependence on specific operations. When you specify a particular operation in a request, you commit to a particular way of satisfying that request. By avoiding hard-coded requests for operations, you make it easier to change the way a request gets satisfied.

Design patterns can help you avoid many reuse errors by ensuring that a system can change in specific ways. Each design pattern lets you vary some aspect of system structure independently of other aspects, thereby making a system more robust to a particular kind of change. There are patterns which concern the extensibility of objects, the flexible creation of objects, the distribution of responsibilities, and patterns in managing relationships between objects. We will look at some of these patterns in later columns. This month we will look at patterns for designing extensible objects.

Creating Extensible Objects

Since an object is defined by its class, extensions to the object are usually defined through class inheritance--by creating subclasses of the original class. Inheritance is a compile-time mechanism for extending a class and reusing implementations. While simple and supported in most object-oriented languages, class inheritance does have some problems:

As we discussed in our previous column, object composition offers an alternative to inheritance. Instead of composing classes to create new functionality, object composition creates new functionality by combining objects in new ways.

However, object composition is a little more difficult to use than inheritance. It requires careful attention to interfaces defined by objects. It also increases the difficulty in understanding a system: Relationships between objects are more implicit, most of the code only uses objects through interfaces, and the implementation classes of the objects behind these interfaces are unknown. Despite these difficulties, object composition can offer a more flexible alternative to inheritance as a reuse mechanism.

So how do you use object composition to create extensible objects? First, you need to consider what it is you are extending: an object's behavior for particular states, the algorithms it uses, properties and individual operations, or its interface. There are four patterns that address these extensions, three of which--Decorator, Strategy, and State--are discussed in our book Design Patterns: Elements of Reusable Object-Oriented Software. The fourth is a new pattern called "Extension Objects," which we introduce in this column. To simplify the discussion and permit comparison, we will name the class to be extended ExtendedObject.

Extending Algorithms

Objects employ algorithms to implement their operations. The Strategy pattern, which we discussed in our previous column, allows an object to be extended with new kinds of algorithms. The basic idea behind the Strategy pattern is that each algorithm is encapsulated and accessed through a common interface to create a family of strategies. At run time, the ExtendedObject instance is configured with a particular Strategy class.

Extending State-Specific Behavior

All objects have internal state. Sometimes the behavior of an object depends on this state. At issue is how to represent this state-specific behavior, isolate each state's behavior, and permit new states and behaviors to be added. The State pattern does this as follows: It encapsulates all state-specific behavior as a state object and creates state objects for each distinct state. The ExtendedObject forwards state-specific behavior to the state object. To change the ExtendedObject's behavior, configure it with a different state object.

Extending Properties and Operations

How can you extend properties and operations? How can you add state and alter the behavior of operations? The Decorator pattern does this by "wrapping" up the ExtendedObject in a decorator object. The decorator presents the same interface to clients as the decorated object (clients will therefore not notice the presence of the decorator object). Most requests made of the decorator are forwarded directly to the decorated object. However, for some requests the decorator adds, removes, or modifies the behavior of the decorated object.

Extending Interface

All objects present interfaces to clients to allow manipulation. However, it is difficult to anticipate how clients will want to use an object, and so it is hard to create a general-purpose interface for all possible clients. Attempts to do so will result in large, clumsy, bloated interfaces which tend to detract from the abstraction the object represents.

The Extension Objects pattern describes a solution to this problem of extending interfaces. There are many different ways to write patterns. This one is based on the format we introduced in our book.

Extension Objects

Category: Object Structural

Intent. Enable clients to extend the interface of an object. Additional interfaces are defined by extension objects.

Motivation. Consider a compound- document architecture as currently promoted by OLE 2, OpenDoc, and soon Taligent's CommonPoint. A compound document is made of components that are managed and arranged by container components. The infrastructure for a compound document requires a common interface to various components such as text, graphics, or spreadsheets. Let's assume that this interface is defined by an abstract class Component. How can we define additional interfaces for components that allow clients to use component-specific functionality?

You cannot define all possible operations on components as part of the general Component abstraction. First, it is not possible to foresee all operations that component writers would like to perform. Second, even if we could, the result would be a bloated interface reflecting all possible uses of components. For example, a spell checker requires a specific interface to enumerate the words of a text component. The spell checker should operate on any component that provides this interface, independent of its concrete type.

One solution is to provide a mechanism allowing clients to define additional interface extensions and to let clients query whether an object provides a certain extension. There is a spectrum of techniques for how this mechanism can be implemented.

The key idea is to define extensions to the interface as separate objects. The extension object implements this extension interface, knows about the object it extends, and implements the extension interface in terms of the extended object's interface. Extensions themselves aren't useful--there needs to be an interface that defines which extension is provided by Component. For our purpose, the extensions will be identified by a name. To avoid conflicts, this name should be registered at a central place. A client can query whether a component provides a certain extension by calling the GetExtension(extensionName) operation. If the component provides an extension with the given name, it returns a corresponding extension object. All extensions are derived from an abstract Extension class which provides only a minimal interface used to manage the extension itself. For example, it can provide an operation that a Component can call to notify its Extensions when it is about to be deleted.

In the case of the spell checker, we define an extension named "TextAccessor." The corresponding interface is defined by the abstract class TextAccessor. Its key operations are GetNextWord(), which returns the next word in the text, and ReplaceCurrentWord(), to replace a misspelled word. Let's assume that there are two different implementations of text components: SimpleTextComponent and FancyTextComponent. Both components want to provide spell-checking support. To do so, both derive their own TextAccessor subclass that implements the interface for their text implementation. The SimpleTextComponent and FancyTextComponent classes implement GetExtension("TextAccessor") to return their specific extension object.

Figure 1 summarizes the class relationships using OMTnotation (abstract operations and abstract classes are shown in italics). The implementation of SimpleText Accessor::GetExtension(_) is shown in Example 1. The extension object stores a reference back to the extended object. It typically implements the extension by forwarding requests to the extended object.

Based on this extension infrastructure, a spell checker for a compound document is implemented as follows: Traverse the components in the document. Ask each component for its TextAccessor extension. If the component returns a corresponding TextAccessor extension object, use it (after down casting it to TextAccessor) to spell check the component. Otherwise, skip the component and move on to the next.

Applicability. You use the Extension Objects pattern when:

Structure. The structure for the Extension Objects pattern is shown in Figure 2.

Participants. The participants for the Extension Objects patterns are:

Collaborations. The client negotiates with an ExtendedObject for a named extension. If it exists, it is returned. The client subsequently uses the extension to access additional behavior of the ExtendedObject.

Consequences. The Extension Object pattern has several consequences:

Implementation. The implementation must define how the extension objects are managed by ExtendedObject. A simple solution is to store an extension object in an instance variable that is returned to clients when the extension is requested. An alternative is to dynamically allocate the extension on demand when it is requested. A further variation is to provide support for clients to attach Extensions to existing objects. In this case, the ExtendedObject has to maintain a dictionary that maps an attached extension to its name.

In C++, the extension returned from GetExtension has to be cast to its corresponding extension class. If the C++ implementation provides run-time type identification, this can be achieved with dynamic_cast.

To permit the extension to have full access to the Extended object, it can be declared as its friend.

Strings are a primitive way to identify extensions. Better solutions are to use special interface identifiers or some internalized form of strings.

An alternative implementation is to use multiple inheritance and run-time type identification. An extension interface can be defined as a mixin class. An object that supports a given extension inherits from this mixin class and implements the extension. See Figure 3.

The operations of the mixin class are all abstract and have to be implemented by derived classes. To query an object for an extension, the client uses run-time type information. For example, in C++, the spell checker would query a component whether or not it is (inherits from) a TextAccessor; see Example 2.

The use of a dynamic cast is often suspicious and can point out a design flaw. However, in this case it is okay since a dynamic cast is only used to ask an object whether it supports a certain interface.

This pattern is less important in dynamic languages like Smalltalk. These languages typically provide enough run-time information to allow asking an object whether it responds to a specific request.

Known uses. Support for extension interfaces is common in Compound Document architectures. The example from the Motivation discussion is based on OpenDoc. In OpenDoc, the common base class ODObject provides the access to the extension interface.

Extension objects are related to Microsoft's Component Object Model (COM) and its QueryInterface mechanism. QueryInterface enables a client to query an object for an interface. In COM there is no extended object to start with, and all interfaces of an object are accessed by QueryInterface.

Related patterns. One related pattern is Adapter which adapts an existing interface. Extension Objects provide additional interfaces.

Figure 1 Class relationships using OMTnotation.

Example 1: SimpleTextAccessor::GetExtension(...) implementation.

Extension*SimpleTextComponent::GetExtension(char* extensionName)
{
    ...  if ( strcmp(extensionName, "TextAccessor") == 0)
    return new SimpleTextAccessor(this);
    else if ( strcmp(extensionName, "AnotherExtension") == 0)
    ...
    else
    return 0;
}

Figure 2 Structure for the Extension Objects pattern.

Figure 3 Using multiple inheritance and run-time type identification.

Example 2: Querying a component.

Component*component;
TextAccessor* accessor =dynamic_cast<TextAccessor*>(component);
if (accessor != 0)
    // use the text accessor interface


Copyright © 1995, Dr. Dobb's Journal