Principles of Object-Oriented Software Development
[] readme course preface 1 2 3 4 5 6 7 8 9 10 11 12 appendix lectures resources

talk show tell print

Guidelines for design

subsections:


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 an on-going and somewhat pointless debate as to 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) historical 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 -- cognitive factors

  • model -> realize -> refine

Design criteria -- natural, flexible, reusable

  • abstraction -- types
  • modularity -- strong cohesion (class)
  • structure -- subtyping
  • information hiding -- narrow interfaces
  • complexity -- weak coupling

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. 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 (is the design understandable?), including subjective criteria such as is it pleasant to read or study the design?

Individual class design

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
class-design, we list a number of suggestions put forward by  [McGregor92].

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

slide: Individual class design

The first two guidelines enforce the principle of information hiding, advising that only methods should be public and all implementation details should be hidden. 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 properties of classes, including their interfaces and relations with other classes, must be laid down in the design document. 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!

Inheritance and invariance

When developing complex systems or class libraries, reliability is of critical importance. As shown in section contracts, assertions provide a means by which to check the runtime consistency of objects. In particular, assertions may be used to check that the requirements for behavioral conformance of derived classes are met.

Invariant properties -- algebraic laws


  class employee { 
employee

public: employee( int n = 0 ) : sal(n) { } employee* salary(int n) { sal = n; return this; } virtual long salary() { return sal; } protected: int sal; };

Invariant


     k == (e->salary(k))->salary() 
  

slide: Invariant properties as algebraic laws

Invariant properties are often conveniently expressed in the form of algebraic laws that must hold for an object. Naturally, when extending a class by inheritance (to define a specialization or refinement) the invariants pertaining to the base class should not be disrupted. Although we cannot give a general guideline to prevent disruption, the example discussed here clearly suggests that hidden features should be carefully checked with respect to the invariance properties of the (derived) class. The example is taken from  [Bar92].

In slide object-invariant, we have defined a class employee. The main features of an employee are the (protected) attribute sal (storing the salary of an employee) and the methods to access and modify the salary attribute. For employee objects, the invariant (expressing that any amount k is equal to the salary of an employee whose salary has been set to k) clearly holds.

Now imagine that we distinguish between ordinary employees and managers by adding a permanent bonus when paying the salary of a manager, as shown in slide hidden-bonus. The reader may judge whether this example is realistic or not.


Problem -- hidden bonus


  class manager : public employee { 
manager

public: long salary() { return sal + 1000; } };

Invariant


      k =?= (m->salary(k))->salary() 
  

slide: Violating the invariant

Then, perhaps somewhat to our surprise, we find that the invariant stated for employees no longer holds for managers. From the perspective of predictable object behavior this is definitely undesirable, since invariants are the cornerstone of reliable software. The solution to this anomaly is to make the assignment of a bonus explicit, as shown in slide explicit-bonus.

Solution -- explicit bonus


  class manager : public employee { 
manager'

public: manager* bonus(int n) { sal += n; return this; } };

Invariant -- restored


       k + n == ((m->salary(k))->bonus(n))->salary() 
  

slide: Restoring the invariant

Now, the invariant pertaining to managers may be strengthened by including the effects of assigning a bonus. As a consequence, the difference in salary no longer occurs as if by magic but is directly visible in the interaction with a manager object, as it should be.

An objective sense of style

The guidelines presented by  [LH89] were among the first, and they still provide good advice with respect to designing class interfaces.

Good Object-Oriented Design