The
goal of every program is to be useful (solve the right problems), usable
(easy to use), and modifiable (easy to maintain). Two important design
principles that help developers achieve the last goal are modularity and
abstraction:
The Modularity Principle
Systems should be decomposed into cohesive, loosely coupled classes.
The Abstraction Principle
The interface of a class should be independent of its implementation.
There
are 4 levels of reusability:
Level 4: Foundation Scope
Level 3: Architectural Scope
Level 2: Domain Scope
Level 1: Application Scope
A
class has application scope if its purpose is specific to a particular
application. A class has domain scope if it can be reused in other applications
that belong to the same application area or domain. A class has architectural
scope if it can be reused in all applications that require a certain
architectural feature. Finally, a class has foundational scope if it can be
reused in any application.
Objects
that handle application-specific events such as mouse button clicks, menu
selections, and errors have application scope:
class MenuHandler { ... }
class MouseListener { ... }
class ErrorHandler { ... }
class Error { ... }
Classes
that render views of domain objects have application scope:
class EmployeeView { ... }
class AccountView { ... }
class TransactionView { ... }
Classes
that represent application specific commands and messages have application
scope:
class Command { ... }
class Message { ... }
Of
course domain classes are categorized by their domains. For example:
health care
retail sales
science
government
military
manufacturing
academia
publishing
games
etc.
Domain
scope classes usually fall into one of four categories: entity, event, role,
and description.
Examples
of entities include:
class Person { ... }
class Address { ... }
class Organization { ... }
class Report { ... }
class Inventory { ... }
class Account { ... }
class Invoice { ... }
class PurchaseOrder { ... }
class AccountManager { ... }
class Product { ... }
class LineItem { ... }
class Sprite { ... } // for games
Examples
of events include:
class Sale { ... }
class Transaction { ... }
class Observation { ... }
class ButtonClick { ... }
class Accident { ... }
Examples
of roles include:
class Customer { ... }
class Student { ... }
class Employee { ... }
Examples
of descriptions include:
class ProductDescription { ... }
class EmployeeType { ... }
class Phenomenon { ... } // describes an observation
class Unit { ... } // describes a quantity
Classes
with architectural scope usually fall into one of two categories: control and
interface.
Interface
classes manage the interface between a program and users, remote systems, or
devices. User interface classes include:
class Window { ... }
class Menu { ... }
class ControlPanel { ... }
class Button { ... }
class TextBox { ... }
class Console { ... } // for an interpreter
Classes
that interface with remote servers include:
class Socket { ... }
class ServerProxy { ... }
class DAO { ... } // Database Access Object
class Query { ... }
class QueryResult { ... }
class URL { ... }
Classes
that interface with devices include:
class File { ... }
class InputStream { ... }
class OutputStream { ... }
Architectural
level controllers deal with forwarding messages and managing other objects:
class MessageDispatcher { ... }
class CommandProcessor { ... }
class MessageQueue { ... }
class WindowManager { ... }
class Thread { ... }
Almost
any application uses these classes:
class String { ... }
class Number { ... }
class Quantity { ... } // = number + unit
class Date { ... } // = month-day-year
class Time { ... } // = hour:min:sec
class Duration { ... }
Data
structures make good classes:
class Stack { ... }
class Queue { ... }
class Tree { ... }
class List { ... }
class Set { ... }
class Vector { ... }
class Graphe { ... }
class Table { ... }
Geometric
and graphics classes can be viewed as foundational, architectural, or domain
scope:
class Point { ... }
class Line { ... }
class Polygon { ... }
class Sphere { ... }
class Pen { ... }
class Canvas { ... }
The methods of a cohesive class work together
to achieve a common goal.
Classes that try to do too many marginally related tasks are difficult to
understand, reuse, and maintain.
Although
there is no precise way to measure the cohesiveness of a class, we can identify
several common "degrees" of cohesiveness. At the low end of our
spectrum is coincidental cohesion. A class exhibits coincidental cohesion
if the tasks its methods perform are totally unrelated:
class MyFuns {
void initPrinter() { ... }
double calcInterest() { ... }
Date getDate() { ... }
}
The
next step up from coincidental cohesion is logical cohesion. A class exhibits logical
cohesion if the tasks its methods perform are conceptually related. For
example, the methods of the following class are related by the mathematical
concept of area:
class AreaFuns {
double circleArea() { ... }
double rectangleArea() { ... }
double triangleArea() { ... }
}
A
logically cohesive class also exhibits temporal cohesion if the tasks
its methods perform are invoked at or near the same time. For example, the
methods of the following class are related by the device initialization
concept, and they are all invoked at system boot time:
class InitFuns {
void initDisk() { ... }
void initPrinter() { ... }
void initMonitor() { ... }
}
One
reason why coincidental, logical, and temporal cohesion are at the low end of
our cohesion scale is because instances of such classes are unrelated to
objects in the application domain. For example, suppose x and y are instances
of the InitFuns class:
InitFuns x = new InitFuns(), y = new InitFuns();
How
can we interpret x, and y? What do they represent? How are they different?
A
class exhibits procedural cohesion, the next step up in our cohesion
scale, if the tasks its methods perform are steps in the same application
domain process. For example, if the application domain is a kitchen, then cake
making is an important application domain process. Each cake we bake is the
product of an instance of a MakeCake class:
class MakeCake {
void addIngredients() { ... }
void mix() { ... }
void bake() { ... }
}
A
class exhibits informational cohesion if the tasks its methods perform
are services performed by application domain objects. Our Airplane class
exhibits informational cohesion, because different instances represent
different airplanes:
class Airplane {
void takeoff() { ... }
void fly() { ... }
void land() { ... }
}
Note
that the informational cohesion of this class is ruined if we add a method for
computing taxes or browsing web pages.
For
an example of reducing coupling see the following patterns:
The state of a value object can't be changed. Value objects are also called stateless or immutable objects. Examples:
Point
Rectangle
Double
String
Date
It's not uncommon or problematic to have several value objects that represent the same thing.
The state of a reference object can change. Reference objects are also called mutable or stateful objects. Usually methods will be provided for changing the state of a reference object. These methods are called mutators. It can be a problem having several stateful objects that represent the same thing, because their states can get out of synch with each other. Examples:
Student
BankAccount
JFrame
Every method has
class HomeLandSecurity {
static final double SUSPICIOUS = 10000;
static public report(Account a, double
amt) { ... }
}
class Account {
private double balance = 0;
public void deposit(double amt) {
if (HomeLandSecurity.SUSPICIOUS <
amt) {
HomeLandSecurity.report(this,
amt);
}
balance += amt;
}
// etc.
}
A
package is a named set of classes, functions, interfaces, and sub-packages.
Packages are useful for partitioning large programs into libraries and
subsystems. A package diagram shows an application's important packages and
their dependencies. Packages appear in these diagrams as labeled folders called
package icons. A dependency is a dashed arrow pointing from an importer package
to an exporter package, and indicates that changes to the exporter package may
force changes to the importer package.
For
example, the following package diagram indicates that components in the
Business Logic package import (use) components defined in the Database package,
while components in the User Interface package import components defined in the
Business Logic package. Of course some of these components may have been
imported by the Business Logic package, so the dependency relationship is
transitive.