topical media & game development
object-oriented programming
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 \sliref{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
- organize and reduce dependencies between classes
Client
-- A method m is a client of C if m calls a method of C
Supplier
-- If m is a client of C then C is a supplier of m
Acquaintance
-- C is an acquaintance of m if C is a supplier of m but not (the type of) an argument of m or (of) an instance variable
of the object of m
- C is a preferred acquaintance of m if an object of C is created in m or C is the type of a global variable
- C is a preferred supplier of m if C is a supplier and C is (the type of) an instance variable, an argument or a preferred acquaintance
slide: Clients, suppliers and acquaintances
In slide [Demeter], an explicit definition of the dual notions
of client and supplier has been given.
It is important to note that not all
of the potential suppliers for a class may
be considered safe.
Potentially unsafe suppliers are distinguished
as acquaintances, of which those that are either
created during a method call or stored in a global
variable are to be preferred.
Although this may not be immediately obvious,
this excludes suppliers that are
accessed in some indirect way, for instance
as the result of a method call to
some safe supplier.
As an example of using an unsafe supplier,
consider the call
screen->cursor()->move();
which instructs the cursor associated with the screen
to move to its home position.
Although screen may be assumed to be a safe supplier,
the object delivered by need
not necessarily be a safe supplier.
In contrast, the call
screen->move_cursor();
does not make use of an indirection
introducing a potentially unsafe supplier.
The guideline concerning the use of safe suppliers is known
as the Law of Demeter, of which the underlying
intuition is that the programmer should not be bothered
by knowledge that is not immediately apparent
from the program text (that is the class interface)
or founded in well-established conventions
(as in the case of using special global variables).
See slide [4-demeter].
Law of Demeter
ignorance is bliss
Do not refer to a class C in a method m unless C is (the type of)
1. an instance variable
2. an argument of m
3. an object created in m
4. a global variable
- Minimize the number of acquaintances!
Class transformations
- lifting -- make structure of the class invisible
- pushing -- push down responsibility
slide: The Law of Demeter
To remedy the use of unsafe suppliers,
two kinds of program transformation are suggested
by [LH89].
First, the structure of a class should be made invisible
for clients, to prohibit the use of a component
as (an unsafe) supplier.
This may require the lifting of primitive actions
to the encompassing object, in order to make these
primitives available to the client in a safe way.
Secondly, the client should not be given
the responsibility of performing
(a sequence of) low-level actions.
For example, moving the cursor should not
be the responsibility of the client of the screen,
but instead of the object representing the screen.
In principle, the client need not be
burdened with detailed knowledge
of the cursor class.
The software engineering principles underlying
the Law of Demeter may be characterized
as representing a compositional approach,
since the law enforces the use of immediate
parts only.
As additional benefits, conformance
to the law results in hiding the component
structure of classes, reduces the coupling of control
and, moreover, promotes reuse by
enforcing the use of localized (type)
information.
(C) Æliens
04/09/2009
You may not copy or print any of this material without explicit permission of the author or the publisher.
In case of other copyright issues, contact the author.