Tangible Computing
19. Object-Oriented Class Design Principles




19.1 Separation of Concerns

One of the goals of the software architect is separation of concerns, that is, to break the problem into issues that are as independent (or orthogonal) as possible. The idea of separation of concerns is that the less that issues interact, the easier it is to grapple with their solution.

Concerns are intellectual encapsulations of issues. It may not necessarily be possible to localize the addressing of a concern into a single spot. Some concerns are diffused over the entire system. For example, instrumenting every method with entry and exit timing code. Such concerns are called aspects, and are associated with aspect-oriented programming.

The goal of language and methodology designers is to at least localize the descriptions of concerns and their solutions, even if the actual solution is spread out over the entire system.

A reasonable question to ask is what are the kinds of concerns that Object-Orientation addresses well, and what other kinds is it poor at.

19.2 Abstraction

Abstraction is the architectural principle of hiding complexity inside a well-defined container, and accessing that complexity though a well-defined interface. One way of thinking about abstraction is to consider what is being hidden. Most abstraction involves two kinds of hiding: Modules, abstract data types, objects, systems, files, processes, and many other notions of computing are examples of using abstraction to hide complexity. To be general, we call any such kind of abstraction a unit, and the things inside a unit we call elements. A system is a collection of units.

The two main measures of the quality of a collection of abstraction units are coupling and cohesion.

Coupling is the measure of the strength of association established by a connection from one unit to another. Strong coupling complicates a system because understanding one unit requires understanding another. The goal is to reduce coupling as much as possible, which is accomplished by minimizing the relationships between elements not in the same unit.

Cohesion is the measure of the strength of association between elements of a unit. Cohesion can be measured, informally and nonlinearly, by asking why a collection of elements appears in a unit, that is, what binds them together. Possible (but not exhaustive) kinds of binding, in increasing degree of cohesiveness, can be:

  1. Coincidental - the elements are related only because they appear in the unit.

  2. Logical - the elements are present in the unit because they appear together under some logical view of the system. For example, they all have something to do with the user interface. Under a different view they may not be related.

  3. Temporal - the elements are related because the are activated at roughly the same time. For example, they are all initialized at once. Temporal binding is logical binding under a temporal view.

  4. Communicational - the elements are together because they share some common repository of information.

  5. Collaborative - the elements of the unit are together because they collaborate to perform some process. For example, all or portion of a workflow.

  6. Functional - the elements of the unit are together because they collaborate to perform some well-defined function.
A good architecture strives for loose coupling and strong cohesiveness.

19.3 Acessors

Instance variables of objects should always be private. Provide accessor functions to Set and Get the values of the instance variables. Using accessors allows you to

19.4 Law of Demeter

One of the key measures of a design is how much one object needs to know about another object or the system context in order to operate. The Demeter project, of Karl Liebeeerherr and Ian Holland, http://www.ccs.neu.edu/home/lieber/demeter, has identified some desireable properties associated with message dispatch. The idea is that you do not want to encode too many details of the class structure in the method implementations, otherwise when structure is changed, many methods will also have to be changed. In general, public methods of derived classes should invoke only their own private and public methods, or the public methods of their parents. This preserves encapsulation of their parents, and allows parent implementations to be changed without breaking the children.

A guideline for minimizing this kind of coupling is expressed by the Law of Demeter:

A supplier object to a method M is an object to which a message is sent in M.

Law of Demeter: An object O in response to a message M should send messages only to the following preferred supplier objects:
  1. O itself,
  2. objects sent as arguments of message M,
  3. objects O creates as part of its reaction to message M,
  4. objects which are directly accessible instance variables of O,
  5. objects which provide global services to O.


19.5 Styles of Reuse

There are many ways in which existing designs and implementation can be reused in a different context. Here is a simply taxonomy:

19.6 Inheritance as Reuse

When we take a class C and construct a new class D from it, possibly by having D inherit from C, we have to ask: This is very important, as it affects class design considerably. Of course to answer this question we need a reasonably precise description of the behaviour of the class. One way of doing this is to follow the philosophy of:

Design By Contract: Every attribute and method within the class should be documented with pre-conditions, post-conditions, exceptional-conditions, invariants, and side-effects. If possible, assertions about these should be incorporated into the code and left enabled in production releases.

Design Principle: You should try to build your class hierarchy so that factoring of parent classes does not affect interfaces and implementations of derived classes. That is, derived classes should not look at how their ancestors provide services, but only at the interface to those services, and should not explicitly mention where those services are located.

