User Interface Design Issues for Computer Aided Design (CAD) and Computer Graphics Applications: Emerging Patterns

 

Summary

This article presents and discusses a number of software design techniques which are applied to creating highly-interactive graphics applications. In particular, we describe how suitable architectures can be set up for such applications, what is needed in order to implement these architectures and how applications can be easily modified to suit new environments.
As a test case, we discuss how to create design patterns in order to ensure that an application's user interface is weakly coupled to application objects. Although this article focuses specifically on CAD, the underlying principles are useful in any application area in which both complex graphical objects and user interaction play a major role.

 

Introduction and Background

This article discusses how object-oriented technology (OOT) is applied to creating applications in which two-dimensional and three-dimensional graphics objects are created, modified, viewed and manipulated by a user. In order to restrict the scope of this article, we focus on describing how OOT can be used to create flexible, portable and reusable solutions to problems in Computer Aided Design (CAD). In particular, we adopt a special building-block approach in which a given application is assembled from ready-made components. Each component is essentially a subsystem consisting of several classes and a given subsystem has well-defined input and output ports so that it can be linked up to other subsystems. A given application is constructed by combining a number of subsystems.
This approach is not new. In fact, solving a problem by partitioning it into smaller independent pieces has been applied for a number of years (see [Parnas72]). Unfortunately, however most of the present-day CAD systems tend to be monolithic programs which are either difficult to extend or to modify to new hardware and software environments. Furthermore, it is rarely possible to use subsets of the functionality of these packages. This means first of all, that the full product must be purchased and secondly that users are exposed to functionality that they do not need, thus prolonging the learning period for the CAD product. A number of major CAD vendors (such as Autodesk, Intergraph and Bentley systems) are moving to component-based solutions and we see this trend continuing well into the next century as vendors and users learn how to work with the new object oriented technology and apply it effectively in real-life applications. As with many application areas, the CAD market is embracing OOT and we see a bright future for software companies which can produce software components which fit in seamlessly with other vendors' components. The era of the monolithic CAD packages is nearing its end, mainly because such packages are unable to adapt to changing market and technology pressures. To this end, we adopt the stance that the only way to proceed is to create portable and extendible components which can be customised to suit special customers and platforms and which can be combined to create flexible solutions for the ever-changing marketplace.

The rest of this article will be devoted to showing how these laudable objectives can be realised by first showing how a given problem can be partitioned into independent subject areas or subsystems and then showing how parts of each subsystem can be designed using well-known design pattern technology (see [Gamma95]). In particular, we concentrate on user-interface design issues. Other important problems, such as object oriented analysis and design will not be discussed (for more information, see [Duffy95] and [Rumbaugh91]).

The organisation of the rest of this article is as follows: first, we give an introduction to how computer graphics applications are partitioned into smaller subproblems so that each subproblem can be separately designed and implemented. We then show how the combination of user requirements and architectural partitioning techniques are mapped to well-known design patterns. Finally, we discuss how three design patterns (namely, Visitor, Bridge and Composite) are applied as powerful tools in user interface design for graphics applications. We conclude the article with some personal predictions on the future of CAD and graphics software endeavours.

 

Software Architecture for Graphics Applications

There is a growing awareness in the software industry that a number of current object oriented methods are not suited to solving large problems or do not scale up very well when new requirements are introduced. One of the reasons for this state of affairs is that many OO methods tend to view a problem as a flat network of classes and objects (the OMT method being a good example) and this leads to systems which are difficult to maintain and to extend. In particular, separating volatile elements from those which do not change during the design phase can be difficult. The main problem is that changes in one part of the system can influence other parts of the system; in short, this approach leads to information hiding problems (see [Parnas83]). Traditional CAD systems have suffered from this situation. It is very difficult (and sometimes even impossible) to separate or replace logically related functionality. In order to promote flexibility we need some way of separating those parts of a system which can change (for example, the user interface) from those parts which remain stable. To take a simple example, we could consider creating a C++ class interface for a circle abstraction. The interface consists of functions for creating circles, finding intersections of circles and other simple geometric shapes and determining circle properties. However, this class has no user interface and furthermore, circle instances cannot save themselves to disk because both of these features can be implemented in different ways. For example, a user may wish to interactively create circles in a Windows environment while another may not need any interface but is happy with the interface functions provided by the circle class. Finally, some users may wish to store circles in an object-oriented database while others may wish to store such objects in proprietary formats, such as AutoCAD database representation.
The above example was taken in order to convince the reader that it is important to separate I/O aspects of a problem from essential application design and code. This has been achieved in languages such as Smalltalk by the use of the well-known MVC (Model View Controller) paradigm but this is restricted to the implementation phase of the software lifecycle and is too limited for our purposes. Instead, we need some way of partitioning a given problem in the analysis phase into a number of independent and co-operating components and to this end we use a variation of the so-called ANSI/SPARC model (see [Tsichritzis78]). This is a three-layer model and it consists of three levels, namely:

 

