Выбрать главу

This chapter introduces the important and yet nontraditional "patterns" approach to program design.

In recent times, the most important step forward in object-oriented design is probably the "design patterns" movement, chronicled in Design Patterns, by Gamma, Helm, Johnson & Vlissides (Addison-Wesley, 1995).[116] That book shows 23 solutions to particular classes of problems. In this chapter, we discuss the basic concepts of design patterns and provide code examples that illustrate selected patterns. This should whet your appetite for reading Design Patterns (a source of what has now become an essential, almost mandatory vocabulary for object-oriented programming).

The pattern concept

Initially, you can think of a pattern as an especially clever and insightful way to solve a particular class of problems. That is, it looks like a lot of people have worked out all the angles of a problem and have come up with the most general, flexible solution for that type of problem. The problem could be one you have seen and solved before, but your solution probably didn’t have the kind of completeness you’ll see embodied in a pattern. Furthermore, the pattern exists independently of any particular implementation and, indeed, can be implemented in a number of ways.

Although they’re called "design patterns," they really aren’t tied to the realm of design. A pattern seems to stand apart from the traditional way of thinking about analysis, design, and implementation. Instead, a pattern embodies a complete idea within a program, and thus it can sometimes also span the analysis phase and high-level design phase. Because a pattern has a direct implementation in code, you might not expect it to show up before low-level design or implementation (and in fact you might not realize that you need a particular pattern until you get to those phases).

The basic concept of a pattern can also be seen as the basic concept of program design in generaclass="underline" adding layers of abstraction. Whenever you abstract something, you’re isolating particular details, and one of the most compelling motivations for this is to separate things that change from things that stay the same. Another way to put this is that once you find some part of your program that’s likely to change for one reason or another, you’ll want to keep those changes from propagating other modifications throughout your code. Not only does this make the code easier to maintain, but it also renders code easier to read and understand (which invariably results in lowered costs over time).

The most difficult part of developing an elegant and maintainable design is often discovering what we call "the vector of change." (Here, "vector" refers to the maximum gradient as understood in the sciences, and not a container class.) This means finding the most important thing that changes in your system or, put another way, discovering where your greatest cost is. Once you discover the vector of change, you have the focal point around which to structure your design.

So the goal of design patterns is to isolate changes in your code; to put it another way: discover what changes and encapsulate it. If you look at it this way, you’ve been seeing some design patterns already in this book. For example, inheritance could be thought of as a design pattern (albeit one implemented by the compiler). It allows you to express differences in behavior (that’s the thing that changes) in objects that all have the same interface (that’s what stays the same). Composition could also be considered a pattern, since it allows you to change—dynamically or statically—the objects that implement your class, and thus the way that class works. Normally, however, features that are directly supported by a programming language have not been classified as design patterns.

You’ve also already seen another pattern that appears in Design Patterns: the iterator. This is the fundamental tool used in the design of the STL, described earlier in this book. The iterator hides the particular implementation of the container as you’re stepping through and selecting the elements one by one. Iterators allow you to write generic code that performs an operation on all the elements in a range without regard to the container that holds the range. Thus, your generic code can be used with any container that can produce iterators.

The Singleton

Possibly the simplest design pattern is the Singleton, which is a way to provide one and only one instance of a class. For example, if a class controls a Singleton resource, you only want to allow one instance of that class. The following program shows how to implement a Singleton object in C++:

//: C10:SingletonPattern.cpp

#include <iostream>

using namespace std;

class Singleton {

  static Singleton s;

  int i;

  Singleton(int x) : i(x) { }

  Singleton& operator=(Singleton&);  // Disallowed

  Singleton(const Singleton&);       // Disallowed

public:

  static Singleton& instance() {

    return s;

  }

  int getValue() { return i; }

  void setValue(int x) { i = x; }

};

Singleton Singleton::s(47);

int main() {

  Singleton& s = Singleton::instance();

  cout << s.getValue() << endl;

  Singleton& s2 = Singleton::instance();

  s2.setValue(9);

  cout << s.getValue() << endl;

} ///:~

The key to creating a Singleton is to prevent the client programmer from having any control over the lifetime of the object. To do this, declare all constructors private, and prevent the compiler from implicitly generating any constructors. Note that the copy constructor and assignment operator are declared private to prevent any sort of copies being made.

You must also decide how you’re going to create the object. Here, it’s created statically, but you can also wait until the client programmer asks for one and create it on demand. This is called lazy initialization, and it only makes sense if your object is expensive to create and if it might not always be needed.

If you return a pointer instead of a reference, the user could inadvertently delete the pointer, so the implementation above is considered safest. In any case, the object should be stored privately.

You provide access through public member functions. Here, instance( ) produces a reference to the Singleton object. The rest of the interface (getValue( ) and setValue( )) is the regular class interface.

Note that you aren’t restricted to creating only one object. This technique easily supports the creation of a limited pool of objects. In that situation, however, you can be confronted with the problem of sharing objects in the pool. If this is an issue, you can create a solution involving a check-out and check-in of the shared objects.

Variations on Singleton

Any static member object inside a class is an expression of Singleton: one and only one will be made. So in a sense, the language has direct support for the idea; we certainly use it on a regular basis. However, a problem is associated with static objects (member or not), and that’s the order of initialization, as described in Volume 1 of this book. If one static object depends on another, it’s important that the objects are initialized in the correct order.

вернуться

116

Also known as the “Gang of Four” book (GoF). Conveniently, the examples are in C++.