Top 20 Design Heuristics
from Arthur Riel's 61 Object Oriented Design Heuristics
This document lists a subset of the 61 heuristics from Arthur Riel's book Object-Oriented
Design Heuristics (Addison-Wesley, 1996). The accompanying text
gives some interpretation of the meaning of the heuristics.
What are design heuristics?
The OO Design Heuristics are a set of guidelines for good
object oriented designs.
As Bjarne Stroustrup (inventor of the C++ programming language)
pointed out many years ago, just making a design "object oriented"
doesn't always mean that you are making the design "good".
In fact, many inexperienced designers have a very weak understanding
of object oriented design -- and there are a number of common
design mistakes that they make.
Here is one example:
- Some designers will define a single big central class
(a "god class") that is responsible for controlling all of the
principal system behavior.
What is wrong with this?
-
It is a disaster for evolution and maintenance:
"God classes impede evolution because they achieve only a low level
of procedural abstraction, so changes may affect many parts of a
god class, its data containers, and its clients.
By splitting a god class up into object oriented abstractions,
changes will tend to be more localized and therefore easier
to implement." [from Object-Oriented Reengineering Patterns
by Serge Demeyer, Stephane Ducasse, and Oscar Nierstrasz
(Morgan Kaufmann, 2003):
http://scg.unibe.ch/download/oorp].
How should I use the heuristics?
The design heuristics are a good "checklist for designers":
-
Use the design heuristics to help evaluate your design alternatives
early in the design process. This can save a lot of design
rework later.
-
Use the design heuristics as part of a design review or
code review process. Design reviewers should look for potential
violations of the design heuristics, and they should suggest
possible restructuring steps to restore a good system structure.
The heuristics should never be taken as "absolute rules" --
and in fact, some of the heuristics are contradictory.
The heuristics are useful for evaluating the goodness of
a design.
Heuristic 2.2: Users of a class must be dependent
on its public interface, but a class should not be dependent on its users.
Interpretation: The "users" of a class are the other functions
and classes that call upon the class. If the class is going to be
as reusable as possible, the class implementor should make only
minimal assumptions about the external functions
and classes that will be calling on the public operations of the class.
A LinkedList class will provide insert(), remove(), search_for(), and length_of()
operations the same way no matter what class is requesting these services.
A Timer class can time PhoneCall or TrackMeet objects.
Heuristic 2.6: Do not clutter the public interface
of a class with things that users of that class are not able to use or
are not interested in using.
Example: Some class developers might write one or more
public constructors for an abstract class (a class that contains declarations
of "pure virtual functions" [in C++] or abstract functions [in Java]).
It is a better idea to make these constructors "protected" because they
will only really be callable in the definition of the constructors of derived
classes.
Heuristic 2.8: A class should capture one and only
one key abstraction.
Interpretation: The process of object oriented design
is the process of discovering the important abstractions in the system
and mapping them to implementable classes. The "abstractions" that
the classes will implement might come from several different places:
-
an object oriented analysis of the specific problem
-
the domain engineering process applied to a set of related software systems
in the same problem domain
-
the proposed "architecture" of the system
-
low-level design abstractions that are used to help implement interfaces
to hardware devices, external databases, and other subsystems
The main job of class design is to make sure that each class is "just the
right size". If a class contains multiple abstractions, there will
be more cross-checking required later when a class needs to be modified
or extended.
Heuristic 3.2: Do not create god classes/objects in
your system. Be very suspicious of an abstraction whose name contains
Driver, Manager, System, or Subsystem.
Interpretation: This heuristic is an extension of the
previous one. A god class is a class that is either
-
a class that makes all of the policy decisions for a significant part of
the system after collecting the necessary information from its peers (a
behavioral god class), or
-
a class that encapsulates all of the important data in the system, giving
it out to any of the other classes when they ask for it (a data
god class).
A god class is a symptom of poor distribution of system intelligence.
Behavioral god classes are often created when a designer creates a "controller"
class to combine all of the policy decisions in a single place. Data
god classes may result from naive conversion of an existing data-oriented
system to an object oriented structure.
God classes are a problem because they increase the effort needed
to change the design later.
Most designs need to expand to handle new behaviors and new usage
scenarios.
In a system with a god class, that god class is often involved
in most of the usage scenarios that will be modified as new
features are added to the system.
The designers will have the burden of verifying that a large
number existing scenarios are unaffected by each change to the
god class.
Exceptions: This heuristic might be violated when
designing an interface to legacy code:
- You are creating a Facade class that acts as a wrapper for
a large legacy subsystem. This kind of wrapper class might
need to be a behavioral god class -- which reflects the design
decision that the legacy subsystem is taking the responsibility
for specific behavior and scenarios.
- The Facade class serves an important purpose. It "hides"
the details of the legacy implementation, and it provides the
writers of the rest of the application with a simpler and
stable "application programmer interface".
Heuristic 3.3: Beware of classes that have many accessor
methods defined in their public interface, many of them imply that related
data and behavior are not being kept in one place.
Interpretation: An accessor method is a function in a
class that gives access to one data attribute in the class.
It is OK to have some accessor functions for some attributes, but don't
take things too far. If you have accessor function for every attribute,
the data isn't really encapsulated. Other classes will become
strongly coupled to the internal representation of the data, which
will make things harder to change and evolve.
You need to decide on the "role" of each class. If the class
is a "service provider", it probably doesn't need to offer direct access
to much of its data -- most other classes just want it to "do" something.
If the class is a "data organizer", then there probably is a need for
accessor functions, but they might only permit access to selected
internal values.
Heuristic 3.4: Beware of classes which have too much
non-communicating behavior, i.e. methods which operate on a proper subset
of the data members of a class. God classes often exhibit lots of
non-communicating behavior.
Interpretation:
"Non-communicating behavior" is a term
that describes a problem that arises in class design: there might
be a small subset of the public member functions of the class
that is implemented only in terms of a small subset of the data
attributes in the class.
The subset of the member functions is "non-communicating" to most of the
data attributes, which indicates that the class might be split into two
classes.
Non-communicating behavior is considered
to be a sign that a class is responsible for multiple abstractions.
Example: A poorly-designed CustomerOrder class might contain
some attributes that contain information about the customer (with
a set of operations that access and modify the customer attributes) plus
some attributes that contain information about the items that are
being ordered (with other operations that access and modify the item information).
class CustomerOrder {
private:
std::string customer_name;
std::string customer_billing_address;
std::string customer_shipping_address;
Money order_cost;
std::vector<std::string> order_item_names;
std::vector<Money> order_item_costs;
public:
void set_customer_info(std::string name,
std::string addr1, std::string
addr2);
void add_new_item(std::string item_name, Money item_cost);
void clear_all_items();
Money get_cost() const;
};
This CustomerOrder class is a good candidate for being refactored:
the customer attributes and operations can be moved to a Customer class,
the item attributes and operations can be moved to an Item class, and the
new CustomerOrder class will "contain" a reference to a Customer and a
list of references to Items. In the new design, the Customer class
can now be used in other places (in the billing or marketing subsystems,
for example).
Exceptions: This heuristic might be violated for a couple
of reasons:
-
Some classes just have a large number of attributes (that can't easily
be divided into meaningful functional groups), and many of the simple operations
on the class only need to access a small number of attributes.
-
In some programming languages, there may be a performance penalty in breaking
up a large class into several smaller classes. For example, a Java
applet that is constructed from a large number of simple classes may take
more time to download to a workstation than an applet constructed from
a small number of more complex classes.
Heuristic 3.9: Do not turn an operation into a class.
Be suspicious of any class whose name is a verb or derived from a verb.
Especially those which have only one piece of meaningful behavior (i.e.
do not count sets, gets, and prints). Ask if that piece of meaningful
behavior needs to be migrated to some existing or undiscovered class.
Interpretation: This is a sure sign of procedural thinking.
Any procedural design can be turned into a "superficially object oriented"
design by converting each procedure into a class.
Heuristic 3.10: Agent classes are often placed in
the analysis model of an application. During design time, many agents
are found to be irrelevant and should be removed.
Interpretation: An agent class is an intermediary class
that is mainly concerned with forwarding requests for operations to other
classes. These classes often find their way into a design because
the designer wants to "model the real world".
Exceptions: Some object oriented methodologies (such as
Ivar Jacobson's Object Oriented Software Engineering [Objectory]) emphasize
the creation of agent classes to decouple the behavior of major entities
in the system. This decoupling can make individual classes more reusable.
Heuristic 4.8: Distribute system intelligence vertically
down narrow and deep containment hierarchies.
Interpretation: This heuristic attempts to
make the design simpler by dividing the attributes among
lower-level classes that each contain groups of related attributes.
If all of the intelligence of the system is in the big "domain-level"
classes, then each of those classes may need to access many of the individual
primitive data fields that are contained either directly in the class or
indirectly in other classes that it contains. If much of the system
intelligence is delegated to the subcomponents of the "domain-level" classes,
then these top-level classes will avoid being god classes.
Heuristic 4.11: The semantic information on which
a constraint is based is best placed in a central third-party object when
that information is volatile.
Interpretation: This heuristic and Heuristic 4.12 give
two alternative ways to organize the state information which needs to be
accessed to enforce a semantic constraint. If the constraint conditions
change frequently, it makes sense to create a separate class to contain
all of the constraint limits, so they don't clutter the other "real" classes.
Heuristic 4.12: The semantic information on which
a constraint is based is best decentralized among the classes involved
in the constraint when that information is stable.
Interpretation: If a constraint is relatively stable,
it doesn't have to be implemented with a third-party class. It can
be implemented in the classes involved instead, eliminating an "unnecessary
class".
Heuristic 5.1: Inheritance should only be used to
model a specialization hierarchy.
Interpretation: This means that a subclass must follow
the "is-a" rule: each operation in the superclass must make sense
in the subclass (although it is permissible to have a different implementation
of any superclass operation in a subclass).
Note: The most common violation of this rule is to
use inheritance to model a "has-a" relationship. For example,
maybe my C++ application class CourseRoster needs to hold a list of
names, so I think about defining my class as a subclass of
list.
class CourseRoster : public list<string> {
public:
CourseRoster(const char *coursename);
void addStudent(const char *sname);
...
};
This is a bad design strategy, because there are now two ways to
add a new student to the CourseRoster: by calling the public
addStudent() function or by calling the list insert function in
the list<string> base class.
You don't want to do this, because the "is-a" relationship isn't
valid -- CourseRoster is not a list, it has very specific behavior
for performing updates of its internal data structures.
There are much better ways to implement the "has-a" relationship -- for
example, define an attrbute within the CourseRoster class:
class CourseRoster {
public:
CourseRoster(const char *coursename);
void addStudent(const char *sname);
...
private:
list<string> registeredStudents;
};
Heuristic 5.7: All base classes should be abstract
classes.
Interpretation: This heuristic makes it "illegal" to create
a concrete subclass of another concrete class. The inheritance tree
should only have concrete classes at the leaves, and all of the interior
nodes of the inheritance tree should be abstract.
This is intended to keep separate abstractions separate: if a new
operation is added to a concrete class that is a base class, there
may be unintended changes to derived classes that inherit from the
base class.
Exceptions: This heuristic is often violated. If
this heuristic is followed blindly, the designer might create a large number
of artificial classes as part of the design.
Simple extensions of concrete classes are often OK in a design.
If future modifications begin to make this part of the design more
complicated, you can consider at that point doing some refactoring
to introduce an abstract base class.
Heuristic 5.8: Factor the commonality of data, behavior,
and/or interface as high as possible in the inheritance hierarchy.
Interpretation: You should minimize the amount of duplicated
code in an inheritance hierarchy. If two leaf classes contain the
same function with identical implementations, the function should be moved
to the common parent class of the two classes. If two leaf classes
contain the same function with different implementations, the base class
might define an abstract function (pure virtual function in C++) so that
other classes can invoke this function polymorphically.
Heuristic 5.9: If two or more classes only share common
data (no common behavior) then that common data should be placed in a class
which will be contained by each sharing class.
Interpretation: This is a situation where inheritance
should be avoided. It doesn't make sense to create a common base
class for two classes that only have some common data but no common operations.
The common data can be encapsulated in a small data class and "contained" by both classes.
Heuristic 5.10: If two or more classes have common
data and behavior (i.e. methods) then those classes should each inherit
from a common base class which captures those data and methods.
Interpretation: This is the most common reason for reorganizing
a design to use inheritance.
If there are methods where behavior is identical in both classes, then
the new common base class is the correct place to put the common
behavior.
If there is some "variation" in the detailed behavior of the methods for
the two classes, then the new common base class might be defined with an
"abstract function" that you override in each subclass.
Or you can implement the basic algorithm in the common base class with
calls to helper functions that are redefined in the subclasses.
Heuristic 5.11: If two or more classes only share
common interface (i.e. messages, not methods) then they should inherit
from a common base class only if they will be used polymorphically.
Interpretation: The "messages, not methods" part of this
sentence means that the two classes have one or more operations that have
the same name and take the same arguments (the "messages" are the same),
even though the implementations of the operation are different in the two
classes (the "methods" are different).
Example: If there is a cancel() operation in both the
CustomerOrder class and the Subscription class, it is not necessary to
have CustomerOrder and Subscription derived from the same superclass, especially
if there are no parts of the system that contain functions that work on
both CustomerOrders and Subscriptions. The name of the operation
in the two classes is just accidentally the same.
On the other hand, if there are different implementations of the edit()
operation for the classes BookList and CDList, but several other classes
need to operate on both BookList and CDList objects, then it makes sense
to create a common base class for BookList and CDList (and to make edit()
an abstract function in that common base class).
Heuristic 5.12: Explicit case analysis on the type
of an object is usually an error, the designer should use polymorphism
in most of these cases.
Interpretation: Object oriented software tries to eliminate
explicit case analysis by putting variations of system behavior into different
subclasses of the same superclass.
Exceptions: Programming languages like C++ and Java have
support for "runtime type identification". This feature can be misused
to create code that is ugly and difficult to maintain. But runtime
type identification may be necessary when trying to extend certain kinds
of library classes.
Heuristic 5.14: Do not model the dynamic semantics
of a class through the use of the inheritance relationship. An attempt
to model dynamic semantics with a static semantic relationship will lead
to a toggling of types at runtime.
Interpretation: You should avoid defining classes with
names that look like "state names" – names that are modified by adjectives.
Names like EmptyStack, DelinquentCustomerAccount, LostPackage, and CallInProgress
are "classes" that are probably just states of the classes Stack, CustomerAccount,
Package, and Call.
Heuristic 5.17: It should be illegal for a derived
class to override a base class method with a NOP method, i.e. a method
which does nothing.
Interpretation: This is a special case of Heuristic 5.1.
If a function from the base class is overridden with a NOP method in a
derived class, then that derived class doesn't really satisfy the "is-a"
relationship to the base class.
This heuristic is often violated for pragmatic reasons, but for
long-term maintenance, it is preferable to use delegation instead
of inheritance.
The following example is from Martin Fowler's Refactoring book (p. 352):
/* don't make Stack a subclass of Vector -
Stack will need to cancel all of the non-Stack
Vector operations */
/* This example shows the wrong way to define a Stack */
class MyStack extends Vector {
public void push(Object element) {
super.insertElementAt(element,0);
}
public Object pop() {
Object result = super.firstElement();
super.removeElementAt(0);
return result;
}
/* size() and isEmpty() are inherited from Vector */
/* Unfortunately, we now have to "cancel" the other
Vector operations, to prevent users from inadvertently
modifying the Stack internals */
private void insertElementAt(Object,int) {}
private Object firstElement() { return nil; }
private void removeElementAt(int) {}
}
/* A better way - use delegation */
class MyStack {
private Vector _vector = new Vector();
public void push(Object element) {
_vector.insertElementAt(element,0);
}
public Object pop() {
Object result = _vector.firstElement();
_vector.removeElementAt(0);
return result;
}
public int size() {
return _vector.size();
}
public Boolean isEmpty() {
return _vector.isEmpty();
}
}
More information:
dmancl@acm.org
Last modified: Apr. 2, 2018