The internal layer is responsible for processing the raw data in a system. This typically involves reading data from disk, carrying out pre-processing activities and generally preparing information so that it can be delivered to the next layer. The external layer is responsible for all I/O activities and in this sense it is responsible for all presentation activities. In particular, this layer is responsible for displaying information and data in a form that is understandable to clients (whether they be real users of other software systems). We say that the external layer presents different views of data to clients. Furthermore, this is the layer where user control input takes place; for example, it is here where we create user-interface screens and forms. Finally, the conceptual layer can be seen as the mediator between the internal and external layers. It harmonises the user expectations of the external layer with the incompatible data formats of the internal layer. This is the layer which contains the 'intelligence' in a system and it is here that we see core business competence such as reusable classes, business rules and algorithms being implemented.
We have found that many problems can be posed or formulated as a three-layer model and it can be applied in all phases of the software lifecycle. For example, during requirements analysis we can partition system goals into a number of subgoals, each one of which can be mapped to one of the above three layers. During design (as we shall see in the next section) we can map the ANSI/SPARC model to well-known design patterns. This important feature allows us to choose that design pattern which is most suited to the problem in question rather than trying to use a given design pattern in a situation which we do not fully understand. In this way we are assured that the correct pattern is being used for the current problem.

 

Design Patterns and Computer Graphics Applications

We have applied the three-layer model to a number of problems in mechanical engineering, imaging and visualisation applications and a number of recurring themes have been noted. It is not possible to discuss all of these here; instead, we concentrate on a number of issues which are related to user interface design in a given problem. In other words, we concentrate on creating flexible software for the external layer in graphical applications.
To this end, we discuss in some detail the following action points:

 

These are the two main areas of concern in the external layer and it is our goal to be able to create a design and implementation so that it is possible to switch from one input-output regime to another one without having to introduce major changes to the software. To this end, we shall use a number of well-documented design patterns which are fully discussed in [Gamma95]. In particular, we restrict ourselves to the following patterns:

 

These patterns occur so often in graphics and CAD applications that once you know how to apply them in one situation it becomes even easier to use them in many other cases. Furthermore, design awareness is enhanced when we use these patterns because new problems can be formulated in such a way that they can be mapped to some pattern.
How do the above three patterns relate to the ANSI/SPARC model? More specifically, which layers can benefit from using these design patterns? The Visitor and Bridge patterns are directly related to the external layer because they solve a number of user-interface problems. In particular, the Visitor pattern allows objects to be displayed in different formats while the Bridge pattern allows us to create flexible user interface control objects by separating their abstract behaviour from how that behaviour is implemented. The Composite pattern, on the other hand can be used in all three layers of the ANSI/SPARC. We shall give some examples of these when we come to discuss each pattern in more detail.

 

The Visitor Pattern

The main strength of the Visitor pattern lies in its ability to separate the essential properties and functions of a class from non-essential features. What is deemed 'essential' or 'non-essential' depends on the context but for the purposes of this article we consider essential behaviour as constituting the following features:

 

For example, consider the problem of creating an interface for a Circle abstraction. In this case there are many different ways to create circles and this fact should be reflected in a number of corresponding constructor methods. Furthermore, we must have methods for a number of circle attributes such as centre point and radius because this information is needed by client software. Finally, the Circle class must have enough functionality for geometric relationships (such as intersections with other shapes, proximity tests and so on) so that it can be used a basis for application development. In short, the above three function categories constitute the essential behaviour of the Circle class. Similar functionality must also be created for other shapes.
We now discuss non-essential class behaviour. This can take two forms, namely:

 

