Designing Classes

Good versus Bad Classes

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.

Reusability

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.

1.6.3.1      Classes with Application Scope

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 { ... }

1.6.3.2      Classes with Domain Scope

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

1.6.3.3      Classes with Architectural Scope

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 { ... }

1.6.3.4      Classes with Foundation Scope

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 { ... }

 

Cohesion

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.

Coupling

For an example of reducing coupling see the following patterns:

Facade

Mediator

 

Value versus Reference Objects

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

The implicit parameter

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.
}

Packages

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.