Visitors

.

Since Jan1,2000

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:

  1. 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.

  2. 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.

  3. 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

  1. Gamma, E., Helm, R., Johnson, R., and Vlissides J. Design Patterns: Abstraction and Reuse of Object-Oriented Design, ECOOP'93. Karlsruhe, Germany, August 1993.
  2. Coad, P. Object-oriented patterns. Communications of the ACM 35, 9 lSeptcmber 1992}: 152-159.
  3. Booch, G. Object-Oriented Design with Applications. Redwood City, CA: Benjamin/Cummings .
  4. Soukup, J. Taming C++: Pattern Classes and Persistence for Large Projects. Reading, MA: Addison-Wesley, 1994.
  5. Gamma, E.; Helm, R.; Johnson, R.; and Vlissides, J. Design Patterns: Elements of Reusable Object-Onented Software. Reading, MA: Addison-Wesley, 1995.
  6. 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:
  1. Patterns usually get lost during coding.
  2. Patterns may lead to large clusters of mutually dependent classes.
  3. 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); }

 

Home | About Us | Products | Services | Downloads | Support | Publications | Contact | Sitemap