Both of these activities are addressed by the Visitor pattern. For more information on how to achieve this in practice, we refer to [Gamma95]. In this article we discuss the second problem since this is directly concerned with user interface problems. In this case we use the Visitor pattern to display objects in different ways and formats. This is an extremely important issue in CAD because of its highly visual nature. In particular, different user groups need to visualise the same data in different ways and a given user group may wish to visualise information from different viewpoints at various stages in the life of a CAD project. In other words, only certain functionality is needed at any given moment in time and for this reason we need some way of separating this from the essential functionality. The answer is the Visitor pattern and in order to motivate it we give an example which shows what can happen if we include non-essential behaviour in a class interface. More specifically, the following text shows what the consequences are of not separating an application from its I/O functionality.

One of the main shortcomings when C++ is applied to real-life problems is that developers often fail to separate essential object functionality from that functionality which is changing and volatile. For example, the much-used (incorrect) example

class SHAPE
{ // Abstract base class for 2d shapes for CAD. Other shapes are derived from this class.
public:

// ...
virtual void draw() = 0;

};

 

which is part of the class interface for two-dimensional shapes (such as circle and line) is incorrect because the capability of drawing does not belong to their responsibilities. In this sense we see objects as simple entities having limited intelligence. In this case shapes are responsible for geometrical functionality and nothing else.
The main problem with the above class interface is that the function draw() only works in one particular environment, for example a CAD package. If we wish to port the software so that it works in other environments we have to rewrite the body of this function. This problem can be avoided by first realising that drawing capabilities do not belong to shapes and secondly that this type of functionality actually represents a 'view' of shapes. There are many views for any given object and this is why it is important to externalise this type of information outside the object itself. In this way client software can create new views without distorting the shape's class interface. This state of affairs can be achieved by applying a variation of the Model View Controller (MVC) in C++ by the use of the so-called multiple polymorphism principle (see [Duffy95], [Gamma95]). Instead of creating a draw() function in the shape class we create an abstract 'driver' class which can draw shapes and which has concrete derived classes for vendor-specific environments. In this way clients can add their own drivers without having to have access to the source code of the shape hierarchy.

In order to achieve flexibility, we create a so-called Visitor pattern in which the draw() function has been placed in a new hierarchy which communicates in a certain way (see [Gamma95]) with our SHAPE hierarchy above. In essence, we create 'driver' objects which take shapes as parameters and display them in different formats. The display facility in the form of the draw() function is very flexible because of the run-time polymorphic behaviour which C++ provides. The interface for the top-level Visitor class is:

class SHAPEVISITOR
{
public:

// Drivers here
virtual void draw(const Circle& c)
const = 0;

};

 

Derived classes must then redefine the draw() function for each type of shape. This means that the code body for draw() must be created. For example, the body of the draw() function for AutoCAD entails creating a wrapper and using the functionality of the ADS C- library provide by AutoCAD (we see that this is one way of re-engineering legacy code which has been written in the C language).
To summarise, the Visitor pattern is used to output objects in different representations and in this sense can be seen as playing its role between the conceptual and external layers of the ANSI/SPARC model. The main advantages lie in added flexibility and extendibility. In particular, we have used it for the following scenarios:

 

The Bridge Pattern

This is a general pattern. It is used to separate abstract behaviour from its implementation. It is not just restricted to GUI problems but it can be applied to other systems in which different implementations are possible for a given abstract behaviour. For example, it is a very useful design artefact when designing real-time software which uses different types of hardware components from different vendors.
Applying the Bridge pattern to user interface design entails creating a number of class hierarchies for both the abstract objects and their implementation. Roughly speaking, the abstract objects belong to the conceptual layer and the implementation objects belong to the external layer in the ANSI/SPARC model. To this end, we construct an abstract control hierarchy with derived classes for all the common control types, such as buttons, text boxes and so on. The hierarchy also contains classes for creating so-called composites but these will be discussed in the next section.
The abstract control class hierarchy interface is as follows:

class DSCONTROL
{ // Sanitised version, production version contains much more
private:

DSCONTROLIMP *imp; // Pointer to the implementor class. More and other state info here.

public:

DSCONTROL(); // Default constructor
DSCONTROL(const DSCONTROL& control); // Copy constructor
virtual ~DSCONTROL(); // Destructor

//Accessing functions
double width()const; // What is the width
double height() const; // What is the height
DSBOOLEAN fixedwidth() const; // Is the width fixed
DSBOOLEAN fixedheight() const; // Is it a fixed height
DSDCLTYPES::DSALIGNMENT alignment()const; // Return the alignment
DSDYNSTR mnemonic() const; // Return the mnemonics key
DSDYNSTR label() const; // Return the label
DSDYNSTR value() const; // Return the initial value
DSDYNSTR key() const; // Return the key virtual
DSCONTROLIMP* implementor() const; // Return the implementor

//Modifier functions
virtual void implementor(DSCONTROLIMP& imp); // Set the implementor

// More and other modifiers here

};

 

