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:
Systems should be constructed out of cohesive, loosely coupled modules.
The Abstraction Principle
The implementation of a module should be independent of its interface.
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:
Member functions are related by a logical concept, such as area:
Member functions are called at or near the same time. For example, at system initialization time:
Member functions are steps in an application domain procedure. Note that this often implies logical and temporal cohesion:
These are variations on procedural cohesion.
Functional/Informational cohesion
Member functions are services performed by application domain objects:
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.
In C++:
Content Coupling: Access to Private Members
Changing the private members of Plane may cause changes in Pilot:
class Plane
{
double altitude,
speed;
friend class
Pilot;
// etc.
};
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 Plane: public Aircraft{ ... };
We can loosen the client coupling between pilots and planes. In this Java example a pilot only knows the interface of its provider:
class Pilot
{
AirCraft
myAirCraft = new Plane();
// etc.
}
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:
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 Tower // the mediator
{
list<Plane*>
planes;
// etc.
};
class Pilot
{
Tower* the
Tower;
// etc.
};
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:
class Pilot
{
AirCraft
myAirCraft = new Plane();
// etc.
}