Design guidelines

Instructor's Guide


intro, methods, objects, contracts, formal, summary, Q/A, literature
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.

Structural versus behavioral encapsulation

Object-oriented modeling has clearly been inspired by or, to be more careful, shows significant similarity to the method of semantic modeling that has become popular for developing information systems. In an amusing paper,  [Ki89] discusses how semantic modeling and object-oriented modeling are related. Apart from a difference in terminology, semantic modeling differs from object-oriented modeling primarily by its focus on structural aspects, whereas object-oriented modeling is more concerned with behavioral aspects, as characterized by the notion of responsibilities.

Structural versus behavioral \c{encapsulation}

Semantic modeling {\em -- constructing types}


slide: Semantic modeling

Typically, semantic modeling techniques provide a richer repertoire for constructing types, including a variety of methods for aggregation and a notion of grouping by association. See slide 3-semantic. The object-oriented counterpart of aggregation may be characterized as the has-a or part-of relation, that is usually expressed by including the (part) object as a data member. Associations between objects cannot be expressed directly in an object-oriented framework. On an implementation level, the association relation corresponds to membership of a common collection, or being stored in the same container. However, the absence of an explicit association relation makes it hard to express general m-n relations, as, for example, the relation between students and courses.

Object-oriented modeling

  • is-a -- inheritance
  • has-a, uses -- delegation
  • uses -- templates

Challenges

  • {\it Clearly, the trend is for database researchers to view object-oriented models as having both structural and behavioral encapsulation facilities}

slide: Relations between objects

The influence of a semantic modeling background can be clearly felt in the OMT method. The object model of OMT is a rather direct generalization of the entity-relationship model. Entities in the entity-relationship model may only contain (non-object) data members, which are called attributes. In contrast, objects (in the more general sense) usually hide object and non-object data members, and instead provide a method interface. Moreover, object-oriented modeling focuses on behavioral properties, whereas semantic modeling has been more concerned with (non-behavioral) data types and (in the presence of inheritance) data subtypes. Relations, as may be expressed in the entity-relationship model, can partly be expressed directly in terms of the mechanisms supported by object-oriented languages. For instance, the is-a relation corresponds closely (although not completely) with the inheritance relation. See slide 3-challenges. Both the has-a and uses relation is usually implemented by including (a pointer to) an object as a data member. Another important relation is the is-like relation, which may exist between objects that are neither related by the inheritance relation nor by the subtype relation, but yet have a similar interface and hence may be regarded as being of analogous types. The is-like relation may be enforced by parametrized types that require the presence of particular methods, such as a compare operator in the case of a generic list supporting a sort method. Due, partially, to what may be regarded as the natural dynamics of research (which includes financing), there seems to be a trend of convergence between the interests of the object-oriented community and the proponents of semantic modeling. The main challenge, from the perspective of object-oriented programming, is to provide efficient support for persistent objects and (explicit) support for aggregation and association relations. Within the community of database researchers, an interesting debate is going on about whether to extend the relational model with object-oriented notions or whether to start anew from fresh premises. The main challenge, from the perspective of database management systems, is clearly to provide efficient support for the behavioral aspects of objects. One approach is to extend the object model of object-oriented programming languages with persistence as proposed by the Object Database Management Group (ODMG) in  [Cattell94]. See section ODMG. To meet the demands of future applications (featuring graphics and animation), support for persistence should include active objects, having activity of their own, as well. From the perspective of object-oriented modeling, the main issue is to combine both structural and behavioral aspects in a coherent way.

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

  • abstract system = abstract data types + protocol

Events -- high level glue

  • realization of the interaction protocol

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