Design Principle: Do not use inheritance to reuse implementation (code). It should only be employed to reuse and extend functionality.

19.7 Liskov's Principle of Substitution

The following rule is called the Liskob substitution principle:
If a class D is "just like" a class C except for extensions, then it should be possible to use a D object anywhere you an use an C object. That is, a child should be able to be used where ever the parent can be used.

Design your classes to preserve this property unless you have a strong reason to do otherwise.
Design Principle: Suppose class D is derived from class C. Then derived class D is-a C. That is, whatever an object of super-class C can do, so should an object of sub-class D.

The principle of substitution is designed to preserve the relation that a derived class is-a class of the same kind as all of its ancestors. A more careful treatment of substitution follows.

Suppose that D is a subclass of C. Then a D object has all of the state of a C object, plus possibly some extra state added when defining D. Let's call the state in D that is from C, the C-state of D.

Now assume the following:

Then we observe that:

Note that the C-state of y.M need not be equivalent to the state resulting from x.M. That is, doing something in D might generate a different result than in C, yet both satisfy the postconditions and maintain the invariants.

For example, the operation M might have the postcondition that M increases the value of attribute d1. But M in D might increase it by 1, while M in C increases it by 2. In both cases, the invariant that d1 is positive is preserved.

Also note that the principle of substitution is relative to the method M. There may be other methods that don't obey substitution, and these should not be used polymorphically, or in circumstances where they might invalidate the invariants needed for substitution of other methods.

19.8 Is-a vs Has-a

Design Principle: Roles are acquired via attributes, not by subclassing.

Full Size


Consider this example. The first diagram shows how a Person can be subclassed into Manager, Employee, or Student. This is the wrong way to assign a role. It implies that a person can only have one role, and can never change their role. The problem is that the inheritance hierarchy for Person is being confused with the inheritance hierarchy for Role. The second diagram is a better way.

Now a resonable question is how does a person know what function they can do? That is, how do they know what the capabilities of their current role is? And if some other object needs a person with those capabilities, how do they locate one?


19.9 Slicing

The remarks apply to C++, or any language where objects are passed by value. Suppose that class D is derived from class C. We can think of D as class C with some extra data and methods. In terms of data, D has all the data that C has, and possible more. In terms of methods, D cannot hide any methods of C, and may have additional methods. In terms of existing methods of C, the only thing that D can do is to override them with its own versions.

If x is an object of class D, then we can slice x with respect to C, by throwing away all of the extensions that made x a D, and keeping only the C part. The result of the slicing is always an object of class C.

Full Size


Design Principle: Slicing an object with respect to a parent class C should still produce a well-formed object of class C. That is, the state invariants for D should imply the state invariants for C.

Usage Warning: Even though D is-a C, you must be careful. If you have an argument type that is a C and you supply a D it will be sliced in most compiled languages if you are doing call by value (not pointer or reference).

Watch out for the sliced = operator, it can make the lhs inconsistent. Also, the = operator is never virtual, it wouldn't make sense. For example, suppose classes A, B are both subclasses of class C. Just because an A is a C, and a B is a C, it doesn't mean you can assign a B object to an A object. Without run-time type information you cannot make a safe assignment.

Slices should obey the Principle of Substitution.

19.10 Polymorphism and Virtual Functions

Suppose that class D has been derived from C in a way that preserves substitution. Can a D be simply substituted for a C? A sliced D will certainly work as a C, but the effect of performing a method on the slice could be to make the object internally inconsistent.

This is where you need virtual functions. Virtual functions achieve polymorphism by allowing any derived class to substitute for the base class.

An abstract base class, that is, one that defines an interface without implementing anything, has all methods pure virtual.

Design Principle: If one method is virtual in a class, all of them should probably be virtual, including the destructors. (Except for those methods that would never be overridden by a derived class.)

Design Principle: Do not, if at all possible, inspect an object to determine its class. Sometimes this is necessary, for example when building a wrapper class that has to inspect the kind of class provided to it in order to achieve the correct transformation of the interface - this typically occurs with generic callback mechanisms.


19.11 Containers

We frequently need to collect objects together into containers. A container for class C is a class whose purpose is to contain objects from class C or its descendents.

Design Principle: Container classes should be generated by template substitution, not inheritance.

