The
pattern concept
Initially,
you can think of a pattern as an especially clever and insightful way of
solving 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 it. 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.
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 appear at the analysis phase or high-level design phase. This is
interesting because a pattern has a direct implementation in code and so 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: adding layers of abstraction.
Whenever you abstract something you’re isolating particular details, and
one of the most compelling motivations behind 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 much cheaper to maintain, but it also turns
out that it is usually simpler to understand (which results in lowered costs).
Often,
the most difficult part of developing an elegant and cheap-to-maintain design
is in discovering what I call “the vector
of change.” (Here, “vector” refers to the maximum gradient
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. 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 are not 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; it hides the
particular implementation of the container as you’re stepping through and
selecting the elements one by one. The iterator allows you to write generic
code that performs an operation on all of 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 an object:
//: C25:SingletonPattern.cpp
#include <iostream>
using namespace std;
class Singleton {
static Singleton s;
int i;
Singleton(int x) : i(x) { }
void operator=(Singleton&);
Singleton(const Singleton&);
public:
static Singleton& getHandle() {
return s;
}
int getValue() { return i; }
void setValue(int x) { i = x; }
};
Singleton Singleton::s(47);
int main() {
Singleton& s = Singleton::getHandle();
cout << s.getValue() << endl;
Singleton& s2 = Singleton::getHandle();
s2.setValue(9);
cout << s.getValue() << endl;
} ///:~
The
key to creating a singleton is to prevent the client programmer from having any
way to create an object except the ways you provide. To do this, you must
declare all constructors
as
private,
and you must
create
at least one constructor to prevent the compiler from synthesizing
a default constructor for you.
At
this point, you decide how you’re going to create your object. Here,
it’s created statically, but you can also wait until the client
programmer asks for one and create it on demand. In any case, the object should
be stored privately. You provide access through public methods. Here,
getHandle( )
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, there’s a problem
associated with static objects (member or not), and that’s the order of
initialization, as described earlier in this book. If one static object depends
on another, it’s important that the order of initialization proceed
correctly.
Fortunately,
there’s another language feature that allows you to control many aspects
of initialization order, and that’s a static object defined inside a
function. This delays the initialization of the object until the first time the
function is called. If the function returns a reference to the static object,
it gives you the effect of a singleton while removing much of the worry of
static initialization. For example, suppose you want to create a logfile upon
the first call to a function which returns a reference to that logfile. This
header file will do the trick:
//: C25:LogFile.h
#ifndef LOGFILE_H
#define LOGFILE_H
#include <fstream>
inline std::ofstream& logfile() {
static std::ofstream log("Logfile.log");
return log;
}
#endif // LOGFILE_H ///:~
Since
it’s
inline,
the compiler and linker are responsible for guaranteeing there’s only one
actual instance of the function definition. The
log
object will not be created until the first time
logfile( )
is called. So if you use the function in one file:
//: C25:UseLog1.cpp {O}
#include "LogFile.h"
using namespace std;
void f() {
logfile() << __FILE__ << endl;
} ///:~
And
again in another file (for simplicity I didn’t create a header file for
UseLog1.cpp):
//: C25:UseLog2.cpp
//{L} UseLog1
#include "LogFile.h"
using namespace std;
void f(); // In lieu of a header file
void g() {
logfile() << __FILE__ << endl;
}
int main() {
f();
g();
} ///:~
Then
the
log
object
doesn’t get created until the first call to
f( ). You
can easily combine the creation of the static object inside a member function
with the singleton class.
SingletonPattern.cpp
can be modified to use this approach:
//: C25:SingletonPattern2.cpp
#include <iostream>
using namespace std;
class Singleton {
int i;
Singleton(int x) : i(x) { }
void operator=(Singleton&);
Singleton(const Singleton&);
public:
static Singleton& getHandle() {
static Singleton s(47);
return s;
}
int getValue() { return i; }
void setValue(int x) { i = x; }
};
int main() {
Singleton& s = Singleton::getHandle();
cout << s.getValue() << endl;
Singleton& s2 = Singleton::getHandle();
s2.setValue(9);
cout << s.getValue() << endl;
} ///:~
An
especially interesting case is if two of these singletons depend on each other,
like this:
//: C25:FunctionStaticSingleton.cpp
class Singleton1 {
Singleton1() {}
public:
static Singleton1& ref() {
static Singleton1 single;
return single;
}
};
class Singleton2 {
Singleton1& s1;
Singleton2(Singleton1& s) : s1(s) {}
public:
static Singleton2& ref() {
static Singleton2 single(Singleton1::ref());
return single;
}
Singleton1& f() { return s1; }
};
int main() {
Singleton1& s1 = Singleton2::ref().f();
} ///:~
When
Singleton2::ref( )
is called, it causes it’s sole
Singleton2
object to be created. In the process of this creation,
Singleton1::ref( )
is called, and that causes the sole
Singleton1
object to be created. Because this technique doesn’t rely on the order of
linking or loading, the programmer has much better control over initialization,
leading to less problems.
You’ll
see further examples of the singleton pattern in the rest of this chapter.
Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru