This article introduces design patterns and shows how they can be used when creating applications in C++ for problems where two-dimensional and three-dimensional graphical objects need to be created, manipulated and displayed under different hardware and software platforms. This approach is taken if we wish to create flexible and reusable applications.
The topics and techniques discussed in this article will be used in future articles when we consider specific application areas such as CAD, imaging and visualisation. Thus, the treatment given here will lay the foundations for applying design patterns to real-world test cases in future articles in X Advisor.
Our main interest in this article is in showing how the popular design patterns approach (see [Gamma95]) is applied to writing applications in which graphics plays a central role. In particular, our aim is to produce class libraries in C++ which can be constructed and can easily be extended and reused in many different situations. In short, we wish to create program families which can be customised to suit many different types of customer needs.
We introduce a number of objectives which we would like to attain when C++ applications are being built for computer graphics. In general, we wish our classes and code to do no more and no less than is needed for the problem at hand. This is in keeping with the spirit in [Parnas79] in which customers do not get functionality which they do not need. This implies that our software can be extended to suit different types of customer; further, it can be contracted to suit the needs of less demanding customers and environments. Some examples are: (1)
These are the general issues that we wish to discuss in this article. We shall discuss the impact of ensuring that we can satisfy these wishes and that we can find a suitable design pattern which will help us in coming up with a solution in C++ to make those wishes come true. We now give a brief explanation and some examples of where the issues in the list (1) above are used. Furthermore, we determine which particular design pattern from [Gamma95] is the most suitable candidate which will help in producing flexible software.
An implicit underlying assumption in most of the following discussions is
that we have a class library of objects. These could be two-dimensional objects
such as lines, circles and polygons or they could be three-dimensional entities
such as solids, curves and surfaces. Finally, the underlying objects could also
be images because as with other geometric entities images can be created,
manipulated and stored and thus the following techniques are applicable to them
as well. We assume also that each library consists of one or more C++ class
inheritance structures with each class being derived from a common ancestor. For
example, in two dimensions each class is derived from an abstract SHAPE class.
In this way we are able to use all the powerful run-time features of C++ and
furthermore, we can implement design patterns.
The patterns which are discussed in this article are:
We shall discuss these and other patterns in more detail in future articles.
One of the most difficult problems when developing class libraries for computer graphics applications is that we do not know who our clients are. For example, if we create a C++ class for a simple solid (such as a cube) and we wish to make it useful, what functionality should be included? To answer this question, we must determine who might be interested in using the class. Typical candidates are:
Each particular view or application will need its own particular set of attributes and operations which need to be 'attached' to the geometric objects which we are interested in. For example, in a virtual reality application we may wish to attach acoustic, lighting and material properties to an object while a CAD software developer may need to have bill-of-material (BOM) data coupled with the object. Finally, a mathematician may not need any extra functionality whatsoever. Other examples of where the Decorator pattern can be used are:
Given the constraint that we do not have to pay for what we do not use, how do we organise our classes in such a way that they can be extended and contracted so that each application only gets what it needs? The answer is the Decorator pattern. It provides an alternative to creating derived classes in order to extend functionality. Furthermore, it adds great flexibility to an application since properties can be added and removed at run-time.
A common theme when developing software in general and C++ class libraries in particular is how to extend the software to suit new customer needs. In this sense we must consider the problem of adding new essential operations to the classes in a class hierarchy without changing the original classes. For example, suppose that a software vendor has sold you a C++ class library which satisfies 90% of your needs but you do not have access to the source code. One way to add the extra 10% is to create derived classes and adding the needed functionality. The main problems in this case are:
In short, this approach draws no distinction between the different types of
extensions to classes that are needed and hence results in customers getting
chunks of code which are not relevant to the current situation.
The approach that we take when developing C++ classes is to decide on the basic services which each class provides to all possible customers in a given domain. In two and three dimensional computer graphics applications this amounts to providing basic geometric functionality, such as:
We thus start with a set of 'lean and mean' classes whose functionality can
then be extended by using the Visitor pattern. Typical extensions are:
// Two-dimensional and three-dimensional applications
Adding operations for geometric transformations (rotate, scale, skew, ...)
New operations for 'advanced' geometric properties, such as centre of gravity and volume
Interference and collision detection
Overlaying, blending and composing images
Geometric transformations on images
Applying filtering to images to produce false colours (see [Foley87])
In order to add new functionality to a class hierarchy we recommend the use of the Visitor pattern. This has the advantage of allowing us to define a new operation in a class without having to change the class interface. Furthermore, its use results in a flexible solution to the problem of delivering different functionality to different customers; only that functionality which is needed and paid for will be provided. In fact, we can package related operations from each class in a separate visitor object. In this way we can create reusable components for different needs, such as:
The Visitor Pattern is probably one of the most widely used and powerful patterns around and the author has applied it in many cases. It offers unlimited extensibility. It is similar to the Decorator pattern except that the Visitor represents new functionality which is long-term while the Decorator pattern is used when we wish to add 'temporary' functionality to an object.
The Visitor pattern is very useful when we wish to create different types of 'views' of an object. In order to explain what a view is we need to be aware that each instance of a class has one so-called canonical representation, usually in terms of numerical data. For example, a spreadsheet object may be represented internally as a list of numbers. This internal representation is usually not directly of interest to clients. Instead, they wish to see the data represented in some other form, for example as a pie-chart, histogram or a matrix of cell values. These latter representations are views and as such are not part of the class interface of a given class. A Visitor needs to be made for each type of view.
Examples of views are:
// Two-dimensional and three-dimensional applications
Displaying graphical objects in X, OpenGL and GDI output forms
Saving geometric objects to a database
Representing objects in different forms
Halftoning techniques ([Gems95])
Embossing on raster image date
Display an image on a display device (see for example [Kofax94])
Scanning an image
This application of the Visitor pattern is very important in applications where different users have different views of an objects. Not only does this pattern ensure portability between different software and hardware platforms but it also avoids data redundancy because there is only one internal representation of an object and all others are derivative and are constructed using a visitor.
In many applications we must often take into account the presence of so-called simple and composite objects. In particular, we often need to create so-called recursive aggregate objects. By definition, an aggregate object is one which may consist of other objects of the same type. Examples crop up all over the place and for this reason the Composite pattern was born since learning how to apply it in one situation ensures that we can discover its applicability in new applications. In general, composite objects may consist of simple objects and other composites; some examples are:
The advantages of using a composite are:
We have applied this pattern in 2d CAD software development work when working with objects which need to be accessed in different ways. In some situations we wish to access groups of objects sequentially, randomly or based on some search criterion. For each case we chose a special composite based on an internal data structure. For example, composites can be built using the following data structures:
Thus, we can choose our composite according to space and time restrictions.
We have discussed a number of design patterns which are used when creating computer graphics applications. There are many more in practice and it is our intention to introduce more of them as we progress in this series. However, we mention some important ones at this stage in order to get you acquainted with them as soon as possible. The patterns are:
The Strategy pattern is very important for computer graphics applications. It provides us with the ability to define different algorithms to solve a given problem and make these algorithms interchangeable at run-time. The reasons for using this pattern can be:
In many applications we not know the best algorithms to choose and in these cases it must be possible to 'switch' between the various choices This is made possible by the use of the Strategy pattern.
The main use of the Bridge pattern is to be able to define a class interface without having to worry about the underlying implementation. For example, it is possible to define a class which represents a radio button in a GUI application. The class can be developed without any reference to the specific graphical environment. In other words, the interface and its implementation are decoupled and in this way it is possible to change implementation at run-time. For example, the class for radio button could be implemented in both X and Windows and only a pointer-change is needed. Another application of the Bridge pattern is when we load and save images; an abstract load operation can be implemented in different ways, depending on how the image was originally stored.
The Facade pattern is very important when creating C++ class libraries with very many classes. Once the number of classes in such libraries reaches the 100 level these libraries become very difficult to maintain and to understand unless some layering technique is used. To this end, the Facade pattern allows us to replace a set of interfaces by one simpler interface. This is similar to the way that systems are partitioned into layered subsystems so that applications become easier to develop and maintain (see [Duffy95]).
The Observer patterns has many applications in computer graphics software. It represents the case when there is a one-to-many dependency between a so-called subject and a number of observers. When the subject's state change all the observers must be notified of the change so that they can be updated in some way. This is a very important pattern and some cases are:
More generally, the Observer pattern can be used to create constraint-management systems using a Change Manager class. For example, it is possible to define relationships between objects which must be satisfied at all times. Such applications are important when implementing CAD design rules, collision detection rules in virtual reality applications and geometric constraints (parallel, perpendicular, concentric).
These patterns will be fully expanded in the forthcoming articles.
Design patterns dovetail well with C++ class library construction because applying the appropriate pattern consistently results in code which is extendible, maintainable and reusable. This applies specifically to designing classes which are useful in a given domain (for example, engineering) or application area (for example, imaging).
There are other advantages associated with using design patterns (see [Schmidt95]) which have direct impact on class library construction. Some of them are:
In short, design patterns promote good software practice, communication and in the long term add to the major effort of creating business and enterprise-wide objects.
One of the pitfalls is that design patterns can become an end in themselves. Although they are extremely useful and powerful not all problems should be cast in terms of patterns. This pattern overload situation can be avoided by looking at the problem and deciding how to proceed. The main thing to remember is that patterns should be strategic to a given domain and they should reuse existing tactical patterns. In this way we can set up pattern catalogues and 'mega-patterns'. These ends can be achieved by designing problems using top-down approaches. We shall give some examples of these in future articles.
This is the first article in a series in which design patterns are explained and applied to a number of problems which are related to computer graphics. Our interest is in using these patterns to create C++ class libraries which are context-independent and which can be easily configured to work in a number of hardware, software and application environments. The areas of concern for us were CAD, three-dimensional graphics and imaging.
Daniel J. Duffy is director and founder of Datasim BV, Amsterdam, a training
and software development organisation which has been involved with OOT since
1988. He is chief designer of CADObject, a class library consisting of 600 C++
classes which is used to build applications for CAD, computer graphics,
engineering and simulated worlds. He has been involved in many types of OO
projects in finance, CAD, logistics and engineering . He is convinced that
software development should be based on a building block or factory foundation
and that new applications and prototypes should be built using ideas,
architectures and designs which have been applied from previous projects. Duffy
has a Ph.d. in numerical mathematics from Trinity College, Dublin.
Daniel Duffy can be reached on the e-mail address firstname.lastname@example.org
[ Homepage | Articles]