Design guidelines

Computing is a relatively young discipline. Despite its short history, a number of styles and schools promoting a particular style have emerged. However, in contrast to other disciplines such as the fine arts (including architecture) and musical composition, there is no well-established tradition of what is to be considered as good taste with respect to software design. There is even a debate whether software design must be looked at as an art or must be promoted into a science. See for example  [Knuth92] and  [Gries]. The debate has certainly resulted in new technology but has not, I am afraid, resulted in universally valid design guidelines. The notion of good design in the other disciplines is usually implicitly defined by a collection of examples of good design, as preserved in museums or (art or music) historian works. For software design, we are still a long way from anything like a museum, setting the standards of good design. Nevertheless, a compendium of examples of object oriented applications such as  [Pinson90] and  [Harmon93], if perhaps not setting the standards for good design, may certainly be instructive.

Development process {\em -- cognitive factors}

Design criteria -- natural, flexible, reusable


slide: Criteria for design

The software engineering literature abounds with advice and tools to measure the quality of good design. In slide 3-design-criteria, a number of the criteria commonly found in software engineering texts is listed. In software design, we evidently strive for a high level of abstraction (as enabled by a notion of types and a corresponding notion of contracts), a modular structure with strongly cohesive units (as supported by the class construct), with units interrelated in a precisely defined way (for instance by a client/server or subtype relation). Other desirable properties are, a high degree of information hiding (that is narrowly defined and yet complete interfaces), and a low level of complexity (which may be achieved with units that have only weak coupling, as supported by the client/server model). An impressive list, indeed. In practice, however, we see that (object oriented) software design is more a matter of give and take. Design is a human process, in which cognitive factors play a critical role. The role of cognitive factors is reflected in the so-called fractal design process model introduced in  [JF88], which describes object oriented development as a triangle with bases labeled by the phrases model, realize and refine. This triangle may be iterated at each of the bases, and so on. The iterative view of software development does justice to the importance of human understanding, since it allows for a simultaneous understanding of the problem domain and the mechanisms needed to model the domain and the system architecture. Good design involves taste. My personal definition of good design would certainly also involve cognitive factors {\em -- is the design understandable --}, including subjective criteria such as {\em -- is it pleasant to read or study the design --}. But rather than expressing these criteria explicitly, I would like to point at a collection of examples of good (object oriented) software design. Lacking such a collection, we will instead discuss some of the issues that may arise in developing a design. First, we will look at the distinction between structural and behavioral encapsulation (which rests primarily on a difference in background rather than goals). Next, we will introduce the notion of abstract systems and we will present a methodology based on event-driven programming to employ abstract systems in modeling interactions between objects. And, we will conclude this section by discussing the tradeoffs involved in designing for reuse and individual class design. We will present an example illustrating the tradeoffs of using either ones of the mechanisms of inheritance, delegation and templates in developing (user-defined) abstract data types, and we will give some guidelines for detailed class design.

Abstract systems and events

User actions may require complex interactions between the objects constituting the object model of a system. Such interactions are often of an ad hoc character in the sense that they embody one of the many possible ways in which the functionality of objects may be used. What we need is a methodology or paradigm that allows us to express these interactions in a concise yet pragmatically amenable way. In  [Henderson93], a notion of abstract systems is introduced that seems to meet our needs to a large extent. See slide 3-abstract. Abstract systems extend the notion of abstract data types to capture the (possible) interactions between collections of objects.

Abstract systems -- design methodology

Events -- high level glue


slide: Abstract systems and events

The idea underlying the notion of an abstract system is to collect the commands available for the client or user of the system. The collection of commands comprising an abstract system are usually a (strict) subset of the commands available in the combined interface of the abstract data types involved. In other words, an abstract system provides a restricted interface, restricted to safeguard the user from breaking the protocol of interaction implicitly defined by the collection of abstract data types of which the system consists. An abstract system in itself merely provides a guideline on how a collection of objects is to be used, but does not offer a formal means to check whether a user plays by the rules. After presenting an example of an abstract system, we will look at how events may be used to protect the user against breaking the (implicit) laws governing the interaction.

Example -- the library

The abstract system comprising a library may be characterized as in slide 3-library. In essence, it provides an exemplary interface, that is, it lists the statements that are typically used by a client of the library software. We use typical identifiers to denote objects of the various types involved.

Abstract system {\em -- exemplary interface}

\zline{\fbox{\fbox{library}}}
  p = new person();
  b = new book();
  p = b->borrower;
  s = p->books;
  tf = b->inlibrary();
  b->borrow(p);
  p->allocate(b);
  p->deallocate(b);
  b->_return(p);
  

slide: The library system

The commands available to the user of the library software are constructors for a person and a book, an instruction to get access to the borrower of a particular book, an instruction to ask what books a particular person has borrowed, an instruction to query whether a particular book is in the library, and instructions for a person to borrow or return a book. To realize the abstract system library, we evidently need the classes book and person. The class book may be defined as in slide 3-book.
  class book { 
\fbox{book}
public: person* borrower; book() {} void borrow( person* p ) { borrower = p; } void _return( person* p ) { borrower = 0; } bool inlibrary() { return !borrower; } };

slide: The book class

It consists of a constructor, functions to borrow and return a book, a function to test whether the book is in the library and an instance variable containing the borrower of the book. Naturally, the class book may be improved with respect to encapsulation (by providing a method to access the borrower) and may further be extended to store additional information, such as the title and publisher of the book.
  class person { 
\fbox{person}
public: person() { books = new set(); } void allocate( book* b ) { books->insert(b); } void deallocate( book* b ) { books->remove(b); } set* books; };

slide: The person class

