Implementing Patterns
Jiri Soukup
1. INTRODUCTION
Until now, activity related to design patterns has focused primarily
on collecting new pattems, describing them as precisely as possible,
and cata- loging and organizing them. Having a good catalog of patterns
is important, but is only the first step. The ultimate goal is to
use these patterns to design software in a new and better way.
Even though all the ground work has not yet been finished, this
paper looks ahead at the problems and possibilities of pattern
implementation. There are three basic problems:
- With the present implementation style, patterns get lost during
program coding. This leads to debugging and maintenance problems
later. Good documentation certainly helps here, but a safer
and cleaner solution would be for the final code to record the
patterns.
- Multiple patterns can lead to large clusters of mutually dependent
classes, which is one of the prime reasons why object-oriented
systems can be difficult to manage. Any method that would break
at least so of these dependency cycles would greatly improve
the program.
- Until now it has been quietly assumed that the programmer
will implement patterns in code by designing the classes as
prescribed by the pattern. As as I know, nobody has described
a library of concrete reusable patterns.
This paper shows that problems 1 and 2 above can be solved by representing
each pattern with a special class called a Pattern class. This class
encap Sates all the behavior and logic of the pattern. The classes
that form die pattern contain no pattern-related methods, only pointers
and the otherdita required for the pattern. The result is a major
decoupling of the applicati classes, and separation of application
classes from pattems. This paper a shows how you can build a reusable
library of many common patterns.
The idea of representing each pattern with a special class will
first explained with simple, realistic examples. I will then provide
more precise proof. All the examples in this paper have been coded
in C++.
2. ABSTRACT AND CONCRETE PATTERNS
One problem with pattern-based design is that there is currently
no generally accepted definition of what constitutes a pattern.
My definition of a pattern overlaps with Booch's mechanism [3].
A pattern describes a situation in which several classes cooperate
on a certain task and form a specific organization and communication
pattern. Note that most of the patterns described by Gamma et al.
[5] fall into this category. My definition is limited to structural
patterns used in software architecture. It includes neither low-level
language-related idioms nor general patterns related to the managment
of software teams (see chapters throughout this volume).
On the other hand, my definition includes data structures that
require the cooperation of several classes, such as graphs, many-to-many
associattions, and aggregations. Even though there is no accepted
definition of what constitutes a pattern, there is a general agreement
that a pattern description must contain not only the structural
information (classes, their relations, and communication), but
also the pattern's purpose, the forces it resolves, and conditions
necessary for successful implementation. In this paper I am primarily
interested in how to design libraries of patterns; for this reason,
the structural part of patterns is emphasized. I assume that the
remaining information (reasons, forces, conditions, recommended
uses) will be recorded in the comment segment associated with
each pattern stored in the library.
The software architecture patterns cataloged so far jsee Gamma
et al. [5]) are mostly abstract, general pattems. However, if
we discuss architecture only at this high level, we'll never get
a code implemented. At a certain stage of the design process,
we have to implement the patterns. This leads to the differentiation
between abstract and concrete patterns.
Descriptions of abstract patterns usually include one or more
examples of concrete implementations jsee Gamma et al. [51 and
this volumes Often there are several possible ways to implement
a given abstract pattem. For example, a collection can be implemented
as a singly linked list, a doubly linked list, or a dynamic array.
Elements stored in the collection can be linked directly jas in
most custom implementations} or indirectly has in most class libraries
or Smalltalk). Also, once we start the implementation we are no
longer language-independent.
When designing software, you may start with abstract patterns
and switch to concrete patterns later. Or you can start directly
with concrete patterns that imply a certain implementation style
and later replace them with other concrete patterns implementing
the same abstract pattem. The second method is less abstract,
but it may better fit the spiral design model typical of object-oriented
software today. In either case you need many more concrete patterns
than abstract pattems.
In the remaining part of this paper the word pattern
refers only to concrete pattems, unless explicitly stated otherwise.
3. LOSING PATTERNS DURING IMPLEMENTATION
The common practice, design patterns are used only during the initial,
conceptual design. Once the overall architecture is detemmined,
programmers take over to implement the pattems. Except perhaps for
the documentation or comments scattered throughout the code, the
patterns are not visible in the final code.
For example, Coad [2] describes a relatively simple design involving
eight classes and six pattems. In his diagram (see Figure 1),
it is difficult to see which patterns are involved in the final
design shown in Figure 1 (see Figure 2). Two different programmers
could, perhaps, even arrive at two different sets of patterns.
Programmers tend to lose sight of the original pattems; this
is a major source of maintenance problems. Working with the code
does not necessarily allow one to see the underlying concepts.
Adding or removing patterns is difficult, and the documentation
describing the patterns will eventually get lost or become obsolete.
Many of these problems would be solved if the final code reflected
the existence of the patterns in a simple and straightforward
manner.
FIGURE 1 A design that includes eight classes and six patterns,
as published by Coad [2]. Even if the designer used the pattern
approach, the original intent is lost (see Figure 2).
4. CLUSTERS OF INTERDEPENDENT CLASSES
When applying several patterns to the same set of classes (in situations
such as those in Figures 1 and 2), pattern behavior is embedded
in methods associated with various application classes. By definition,
each class in each pattern calls methods of several other classes;
as a result of this chain reaction, the class depends directly or
indirectly on every other class. The entire design becomes a big
knot of interdependent classes and methods.
Note, however, that some relatively less frequent patterns Such
as Filters and Pipes in Gamma et al. [5]) do not imply tight communication
between the participating classes. Therefore these do not fit
the type of class this paper addresses.
Complex interdependency affects both debugging and testing.
Code with complex embedded relations is difficult to understand.
Classes and patterns cannot be tested individually. Adding, removing,
or replacing patterns is a sensitive and error-prone operation.
FIGURE 2 Patterns involved in the design shown in Figure
1, according to Coad [2]. The main problem here is that we have
to deal with the entire design as one indivisible entity. Imagine
maintaining an object-oriented CASE system that has five hundred
classes nested up to seven levels deep, interwoven with fifty
patterns that each involve two to four additional classes tthese
numbers are from an actual project).
You may of course say that such a problem is intrinsically complicated.
Patterns as such are not the source of the complexity; however,
as the next section shows, the implementation of patterns does
play a major role. One of the reviewers of this paper challenged
this entire view by stating that, according to his experience,
"large systems with lots of patterns are not monolithic jungles
of interdependent classes." The difference of opinion may be caused
by different personal experiences, different types of application,
or use of a particular programming language. For example, class
dependencies are a more serious problem for efficient C++ frameworks
with many embedded pointers than they are for Smalltalk programs
based on indirectly linked container classes. The Introduction
and Chapter 9 (Case Studies) in Soukup's book [4] describe some
large C++ projects where things became a serious issue.
5. PATTERN CLASSES
Most of the problems described above can be solved by introducing
a special type of class to manage each pattern_I will refer to it
as a pattern class. This concept is similar to Hogg's Islands and
Bridges [6]. In the C++ language the pattern class will be a friend
of the classes that fomm the pattem, and it will encapsulate all
the code logic (communication, messages) of the pattern. It will
also contain all the external interfaces for the pattem. The classes
that form the pattern will contain all the relevant data (pointers,
indices, values), but no pattem-related methods. Ideally the pattern
class contains no data; its main purpose is the interface.
We will not use the pattern class in situations where the entire
interface (all the methods) is assigned to a single class. This
class already provides the functionality of the pattern class.
Regardless of how many classes are involved, the pattern description
will be recorded in the comment part of the pattern class.
Figure 3 shows the Directed Graph pattern implemented in this
style. Note that classes Node, Edge, and Root contain only pointers
(firstEdge, nextEdge, targetNode, and so
on), while the entire pattern interface (addEdge, removeNode,
and so on) is associated with the pattern class (DirectedGraph).
The iterator class for traversing edges adjacent to a given node
is a separate class, again loosely associated with the classes
that form the pattern (through friends).
Figure 4 shows the implementation of the example in Figures
1 and 2, using pattern classes. Note how the design splits into
two layers of classes, with only friend relations between the
two layers. Since the entire implementation of the patterns is
separated by the pattern classes, there is very little, if any,
interaction between the application classes. Except for the functionality
related to the pattems, each application class can be tested independently.
The functionality of any given pattern can be tested again, in
isolation with only those application classes that fomm the pattern.
One important feature of the pattern class is that it remains
in the final code as a record of what pattern is being used. Adding
or removing a pattern is simple: you add or remove a pattern class
and modify only a few members in the related application classes
(for an example see the State-Town-Highway program).
FIGURE 3 A directed graph implemented as a low-level pattern.
Directed Graph is the pattern class here; it includes all the
methods related to the graph manipulation. Classes Node, Edge,
and Root contain pointers that implement the graph, but no graph-related
methods.
One of the reviewers, Ralph Johnson, suggested that I present
"Pattern as a Class" as a new pattern. IThe pattern proposal is
included here in Appendix A.) Note, however, that this entire
methodology has been developed independently of Alexander, and
that the prime focus of this paper is on the implementation of
patterns, not on formalizing the method as a new pattern.
6. LIBRARY OF GENERIC PATTERNS
The following coding style has been used for generic data structures
in the C/C++ Data Object Library from Code Farms, Inc. (the syntax
has been changed slightly for the purpose of this presentation):
Assume that you have classes State, Town, and Highway. State
contains Towns iforming the pattern Aggregation), and Highways
connect Towns forming Graph). When coding this problem, your code
will have two parts.
FIGURE 4 Design from Figures 1 and 2 using pattern classes
(right column). Even though there are many friend relations (dashed
arrows), application classes (left column) are mutually independent.
File code.hpp will define classes and patterns, while file code.cpp will
contain the methods.
This is file code.hpp:
Class State {
inject_State
// ... any members or methods you wish
};
class Town {
inject_Town
// ... any members or methods you wish
};
Class Highway {
inject_Highway
// ... any members or methods you wish
};
// 'states' identifies the pattern
pattern_Aggregation(states,State,Town);
// 'roads' identifies the pattern
pattern_Graph(roads,Town,Highway);
This is file code.cpp:
#include "generated.hpp" // automatically generated classes
#include "code.hpp"
#include "generated.cpp" // automatically generated methods
. . .
State* s=new State;
Town* t=new Town;
states.add(s,t); // add town to the state
. . .
Highway *h;
roads.rem(h); // remove the highway
// from the road network
A special code generator reads file code.hpp and generates
files generated.hpp and generated.cpp. The internal
implementation of the library is beyond the scope of this paper;
however, the two critical steps in this implementation are described
below.
First, file generated.hpp defines pointers and other
variables needed in all inject_.. statements. It also contains
definitions of all pattern classes with types and pointers already
customized to the particular pattern:
#define inject_State \
friend class pattern_states;\
Town *child states;
#define inject_Town \
friend class pattern states;\
friend class pattern_roads;\
State *parent_states;\
Town *next_states;\
Highway *edges_roads;
#define inject_Highway \
friend class pattern_roads;\
Town *target_states;\
Highway *next_roads;
class pattern_states {
public:
void add(State *s, Town *t);
State *parent(Town *t){return(t->parent_states;)}
Town *next(Town *t){return(t->next_states;)}
. . .
};
class pattern_roads {
public:
void rem(Highway *h);
};
#define pattern_Aggregate(id,a,b) pattern##id id
#define pattern_Graph(id,a,b) pattern##id id
#define typel_states State
#define type2_states Town
#define typel_roads Town
#define type2_roads Highway
Second, pattern classes are stored in the library in a parametric
format, in which both types and variable names depend on a single
parameter, $. For example:
// generic pattern class pattern_Aggregate(typel,type2) id;
class pattern_$ {
public:
void add(typel_$ *s, type2_$ *t);
typel_$ *parent(type2_$ *t){return(t->parent_$;)}
type2_$ *next(type2_$ *t){return(t->next_$;)}
The advantage of this format is that the customized version of the
class can be generated simply by replacing $ with the pattern id
(in this case the string states). Another advantage is that
if you remove $ with a text editor you get a perfectly readable
class, which you can debug independently before putting it back
into parametric format.
Note that we have only a single parameter, even though several
classes r form the pattem, and this format cannot be replaced
by a template. Not only s types, but also some member names (pointers)
are parameterized.
This library design permits the implementation of pattem classes,
for any pattem, in which the cooperation between classes depends
on class members, pointers, and messages being passed between
the classes that form the pattem. It does not facilitate patterns
that involve the use of inheritance; however, if we move the {
symbol from the class definition into the inj ect_ . . . statement,
patterns involving inheritance can be handled in the same style.
For example, assume that the pattem Aggregate requires the first
class to inherit the second class (this has no practical meaning;
we use this assumption simply for demonstration). Then we can
have
class State inject_State // {
// ... any members or methods you wish
};
class Town inject_Town // {
// ... any members or methods you wish
};
...
#define inject_State : public Town { \
friend class pattern_states;\
Town *child states;
#define infject_Town { \
friend class pattern states;\
friend class pattern_roads;\
State *parent_states;\
Town *next_states;\
Highway *edges_roads;
The only problem here is that the syntax of the inject_...
statement will be different if State also inherits from another
class. For example:
class territory { ... };
class State : public Territory inject_State // {
// ... any members or methods you wish
};
#define inject_State , public Town { \
friend class pattern states;\
Town *child_states;
This is really not a serious problem, since the existing Code Famms
generator already knows the inheritance tree of the application
classes.
When storing certain more complex patterns in such a library,
the inject_.. statement may also include some private methods.
For example, when representing pattern Composite (see Figure 5
and Appendix B) in this style, both inject_Graphic and
inject_Picture must include the method detectPicture().
Classes such as Text or Line from Appendix B are not a part of
the pattern; they are only derived from class Graphic as a part
of the application.
FIGURE 5 An example of the Composite pattern obtained from
Erich Gamma. This is the situation before the pattern class is
applied.
7. PROOF
We have already shown that pattern classes remain in the code as
a permanent record of what patterns have been used. This solves
the first problem stated at the beginning of the paper Presently,
patterns are lost during coding). For example:
pattern_Aggregation(states,State,Town);
pattern_Graph(roads,Town,Highway);
Any operations related to the pattern can be easily identified by
looking at the pattern id:
states.add(s,t);
roads.rem(h);
We also showed one possible way of constructing a library of concrete
patterns (the third problem stated above). Also, a class library
built on this idea has been commercially available for over four
years (C++ Data Object Library, from Code Farms, Inc.), and has
been successfully used on hundreds of projects (some of them in
excess of five hundred classes and 100,000 or more lines of coded
However, the present version of the library is limited to patterns
that do not involve inheritance, and the modification described
above has not been extensively tested. Even though the modified
library can store many patterns, it is likely that some patterns
will not fit this library format.
FIGURE 6 One of the edges d(..) must be caused by
a dependency other than inheritance. If this is edge d(i,j),
then introducing pattern class p(k) will break the dependency
cycle.
What remains now is to prove that in the case of multiple, partially
overlapping patterns, the use of pattern classes breaks dependency
cycles. Let us assume that without pattern classes our design
has application classes a(1) to a(n), with mutual
dependencies introduced by the patterns (see Figure 6). Class
a(j) is dependent on class a(i) if a(j) accesses
data from a(i), calls one of its methods, or inherits from
it. When a(j) depends on a(i), we add edge d(i,j)
between the nodes a(i) and a(j).
If we introduce a pattern class p(k), the original dependency
d(i,j) is replaced by friendship dependencies e(k,i)
between p(k) and a(i), and e(k,j) between
p(k) and a(j). Since all dependencies e(k,..)
are directed away from p(k), this automatically breaks
any cycle involving d(i,j). After introducing a pattern
class and possibly one or several iterators for each pattern gas
in Figure 31, all cycles will be removed. In fact, all original
edges d(..) will be eliminated, except for those that represent
inheritance and those that cannot form cycles.
Every dependency cycle we break makes the application classes
more independent and improves the entire architecture.
8. CONCEPTUAL ISSUES
An important question related to the use of pattern classes is whether
any pattern can be represented in this form. Perhaps there are patterns
that cannot be implemented in this style, but I have not seen one
yet. I discussed this with a number of people, and both Erich Gamma
and several other programmers suggested that the Composite pattern
may be one of those that are difficult to implement. My version
of this pattern, using a pattern class, is in Appendix B.
Note that pattern classes permit you to work with two different
kinds of instances. For example, when a set of objects is simultaneously
involved in two different graphs, each graph corresponds to one
instance of the pattern class, but each connected subgraph corresponds
to one instance of class Root (see Figure 4).
ACKNOWLEDGMENTS
I would like to thank all reviewers, and in particular Ralph Johnson,
for suggestions and comments that much improved this paper.
REFERENCES
- Gamma, E., Helm, R., Johnson, R., and Vlissides J. Design
Patterns: Abstraction and Reuse of Object-Oriented Design, ECOOP'93.
Karlsruhe, Germany, August 1993.
- Coad, P. Object-oriented patterns. Communications of the ACM
35, 9 lSeptcmber 1992}: 152-159.
- Booch, G. Object-Oriented Design with Applications. Redwood
City, CA: Benjamin/Cummings .
- Soukup, J. Taming C++: Pattern Classes and Persistence for
Large Projects. Reading, MA: Addison-Wesley, 1994.
- Gamma, E.; Helm, R.; Johnson, R.; and Vlissides, J. Design
Patterns: Elements of Reusable Object-Onented Software. Reading,
MA: Addison-Wesley, 1995.
- Hogg, J. Islands: Aliasing protection in object-oriented languages.
OOPSLA'91, Conference Proceedings 9 (ACM), Nov. 1991, pp. 271-285.
APPENDIX A: PATTERN AS A CLASS
Intent Pattern classes represent entire patterns as objects.
Motivation Present techniques present three major problems
in implementing patterns:
- Patterns usually get lost during coding.
- Patterns may lead to large clusters of mutually dependent
classes.
- Patterns are typically hand-coded in each new application.
The first problem is caused by the pattern not being an object.
The second problem is caused by the presence of dependency cycles
introduced by the patterns. The third problem has its root in the
programming language; nobody thought about design patterns when
Smalltalk or C++ was invented.
Forces An architecture with fewer classes is often not
the cleanest solution. Adding some classes may improve code clarity
and maintenance. The use of friends may appear as a violation
of encapsulation; but in the way they are used here, all the walls
between application classes remain intact. For the pattern class,
accessing the members directly is more efficient.
Applicability This pattern is only applicable to patterns
that describe the cooperation of several classes. It can always
be used when the pattern does not include inheritance. The implementation
of the Composite pattem, generally considered to be very difficult,
exists. So far, no pattern has been found for which this method
would fail. Do not apply this pattern to patterns where all the
methods are already assigned to a single class.
Particpants The pattern class_represents the pattern
and contains all (or almost all) methods.
Several application classes_contain data and pointers, but no
pattern-related methods (some virtual functions may be an exception).
Collaborations The pattern class performs all operations
on the pattern. Application classes only carry the data.
Structure See Figures 3 and 4. Consequences This
pattern improves and sometimes completely cures the three problems
mentioned above. The pattern class breaks dependency cycles, its
declara- tion remains in the code as a record of which pattern
has been used, and in a new parametrized style, it can be stored
in a reusable library.
Application classes and patterns are, from the user's point
of view, orthogonal.
Limitations The technique is limited to patterns that
generally coincide with the definition of a mechanism: a group
of classes with closc cooperation on a certain task. I have added
one pattern class and one iterator for a typical pattern.
The pattern class does not contain the entire pattern; it only
represents its interface. The overall intent, and some structural
details such as inheritance, cannot be expressed by this class
alone. Additional structural information must be stored in the
library: location of members, pointers, inheritance, and other
details.
Implementation In C++, the pattern class is a friend
of the application classes. The pattern is typically implemented
directly, not with indirect lists or pointers.
Known Uses This approach has been used by the Code Farms
C/C++ Data Object Library and was successfully applied to hundreds
of projects, some of them with over 100,000 lines of code. Patterns
with inheritance were subject to only marginal testing.
APPENDIX B: C++ IMPLEMENTATION OF CLASS COMPOSITE
The following code shows an implementation of the Composite pattern
using a pattern class (class Composite). The code is based on Figure
5, which was obtained from Erich Gamma. Note that in this case the
pattern class represents the pattern (the entire external interface
is in class Composites however, it does not implement the pattern.
An important part of the pattern is the inheritance and virtual
functions in classes Graphic, Picture, Line, and Text. Note that
all pattern-related methods are in class Composite except for one
function, pictureDetect().
// file graphic.h starting here #include <iostream.h>
class Composite;
class Graphic {
friend class Composite;
Graphic *next;
virtual int pictureDetect(){return(O);}
public:
virtual void draw(){}
Graphic(){next=NULL;}
};
class Line : public Graphic {
int xl,yl,x2,y2;
public:
virtual void draw(void){cout<<"Line:"<<xl<<yl<<x2<<y2<<'\n';)
Line(int Xl,int Yl,int X2,int Y2){xl=X1; yl=Y1; x2=X2; y2=Y2;}
};
class Text : public Graphic {
int x,y;
char *text;
public:
virtual void draw(void){cout<<"Text:g <<X<<y<< text<<'\n';}
Text(int X,int Y. char *tx){x=X; y=Y; text=tx;}
};
class Picture : public Graphic {
friend class Composite;
Graphic *first;
virtual int pictureDetect(){return(1);}
public:
virtual void draw(void){cout<<"Picture:\nN;}
Picture(){first=NULL;}
// file graphic.h ending here
class Composite {
public:
void add(Picture *p, Graphic *g){g->next=p->first;
p->first=g;}
void remove(Picture *p, Graphic *g);
void draw(Graphic *g);
void dissolve(Graphic *g);
};
void Composite::remove(Picture *p, Graphic *g){
Graphic *t;
if(p->first==g){p->first=g->next; g->next=NULL;}
else {
for(t=p->first; t; t=t->next) if(t->next==g)break;
if(t){t->next=g->next; g->next=NULL;}
else cout<<"error\n";
}
}
void Composite::draw(Graphic *g){
static int level=O; // not necessary, just for better
// displays
int i;
Graphic *t;
for(i=O;i
level;i++)cout<<" "; // just to indent the
// print
g->draw();
if(g->pictureDetect()){
level++;
for(t=((Picture *)g)->first; t; t=t->next) draw(t);
level-;
}
}
void Composite::dissolve(Graphic *g){
Graphic *t,*tn;
if(g->pictureDetect(){
for(t=((Picture *)g)->first; t; t=tn){
tn=t->next;
t->next=NULL;
}
((Picture *)g)->first=NULL;
}
}
int main(void){
Line *nl,*n2,*n3,*n4; Text *tl,*t2,*t3
Picture *pl,*p2,*p3;
nl=new Line(1,1,11,11); n2=new Line(2,2,22,22);
n3=new Line(3,3,33,33); n4=new Line(4,4,44,44);
tl=new Text(l,l,"one"); t2=new Text(2,2,"two");
t3=new Text(3,3,"three");
pl=new Picture; p2=new Picture; p3=new Picture;
Composite comp;
comp.add(pl,nl); comp.add(pl,n2); comp.add(p2,n3);
comp.add(p2,n4); comp.add(p3,tl); comp.add(p3,t2);
comp.add(p2,t3); comp.add(p2,pl); comp.add(p3,p2);
comp.remove(p2,n4); comp.add(p3,n4);
comp.draw(p3);
comp.dissolve(p2);
cout<<'\n'; comp.draw(p3);
return(O);
}
|