One now defines derived classes from DSCONTROL based on the most common controls in existence (in some ways our C++ interface looks similar to how Java has implemented these classes). For example, the interface for a button class would be:

class DSBUTTON : public DSCONTROL
{
private:

DSBOOLEAN can; // Button is activated when cancel key is pressed
DSBOOLEAN def; // Button is activated when accept key is pressed

public:

DSBUTTON();
DSBUTTON(const DSDYNSTR& nkey, const DSDYNSTR& nlabel);
DSBUTTON(const DSDYNSTR& nkey, const DSDYNSTR& nlabel, const DSBOOLEAN& canc, const DSBOOLEAN& defaul);
DSBUTTON(const DSBUTTON& button); // Copy constructor virtual ~DSBUTTON(); // Destructor

// Accessing functions
DSBOOLEAN iscancel() const; // Is button the cancel button
DSBOOLEAN isdefault() const; // Is button the default button

//Modifier functions
void iscancel(const DSBOOLEAN& ncancel); // Set the is_cancel
void isdefault(const DSBOOLEAN& ndef); // Set the is_default
void Enable(); // Enable button
void Disable(); // Disable button (grey-out)
void SetFocus(); // Set focus to button

};

 

This button class provides some of the basic functionality that we see in Visual Basic, for example. In this case the button can be programmed to function in such a way that the button is activated when an enter or cancel key is pressed.
The full class hierarchy is given in the following diagram. The diagram shows only the implementation classes for the DSBUTTON class.

Class diagram

We thus see that DSCONTROL contains functionality common to all types of controls and that it contains a pointer to its top-level implementation class DSCONTROLIMP whose interface is given by:

class DSCONTROLIMP
{ // This class is just a clearing house for specific control-related implementations

private:

// Empty state

public:

DSCONTROLIMP();
DSCONTROLIMP(const DSCONTROLIMP& buttonimp);
virtual ~DSCONTROLIMP();

DSCONTROLIMP& operator=(const DSCONTROLIMP& bx);
virtual void Enable(const DSDYNSTR& key)=0;
virtual void Disable(const DSDYNSTR& key)=0;
virtual void SetFocus(const DSDYNSTR& key)=0;

};

 

For each type of control in the abstract hierarchy we create an abstract implementation class. In the case of a button class this interface is:

class DSBUTTONIMP : public DSCONTROLIMP
{
private:

public:

DSBUTTONIMP();
DSBUTTONIMP(const DSBUTTONIMP& buttonimp);
virtual ~DSBUTTONIMP();
DSBUTTONIMP& operator=(const DSBUTTONIMP& bx);

virtual void Enable(const DSDYNSTR& key)=0;
virtual void Disable(const DSDYNSTR& key)=0;
virtual void SetFocus(const DSDYNSTR& key)=0;

};

 

This is of course an abstract base class and from this we define derived classes for various graphics packages, such as AutoCAD. In this case the interface is:

class DSACADBUTTONIMP : public DSBUTTONIMP
{

private:

DSDCL* dclpt; // Pointer to DCL driver
DSACADBUTTONIMP(); // Default constructor. Not allowed

public:

DSACADBUTTONIMP(DSDCL*);
DSACADBUTTONIMP(const DSACADBUTTONIMP&buttonimp);
virtual ~DSACADBUTTONIMP();
DSACADBUTTONIMP& operator=(const DSACADBUTTONIMP& bx);

void installcallback(const DSBUTTON& button, CLIENTFUNC function);
void Enable(const DSDYNSTR& key); // Enable button
void Disable(const DSDYNSTR& key); // Disable button
void SetFocus(const DSDYNSTR& key); // Set focus to button

};

 

The code for implementing this class demands knowledge of the AutoCAD ADS library. However, only three functions need to be created and this approach allows us to switch from one type of driver to another one. The following example shows how this is done:

DSACADBUTTONIMP AcadButtonImp; // Create AutoCAD implementor
DSXXXBUTTONIMP XxxButtonImp; // Create other implementor

...

DSBUTTON OkButton("accept", "OK"); // Create button with label OK
OkButton.implementor(&AcadButtonImp); // Assign the Acad implementor to the button

... // Use button in AutoCad...

OkButton.implementor(&XxxButtonImp); // Assign another implementor to the button

... // Use button somewhere else

 

The Composite Pattern

A regular occurrence during object oriented design is when we need to create nested objects or objects which contain objects of the same class. These are also sometimes called recursive aggregates and they occur in very many applications. Our interest in this article is due to the fact that many of the interface objects (such as dialog boxes) are essentially recursive aggregates and thus should be designed as composites. For example, a dialog box usually consists of text, labels and other controls but it may contain other dialog boxes.

We can consider a composite object as consisting of a collection of controls with functionality for adding (and possibly deleting) controls to the list. In this case it suffices to implement the collection as a sequential list but for other applications we could possibly need other data structures such as arrays or multi-dimensional search trees. The class interface for the class representing composite controls is:

class DSCONTROLLIST : public DSCONTROL
{

private:

DSGINDEXLIST<DSCONTROL*> lis; // Implement as a linked list

public:

// Constructors
DSCONTROLLIST(); // Default constructor with empty controllist
DSCONTROLLIST(const DSCONTROLLIST& sl); // Copy constructor

// Destructor
virtual ~DSCONTROLLIST(); // Remove controllist and objects

//Accessing functions
DSBOOLEAN empty() const; // Is the list empty?

//Modifier Functions
void clear(); // Delete the list and its elements

// Append functions
void append(const DSCONTROL& s); // Add pointer at the end of list

};

 

Modelling objects as composites results in clean and understandable code because no distinction is made between simple controls (such as text boxes) and recursive objects (such as a radiocluster which contains multiple radiobuttons). In particular, since a control composite is also a control it behaves in the same way as a 'normal' control object. In this way we are assured of uniform behaviour.

 

On-going Efforts and Future Developments

The authors have been applying OOT in the CAD market since 1988. We have been involved in training, consultancy and software development for CAD applications and we have noticed a gradual shift in direction in the way both vendors and users of CAD systems view software. It is our feeling that graphics applications will evolve to a stage where the full power of object technology can be unleashed to provide flexible solutions for this niche market. We are already seeing some signs of this already, for example in the construction industry where the Industry Foundation Classes (IFCs) movement is gaining momentum as a standard between different CAD applications. Finally, we see a lot of hope in Microsoft's COM interoperability model and it would seem that the major CAD vendors have chosen this option for their next-generation products.

 

Conclusions

We have presented a number of architectural and design principles which can be used to construct reusable and flexible solutions for problems in CAD and computer graphics. The emphasis was on showing how object-oriented technology can be successfully used in these application areas.
We welcome your comments on this article.

 

References

  1. [CADObject95] CADObject 2.2 User Reference Manual, Datasim BV Amsterdam, van Diemenstraat 148-150, Netherlands 1995.
  2. [Gamma95] E. Gamma et al Design Patterns, Elements of Reusable Object-Oriented Software, Addison Wesley Reading MA 1995.
  3. [Linton87] M. A. Linton, P. J. Calder "The Design and Implementation of InterViews", USENIX C++ Conference 1987.
  4. [Parnas72] D. Parnas "On the Criteria To Be Used in Decomposing Systems into Modules", Communications of the ACM, December 1972, Volume 15, No. 12.
  5. [Parnas83] D. Parnas et al "Enhancing Reusability with Information Hiding", in Software Reusability, Volume I (T. J. Biggetstaff (ed.)), Addison Wesley, Reading MA 1983.
  6. [Rumbaugh91] J. Rumbaugh et al "Object-Oriented Modeling and Design", Prentice-Hall Englewood Cliffs NJ 1991.
  7. [Tsichritzis78] D. Tsichritzis, A. Klug (eds) "The ANSI/X3/SPARC DBMS framework: report of the study group on database management systems", Information Systems, Volume 3 (173-191), Pergamon Press Oxford UK 1978.

About the Authors

Robert Demming and Daniel Duffy are with Datasim BV, Amsterdam. They can be reached with email: info@datasim.nl.

 

[ Homepage | Articles ]