The next class involved in the library system is the class person, which is shown in slide 3-person. The class person offers a constructor, an instance variable to store the set of books borrowed by the person and the functions allocate and deallocate to respectively insert and remove the books from the person's collection. A typical example of using the library system is given below.
  book* Stroustrup = new book();
  book* ChandyMisra = new book();
  book* Smalltalk80 = new book();
  
  person* Hans = new person();
  person* Cees = new person();
  
  Stroustrup->borrow(Hans);
  Hans->allocate(Stroustrup);
  ChandyMisra->borrow(Cees);
  Cees->allocate(ChandyMisra);
  Smalltalk80->borrow(Cees);
  Cees->allocate(Smalltalk80);
  
First, a number of books are defined, then a number of persons, and finally (some of) the books that are borrowed by (some of) the persons. Note that lending a book involves both the invocation of book::borrow and person::allocate. This could easily be simplified by extending the function book::borrow and book::_return with the statements p->allocate(this) and p->deallocate(this) respectively. However, I would rather take the opportunity to illustrate the use of events, providing a generic solution to the interaction problem noted. }

Events

 [Henderson93] introduces events as a means by which to control the complexity of relating a user interface to the functionality provided by the classes comprising the library system. The idea underlying the use of events is that for every kind of interaction with the user a specific event class is defined that captures the details of the interaction between the user and the various object classes. Abstractly, we may define an event as an entity with only two significant moments in its life-span, the moment of its creation (and initialization) and the moment of its activation (that is when it actually happens). As a class we may define an event as in slide 3-event.
  class Event { 
\fbox{\fbox{Event}}
public: virtual void operator()() = 0; };

slide: The Event class

The class Event is an abstract class, since the application operator that may be used to activate the event is defined as zero.
  class Borrow : public Event { 
\c{\fbox{Borrow}}
public: Borrow( person* _p, book* _b ) { _b = b; _p = p; } void operator()() { require( _b && _p );
\c{// _b and _p exist}
_b->borrow(p); _p->allocate(b); } private: person* _p; book* _b; };

slide: The Borrow event class

For the library system defined above we may conceive of two actual events (that is, possible refinements of the Event class), namely a Borrow event and a Return event. See slides sli-3-borrow and sli-3-return. The Borrow event class provides a controlled way in which to effect the borrowing of a book. In a similar way, a Return event class may be defined as in slide 3-return.
  class Return : public Event { 
\c{\fbox{Return}}
public: Return( person* _p, book* _b ) { _b = b; _p = p; } void operator()() { require( _b && _p ); _b->_return(p); _p->deallocate(b); } private: person* _p; book* _b; };

slide: The Return event class

The operation Has specified in the previous section has an immediate counterpart in the person::books data member and need not be implemented by a separate event. Events are primarily used as intermediate between the user (interface) and the objects comprising the library system. For the application at hand, using events may seem to be somewhat of an overkill. However, as we will further illustrate in section events, events not only give a precise characterization of the interactions involved but, equally as important, allow for extending the repertoire of interactions without disrupting the structure of the application simply by introducing additional event types.

Designing for reuse

Class design

We have nearly completed a first tour around the various landmarks of object oriented design. Identifying objects, expressing the interaction between objects by means of client/server contracts and describing the collaboration between objects in terms of behavioral compositions, belong to a craft that will only be learned in the practice of developing real systems. Ideally, the design document should present a complete and formal description of the structural, functional and dynamic aspects of the system, including an argument showing that the various models are consistent. However, in practice, this will seldom be realized. Partly, because object oriented design techniques are as yet not sufficiently matured to allow a completely formal treatment and partly because most designers will be satisfied with a non-formal rendering of the architecture of their system. Admittedly, the task of designing is already sufficiently complex, even without the additional complexity of a completely formal treatment. Nevertheless, studying the formal underpinnings of object oriented modeling based on types and polymorphism is still worthwhile, since it will sharpen the intuition with respect to the notion of behavioral conformance and the refinement of contracts, which are both essential for developing reliable object models. And reliability is the key to reuse! } \c{ We will conclude this chapter by looking at some informal, pragmatic guidelines for individual class design. } \nop{

Class design

-- reducing name conflicts
  • faithful model of a single concept
  • a reusable plug-compatible component
  • robust, well-designed
  • integrable, extensible
} \nop{ Above, we have rephrased the goals that should be kept in mind when developing a class. } \c{ Ideally, a class should represent a faithful model of a single concept, and be a reusable, plug-compatible, component, that is robust, well-designed and extensible. In slide 3-individual, we list a number of suggestions put forward in  [McGregor92]. } \slide{3-individual}{Individual class design}{

Class design

-- guidelines
  • only methods public -- information hiding
  • do not expose implementation details
  • public members available to all classes -- strong cohesion
  • as few dependencies as possible -- weak coupling
  • explicit information passing
  • root class should be abstract model -- abstraction
} \c{ The first two guidelines enforce the principle of information hiding, advising to make only methods public and to hide all implementation details. The third guideline states a principle of strong cohesion, by requiring that classes implement a single protocol that is valid for all potential clients. A principle of weak coupling is enforced by requiring a class to have as few dependencies as possible, and to employ explicit information passing using messages instead of inheritance (except when inheritance may be used in a type consistent fashion). When using inheritance, the root class should be an abstract model of its derived classes, whether inheritance is used to realize a partial type or to define a specialization in a conceptual hierarchy. The list given above can be used as a checklist to verify whether a class is well-designed. In section 2-metrics we will explore metrics that capture the guidelines given in a more quantitative manner. Such metrics may be an aid in the software engineering of object oriented systems and may possibly also be used to measure the productivity of object oriented programmers. }