Patterns for Reusable Object-Oriented Software

<br />Though they hardly need an introduction, Richard Helm and Erich Gamma are two of the coauthors of the influential book <em>Design Patterns: Elements of Reusable Object-Oriented Software </em>(Addison-Wesley, 1994). In this 1995 article from <em>Dr. Dobb&#39;s Journal</em>, they turn their attention to patterns in relation to OO programming.</p>

InformationWeek Staff, Contributor

June 5, 2008

13 Min Read
InformationWeek logo in a gray background | InformationWeek


Though they hardly need an introduction, Richard Helm and Erich Gamma are two of the coauthors of the influential book Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994). In this 1995 article from Dr. Dobb's Journal, they turn their attention to patterns in relation to OO programming.

 

Patterns for Reusable Object-Oriented Software
By Richard Helm and Erich Gamma


Component-based software, interoperable objects, reusable application toolkits, and frameworks are becoming increasingly important development technologies. Many application architectures and component-interconnect technologies and standards are being put in place: CORBA, OpenDoc, TalAE, COM, SOM, and OLE, to mention a few (see Dr. Dobb's Special Report on Interoperable Objects, Winter 1994/95). However, we still face the problem of creating designs which can effectively exploit these emerging technologies. A common theme in these technologies, standards, and implementations is the notion of object orientation--building systems from objects which offer services to clients. And a common goal when designing with objects is reuse--how to design your applications so that the objects used to build them can, in turn, be used in other applications.
Reuse in object-oriented software is enabled through three primary mechanisms: parameterized types, class inheritance, and object composition. Parameterized types allow you to create new functionality by parameterizing software by the types of objects on which it operates. Inheritance allows you to define new classes in terms of old, reusing implementation from parent classes in the implementation of the new child classes. Inheritance is simple, performed at compile time, and supported directly by most object-oriented languages. Object composition permits you to create new functionality by composing existing objects together in new and interesting ways. It relies on polymorphism and dynamic binding--the ability to substitute objects with similar interfaces for each other at runtime. Object composition lets clients make very few assumptions about the implementations of objects they deal with, other than that they support a particular interface. Object composition makes it easy for new, user-defined objects to work with existing objects.
During the initial stages of object-oriented software's design-and-implementation life cycle, inheritance is the predominant means to achieve reuse. Most effort is spent creating and deriving new classes. In later stages of the life cycle (especially after redesign or refactoring of class hierarchies), the dominant means of reuse is the composition of objects having standard interfaces. At this stage, the important abstractions in the domain have emerged and have their own class hierarchies. Inheritance is only used as an implementation technique to rapidly define families of objects with similar interfaces. Once a design begins to focus on object composition rather than inheritance, the interconnect technologies described here can also be considered.
Toolkits, class libraries, and frameworks are ways to package and deliver larger, reusable abstractions. Many vendors now provide toolkits or frameworks of some sort. Toolkits can be thought of as the object-oriented equivalent of a subroutine library. They provide low-level, ready-made classes which can be extended, through inheritance, to provide access to some underlying abstraction, such as data structures, operating-system services, windowing, or graphics systems. Frameworks provide higher-level functionality, generally targeting a particular application domain such as graphical-object editors, operating systems, or financial engineering. Compared to toolkits and class libraries, frameworks provide higher-level application infrastructures. In particular, they usually include classes which define an application's internal control flow and logic. These classes form the backbone of the application to which the flesh of the application--created from user-defined extensions to the framework's classes and toolkits--is attached. Reusers of the framework customize the framework by:

  • Instantiating classes provided as-is by the framework.

  • Extending the framework by deriving new classes from framework-supplied classes, specializing their behavior and functionality, and instantiating them to create new kinds of objects.

  • Composing these objects with the logic and control-flow classes in framework-defined ways to create a working application.


To be able to extend a framework, the objects composed with the framework must work with and respect the internal interfaces and protocols expected by the framework classes.
Frameworks give rise to an architecture in which most code resides in the reused classes. A characteristic feature of such an architecture is its inverted structure. Most of the high-level application control flow is determined by the reused code, which periodically makes calls of user-supplied extensions (usually to subclasses of framework classes) to request application-specific services and data. Larger applications are usually built from multiple frameworks and toolkits; see Figure 1 (below).


Designing for Reuse
The preceding discussion touches on some of the issues concerning reuse in object-oriented applications: frameworks within frameworks, and objects within each framework communicating with one another. When creating a reusable application, framework, or toolkit, the questions we have to face are: Exactly what sort of abstraction will be supported by our design? How will we enable our design to permit users to extend it easily? How will we design our application to be reusable?
There are no easy answers. Designing reusable, object-oriented software is hard and can be an elusive goal. Many issues must be considered: finding appropriate objects; factoring them into classes at the right level of granularity; defining inheritance hierarchies; defining object interfaces; and specifying appropriate relationships between objects. All these issues must be addressed in designing a system specific to the problem at hand, while remaining general enough to address--and be extended for--future problems and requirements. Experienced designers will tell you that a reusable, flexible design is difficult, if not impossible, to get "right" the first time, especially under the constraints of project and product deadlines, and that multiple attempts at reuse with subsequent redesign is the norm.
Despite these difficulties, it is still possible to write reusable object-oriented software. Many successful object-oriented systems exhibit idiomatic and recurring patterns and structures of communicating objects that solve particular design problems. These design structures are what make these systems flexible, elegant, and ultimately reusable.
Design Patterns
Design structures that occur repeatedly across application domains, programming languages provide well-defined, controlled ways to extend and reuse these applications. Unfortunately, these design structures are not well known, are only learned with much experience, and so are independently rediscovered over and over again by designers creating reusable software. Many of us have had this kind of design deja-vu. Wouldn't it be great if there were a record of the design decisions and experience of others? One way to do this, which is currently gaining a lot of interest, is through "patterns."
Patterns are a way to record and codify expertise and experience so that others may reuse it. Patterns help you base new work on others' prior, distilled experience. A designer familiar with such patterns can apply them immediately to design problems. Many ways of writing patterns to record this experience are being explored, but most agree on the following definition: A pattern describes a solution to a problem in a particular context in such a way that others can reuse this solution over and over again. Our personal interest is in patterns for reusable, object-oriented designs, or "design patterns." A simple example illustrates the concept.
The Strategy design pattern addresses the problem of defining families of interchangeable algorithms so that the algorithms may vary independently from the clients that use them. Situations or "contexts" where it might be applicable include those in which:

  • Many classes only differ in behavior.

  • There exist variants in algorithms, and you want the flexibility to pick and choose.

  • Algorithms have local and private data to which clients should not be exposed.

  • A class defines many different behaviors, typically spread throughout its operations as conditionals governed by internal flags.


The solution provided by the Strategy pattern consists of encapsulating each variation of behavior or algorithm in its own class and accessing this behavior though a common interface, defined by a Strategy class; see Figure 2 (below).

 

At runtime, an instance of a StrategyContext is composed with an instance of ConcreteStrategy. They interact through the interface defined by the abstract Strategy class. Because StrategyContext is only aware of a strategy through the interface defined by the Strategy class, any Concrete-Strategy may be composed with it, and we have freed the StrategyContext from any dependencies on a particular strategy.
Many examples of the Strategy pattern are found in frameworks. For example, word-processor frameworks, have families of text-formatting algorithms that can be interchanged according to how well or how fast you want text formatted. Compiler frameworks have different instruction-scheduling policies that depend on the underlying machine architecture. Financial-engineering frameworks have different ways to value financial instruments. In all these implementations, what is common (the repeating pattern) is that the family of algorithms is defined in its own class hierarchy and is accessed through a common interface in some context.


This touches on only the essentials of the Strategy pattern. A full description would include details of implementation techniques, design trade-offs, benefits and liabilities of the pattern, and relationships with other patterns. The key point is, however, that the Strategy pattern lets you factor out algorithms, allowing your application to be independent of the algorithms it uses.
Note that a design pattern does not describe a particular design for any particular system. The Strategy pattern does not describe how to design text-formatting algorithms. Rather, it abstracts from many designs and (hopefully) describes what is essential, common, and intrinsic to the problems addressed, and solutions found, across all these designs. Just as we can take abstract pseudocode descriptions of algorithms (quick sort or generational garbage collectors, for example) and implement our own systems to sort or collect garbage, so you can take design patterns and create designs and implementations based on them in some modeling notation or object-oriented language; see Figure 3 (below).

 

An often-asked question is, how does a pattern differ from a framework? Most simply, a framework is a design realized as code. In contrast, a pattern describes an abstract design and must first become a design and then an implementation. A pattern will also usually have multiple implementations, each providing different design trade-offs. Patterns also tend to describe designs which are at a different scale than a framework. Think of classes and objects as building blocks, and, of a framework as defining an applications architecture or macro architecture. Most patterns describe something in between, what we call a "micro-architecture"--an architectural element that contributes to the overall software architecture; see Figure 4 (below).

 


Designing for Change
The key to creating reusable software lies in anticipating people's needs and how they might use your solutions to meet those needs. It's important to understand and prepare for future changes in requirements and usage. These could arise from the evolving needs of current users, new users, or both. A design that doesn't take change into account risks major redesign in the future. That will involve class redefinition and reimplementation, modification of existing clients, and retesting. Redesign affects many parts of the software system, and unanticipated changes are invariably expensive to correct. If the expense (real or perceived) is too great, a system will not be reused.
Consequently, you must consider how your system might need to change over its lifetime, and be aware of typical causes of redesign and rework. Ideally, you want to design in this capability from the very beginning. A design that stops people from reusing may be thought of as containing reuse errors. A reuse error does not mean that your software is broken, it is just not as reusable as it might be.
A simple example of a reuse error occurs in C++ when you forget to declare a member function virtual in a parent class for a set of classes that form a class hierarchy. Reusers of these classes will not be able to extend them to change the way their application uses this class hierarchy. The lack of the virtual member function is one reuse error. A higher-level reuse error is when an operation defined by a class is not factored at the right level of granularity to be overridden by subclasses. Subclasses might only want to customize parts of the operation. Unless the operation is designed with such extensions in mind, subclasses will typically copy the existing code and modify it, resulting in duplicated code.
These are minor examples, and careful class design allows you to avoid them. But if you are in the business of providing reusable code to your clients, your organization, or yourself for later reuse, avoiding reuse errors is something to strive for.

Reuse errors have many causes. One common cause is exposing to clients too many details of the implementations of objects it uses. As implementations change, so will client code break. Other causes of reuse errors are badly designed inheritance hierarchies, which grow brittle and difficult to extend as more functionality is added. For example, embedding algorithms into classes on which the algorithm operates on means that as more algorithms are implemented, the classes tend to be buried under the weight of their implementations. The original purpose and abstraction defined by the class will be lost.
Design patterns allow a design to be extended in controlled ways to avoid specific kinds of reuse errors. Strategy, for example, avoids having implementations of algorithms spread all over your code. This property of design patterns gives you a suite of design techniques that will provide your designs with flexibility, extensibility, and ultimately reusability.
Pattern Resources
While there is currently a lot of interest in applying patterns or handbooks of design to develop software, patterns are not new. Patterns have already been used to describe different parts of the software-development process, including reusable object-oriented designs, team structure and process organization, reuse of application frameworks, and description of common themes during systems analysis (see "Evaluating the Software Development Process," by James Coplien, DDJ, October 1994 and "Patterns and Software Development," by Kent Beck, DDJ, February 1994, as well as recent articles in the Journal of Object-Oriented Programming, Object Magazine, and C++ Report).
Within other disciplines of engineering and architecture, it is common to find handbooks of standard design techniques and practices that form a body of common knowledge shared by engineers and designers. Christopher Alexander's book, A Pattern Language: Towns, Buildings, Construction (Oxford University Press, 1977) is one widely quoted example. Another is a five-volume Russian handbook of mechanical engineering we recently discovered, which contains over 4000 mechanical devices, one-per-page, ranging from clutches to aircraft-landing gear. Such a resource is undoubtedly a valuable reference for designers.
But software has few such equivalents. Part of the focus of those working with patterns in software is how to best describe something as intangible as software design and practice. The interest in this effort is reflected in people writing patterns in various books, papers, and a recent conference devoted to the topic as listed shortly. The study of patterns is young but flourishing.
Resources that will help you find more information about patterns and how they are used are included in the World Wide Web home page at http://st-www.cs.uiuc.edu/users/patterns.html. The home page also contains details of forthcoming conferences and examples of patterns and pattern languages and permits you to subscribe to mailing lists about patterns.
Important papers relating to patterns include "Documenting Frameworks Using Patterns," by Ralph Johnson (Proceedings of OOPSLA '92), "Design Patterns: Abstraction and Reuse of Object Oriented Design," by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Proceedings of ECOOP '93), "Design and Reuse in Object-Oriented Frameworks," by Richard Lajoie and Rudolph Keller (ACFAS, 1994), "Progress on Patterns: Highlights of PLoP '94," by Jim Coplien (Object Expo Europe, 1994), and "Patterns Generate Architectures," by Kent Beck and Ralph Johnson (Proceedings of ECOOP '94).

Never Miss a Beat: Get a snapshot of the issues affecting the IT industry straight to your inbox.

You May Also Like


More Insights