Design Goals and Principles

What is good design and why is it important? Let's begin with a few design goals. A well designed program should be useful, usable, and modifiable.

A system can perform complicated functions, but it isn't useful if these functions don't solve the right problems. For example, a word processor might make it easy to perform complicated, infrequent tasks such as creating indices, but simple, common tasks, such as inserting a dash, can take hours of searching through manuals for some unmemorable sequence of commands, while other common tasks, such as spell checking, might not be available at all!

A system might solve the right problems, but it isn't usable if it is difficult to learn and use. A poorly designed user interface, for example, can create an impedance mismatch between human workflow patterns and the workflow patterns imposed by the software. Sometimes human interactions and problem solving are discouraged, even prohibited by the software. Have you ever had to wait for change because the "computer was down" and the clerks weren't authorized to make change on their own?

Modifiable means the program isn't needlessly difficult to alter, which implies the program isn't needlessly difficult to understand. But why is modifiability a design goal? Surveys suggest that between 50% to 75% of a program's lifecycle is spent in the maintenance phase. The same surveys indicate that 65% of the maintenance effort is perfective. Often, maintenance is not done by the application's developers. Modifying or replacing a component is risky and time consuming if the person making the change doesn't fully understand the component or its role in the application. Two important design principles help us write modifiable programs are modularity and abstraction:

The Modularity Principle

Systems should be constructed out of cohesive, loosely coupled modules.

The Abstraction Principle

The implementation of a module should be independent of its interface.

An abstract, cohesive, loosely coupled module is easy to use, reuse, and replace. Taken together, the modularity and abstraction principles say that the overall design of a program should be understandable without needing to understand the design of the component modules, and the design of a component module should be understandable without needing to understand the design of the entire program.

Cohesion

A class exhibits tight cohesion if its member functions are closely related. From loosest to tightest, here are several examples of cohesion:

Coincidental Cohesion

Member functions are totally unrelated:

class MyFuns
{
public:
    void initPrinter();
    double calcInterest();
    Date getDate();
};
Logical Cohesion

Member functions are related by a logical concept, such as area:

class AreaFuns
{
public:
    double circleArea();
    double rectangleArea();
    double triangleArea();
};
Temporal Cohesion

Member functions are called at or near the same time. For example, at system initialization time:

class InitFuns
{
public:
    void initDisk();
    void initPrinter();
    void initMonitor();
};
Procedural Cohesion

Member functions are steps in an application domain procedure. Note that this often implies logical and temporal cohesion:

class BakeCake
{
public:
    void addIngredients();
    void mix();
    void bake();
};
Communicational and Sequential Cohesion

These are variations on procedural cohesion.

Functional/Informational cohesion

Member functions are services performed by application domain objects:

class Plane
{
public:
    void takeoff();
    void fly();
    void land();
};
Coupling

Coupling degree is a property of an association between two classes. Two classes are loosely coupled if a change to one doesn't impact the other one too much. Two classes are uncoupled if they are totally unrelated.

class Plane { ... };
class Shoe { ... };
Assume there is a 1-1, unidirectional association between Pilot and Plane:

In C++:

class Plane { ... };
class Pilot
{
    Plane* myPlane;
    // etc.
};
Let's call this client coupling. The client, Pilot, has access to all of the public members of Plane, the provider. There are two examples of tighter couplings:

Content Coupling: Access to Private Members

Changing the private members of Plane may cause changes in Pilot:

class Pilot { ... };

class Plane
{
    double altitude, speed;
    friend class Pilot;
    // etc.
};

Inheritance Coupling: Access to Protected Members

Although coupling is normally a property of associations, this is where specialization might fit into the scheme. Changing the protected members of Aircraft may cause changes in Plane:

class Aircraft
{
protected:
    double altitude, speed;
    //etc.
};

class Plane: public Aircraft{ ... };

Interface Coupling

We can loosen the client coupling between pilots and planes. In this Java example a pilot only knows the interface of its provider:

interface AirCraft { ... }
class Plane implements AirCraft{ ... }
class Blimp implements AirCraft{ ... }

class Pilot
{
    AirCraft myAirCraft = new Plane();
    // etc.
}

Meta-Interface Coupling

Java's Reflection mechanisms allow us to further decouple pilots and planes. In this example the pilot doesn't even know if myObject refers to an aircraft, a shoe box, or a baloney sandwich. Java reflection is used to dynamically discover what services myObject performs:

class Pilot
{
    Object myObject = new Plane();
    Class class = myObject.getClass();
    Method[] methods = class.getMethods();
    // etc.
}
Message Coupling

In each case we have examined, Pilot objects have handles (pointers or references) to Plane objects. (Although the handle might be typed as an interface or meta-interface.) In C++, it is possible that the Plane object could be deleted, which breaks the Pilot object.

Message passing allows us to decouple a pilot from his plane by introducing a mediator or broker object that passes messages from the pilot to the plane.

class Plane
{
public:
    void register(Tower* t);
    void land();
    // etc.
};

class Tower // the mediator
{
    list<Plane*> planes;
    // etc.
};

class Pilot
{
    Tower* the Tower;
    // etc.
};

Abstraction

There are two ways to look at a module. A client (i.e., the person or client module that uses it) sees it in terms of the services it provides, while the implementer sees it in terms of its internal structure, in terms of how it provides services. We call the client's view the module's interface and the implementer's view the module's implementation.

The abstraction principle says a client shouldn't need to know about a module's implementation in order to use it, and the implementer should be able to change the module's implementation without breaking the client's code (assuming the interface doesn't change).

Java's interface mechanism (seen earlier) is a great abstraction mechanism:

interface AirCraft { ... } // Java
class Plane implements AirCraft{ ... }
class Blimp implements AirCraft{ ... }

class Pilot
{
    AirCraft myAirCraft = new Plane();
    // etc.
}