Suppose D is a subclass of C. A container for D is not a container for C. That is, containers violate the principle of substitution. For example, although the apple class is-a fruit class, and a bag containing apples is-a bag containing fruit, an apple bag is not a fruit bag --- if it was, you could put a banana into it.



19.12 Behaviour Restriction

Design Principle: If a new class D is like an existing class C, but with restrictions on its behaviour, then D should not be derived from C via inheritance. Why: because restriction violates the principle of substitution. Instead, class D should contain an instance of class C (aggregation). Most of D's behaviour is directly delegated to C, with only a few methods being suitably restricted.

Full Size


Example: A square is not a rectangle. Suppose that we have a class Rectangle that has two attributes l, w and a method SetSize(l,w) which sets l and w. It is tempting to derive a new class Square, that is like a Rectangle except that it overrides SetSize(l,w) to only allow cases where l=w. This violates the principle of substitution, because the pre-condition for calling SetSize on a Rectangle does not imply the pre-condition for calling SetSize on a Square.

It is better to define Square in terms of containing a Rectangle, and giving Square a method SetWidth(x) which calls SetSize(x,x) on its contained Rectangle. All other properties of Square that are Rectangle-ish, such as its color, or position in space, can be delegated to the contained Rectangle.

If both Square and Rectangle are subclasses of a graphics Figure, then the Draw method for Square is delegated to the Draw method for its contained Rectangle.

Design Principle: If a derived class does not use data or methods in the parent class, you should check if this is an instance of behaviour restriction.

19.13 Foundation Classes

Consider the following inheritance diagram
Full Size


If C is being used as a foundation class, then the derived classes are always going to be used on their own. That is, they will always be used in situations where their type will be declared as a D or E. We will never want to manipulate them abstractly as C's.

In this case, the substitution rule could be relaxed. A class derived from C is free to redefine the methods of C as it sees fit. Usually the purpose of C is to provide some basic services that need to be extended or modified a bit. If you do violate the substitution rule, it is important that the resulting D and E classes promise never to be used in a situation where they might be asked to substitute for a C. Slicing could make the internal state inconsistent.

This said, you can still argue that foundation classes obey the substitution rule. Simply set the pre-condition of method M in C to be false, and the post-condition of M in C to be true. The pre in C, i.e. false, always implies any possible pre in D. Similarly, post in D will always imply true, i.e. post in C. Because the precondition of M in C is always false, it is never supposed to be called on a C, and respects the promise above.


19.14 Class Refactoring

Full Size


Consider the following inheritance diagram. The idea is that the base Shape class provides an interface for manipulating shapes. The original class has a very flat inheritance hierarchy, with every basic shape descending from the main shape.

In an attempt to generalize, squares were derived from rectangles, and ellipses from circles. This was ok so long as the original size of the shape was set on creation time, and only altered by magnification later. But a new method ChangeAspect was added to allow rectangles and ellipses to have their aspect ratios changed. This broke the polymorphism, because now squares and circles do not obey the principle of substitution.

Often a method can be moved up or down the hierarchy to preserve substitution. So a third refactoring was done to devide the shapes into reshapable and not. This means that not every shape can have its aspect ration changed. It then becomes impossible to polymorphically operate on shapes with respect to changing their aspect ratio (this affects the graphics editing menu for example). Impossible, that is, unless we introduce a method, IsStretchable, in the base class and make the pre-condition of ChangeAspect to be that IsStretchable is true. Then the polymorphic operation becomes guarded with a testable pre-condition. In general, substitutability can be preserved by providing a testable pre-condition predicate that indicates whether it is safe to call the method or not.


Full Size


Another kind of refactoring can occur along service lines. For example, the shape class above might be used to construct a 2 dimensional model of a system, for example a UML diagram. Using components like the shape class above, the model has two roles: to maintain the mathematical stucture that describes the system, and also to render the visual representation of the system. This means that the model and view roles are tightly coupled. In particular, an element of the model (e.g. a class description) is tied to a graphical icon (like a square).

But in many cases, one wants to interact with the model non-graphically. For example a code analyser might want to construct a UML model of an existing system for reverse engineering purposes. Service refactoring in this case means to separate out the two roles into different services: one for maintaining the model, another for providing a view of the model. This way, many different views can be generated of the model, while preserving the core class structure.

19. Object-Oriented Class Design Principles
Tangible Computing / Version 3.20 2013-03-25