Software Reuse - the REBOOT consortium
Even-Andre Karlsson et al
REBOOT (REuse Based on Object-Oriented Techniques) was a four year,
10 company (from 6 European nations) consortium commissioned to synthesize
a holistic approach to software reuse. The major elements of their
results include: organization, methodology, training, and tools. In one
portion of the book, 58 "design for reuse" guidelines are presented
under the headings: general guidelines, OO guidelines, and reuse-specific
guidelines. This list is a refactoring and summary of that material.
Inheritance
-
Use inheritance to express specialization on types.
-
Using inheritance structures is an important reuse-promoting technique that
supports extension and refinement. Inheritance structures identified in early
phases are likely to be usable across an entire problem domain, contrary to
solution-specific structures that are likely to be identified in later stages.
-
Factor common responsibilities as high as possible up the inheritance
hierarchy.
-
If a set of classes all support a common responsibility, they should inherit
that responsibility from a common superclass. Create a common superclass if
it does not already exist and if a good abstraction can be found.
-
If a generalization of several concrete classes can be identified, create a
superclass to encapsulate the generalization.
-
This is a "bottom-up" guideline for use during the construction of classes.
Generalization and abstraction is preferably performed in earlier development
phases when the structure of each component is being architected. By capturing
commonalities in higher abstractions such as superclasses, subclasses can focus
on differences. The reuser can than find the most suitable abstraction from
which to inherit. If commonalities are not captured in higher abstractions,
the reuser is forced to make large modifications and/or extensive redefinitions.
-
Subclasses should be specializations.
-
The standard use of inheritance is to implement specialization relationships,
not to achieve code sharing. In order to not confuse the maintainer or the
reuser, other uses of inheritance should be avoided.
-
Create as many abstract classes as possible.
-
Defining as many abstract classes as possible means you have factored out as
much common behavior as you could possibly foresee. Look for responsibilities,
behaviors, and attributes that are duplicated, or could be common, in existing
or future subclasses. Look for further useful abstractions. If you don't
have, or cannot foresee, at least two subclasses of a candidate abstract
class, reject it.
-
Use abstract classes as far down inheritance hierarchies as possible.
-
Only the leaves of a class hierarchy should be concrete.
It is often easier to inherit from an abstract class that from a concrete
class. An abstract class enforces a standard protocol and prepares for
polymorphism. A concrete class must provide a definition for its data
representation, and some subclasses are likely to need different
representations. Since an abstract class does not have to provide a data
representation, future subclasses can use any representation without fear
of conflicting with the one they inherited. Also, in a concrete superclass it
is easy to make over-restrictive assumptions about specializations.
-
Inhibit classes intended to be abstract from being instantiated.
-
If the virtual member functions of a class that should not be instantiated
are made pure virtual, then several purposes are served. It forces a reuser
to subclass the abstract base class before reusing it. It compels the reuser
to study the protocol and semantics of the base class abstraction. And it
specifies interface only and defers all implementation details, thus eliminating
the problems that have caused such bitter controversy about the use of multiple
inheritance.
-
Class hierarchies should be fairly deep and narrow.
-
Experience shows that class hierarchies are often too shallow and too wide.
If there are more than ten subclasses directly below one superclass,
it is recommended to look for a new abstraction level. The disadvantage of
having too deep a hierarchy is the difficulty of comprehending its behavior,
since the methods are spread in the hierarchy. When making a class hierarchy,
the focus must be no modelling the real world and mapping the class hierarchy
to normal abstractions.
-
Use multiple inheritance rarely.
-
Multiple inheritance is not common in the real world and it is important to
focus on modelling the real world. It makes little sense to imply that an
object is an instance of more than one thing at the same time.
Multiple inheritance can also introduce ambiguity and increase
complexity - both of which can cause difficulties when reusing or maintaining
a component. If multiple inheritance is necessary, at least try to avoid
ambiguous inheritance.
-
Do not use inheritance with cancellation (private inheritance).
-
Private inheritance in C++ is not an OO concept and it is not used in any OO
design methodology. It may confuse the reuser, since it is difficult to
examine what is inherited and therefore what is reused.
Virtual
-
All methods that might be overridden, should be declared as virtual.
-
Defining methods as virtual tells a reuser which methods are intended to be
overridden. If a non-virtual method is "redefined", the reuser must be more
careful, since he will be reusing the class differently from how the
developer of the component intended.
-
Always make destructors virtual in the base class.
-
This will ensure correct calling of destructors in an inheritance hierarchy.
If a base class pointer is used to point to a subclass instance, and the base
class destructor is virtual; then when the instance is deleted, the destructor
of the actual subclass is called
first, and every destructor all the way up the hierarchy is called. If
the base class destructor is not virtual, only the destructor for the actual
class of the pointer is called. If the base classes of a reusable hierarchy
have their destructors declared virtual, then reusers will not be "surprised" by
the behavior of the derived classes they write.
Encapsulation
-
Distribute system intelligence evenly.
-
A very "intelligent" object knows and manages a lot of information, can do a
lot of things, and affect many other objects. Such a class or subsystem
will be hard to subclass or adapt. Distributing the intelligence among a
variety of objects allows each object to know about relatively fewer things.
Such objects will be easier to modify. The only advantage of the centralized
approach is that it may be easier to see and understand the control and
information flow.
-
Minimize the number of collaborations a class has with other classes or
subsystems.
-
Classes should collaborate with as few other classes and subsystems as
possible. Fewer collaborations means that the class is less likely to be
affected by changes to other parts of the system. One way to accomplish this
is to centralize the communication flowing into a subsystem. You can create
a new class or subsystem to be the principal communications intermediary, or
you can use an existing one for the role.
-
Factor implementation differences into "hasa" abstractions.
-
Not all problems are solved by extending the inheritance hierarchy. Some
implementation differences are better treated by introducing a new "contained"
abstraction. Too deep and large an inheritance hierarchy can be hard to
understand. A more flat, and configurable, set of "hasa" relationships can be
easier to comprehend and adapt than one large "isa" hierarchy.
-
Specify attributes as private.
-
This hides data representation and keeps the interface stable even if the
implementation is changed. The reuser should not have to worry about the
component's implementation. The interface for the reuser consists of the public
and protected methods. The developer has the responsibility of providing a
suitable interface for the reuser. By using this interface, the component is
reused correctly, and misuse is prevented.
-
Use protected member functions instead of protected data members.
-
A subclass of a reusable component should not couple itself, or depend upon,
the implementation choices of its base classes. It should use their public
and protected
interfaces just like outside users of the base classes use their public
interfaces. If the component being reused is exchanged for a new version with
a different data representation, the interface for the reuser need not change.
This makes exchanging an old version for a new one easy.
-
Avoid using "friend".
-
The "friend" concept violates encapsulation and makes reuse more difficult. It
is better to make one or more member functions friends than to make an entire
class a friend.
-
Hide the copy constructor and assignment operator when these functions do not
make sense.
-
This will inhibit misuse by preventing the reuser from performing inappropriate
operations with the component.
Small
-
Keep classes small - avoid large total protocols.
-
The total protocol for a class consists of all methods defined in the class and
all its superclasses that are not redefined or overloaded. It is hard to reuse
large classes (25 or more methods). It is easier to understand and modify or
subclass a few smaller classes from different inheritance hierarchies instead.
A large class may be a candidate for several smaller and less complex class
hierarchies instead.
-
Keep classes small - avoid introducing too many methods in a single class.
-
It is harder to reuse a large class than a small inheritance
hierarchy where common behavior is extracted to appropriate superclasses.
When a large class is refactored into a small hierarchy, the reuser can
choose the most suitable layer of abstraction to reuse.
-
Keep methods small.
-
It is easier to reuse by subclassing if the superclasses have small methods,
since the behavior can be changed by overriding a few small methods instead of
a few large ones. The smaller methods are easier to understand and modify.
It is also easier to identify common behavior when the methods are small.
This common behavior may be migrated to an abstract superclass which can then
probably be reused as is. Every method with more than twenty lines of code
should be inspected for potential modification.
-
One task - one method.
-
Each method should perform only one task to keep it simple. For example,
decompose a method called putAndPrint() into two methods - put() and print().
The protocol of the class will be easier to understand and thus to reuse.
-
Keep the number of method arguments small.
-
Methods with more than five arguments are hard to read and therefore hard to
reuse. It is less likely that standard protocols can be found when the
number of arguments is large. Every method with more than three arguments
(except constructors) should be inspected for potential modification.
Subsystems
-
Make subsystems of strongly coupled classes that implement one unit of
functionality.
-
This is a good way of managing the design at a higher level of abstraction.
Classes in each subsystem are likely to be affected by the same changes in
requirements, and are suitable for reuse as a unit. Even if the precise
changes cannot be predicted, it is still worth the effort to predict which
objects or classes will be affected by the same changes. Such objects or
classes should be grouped together to minimize the effect of each change.
-
Introduce subsystems late in the architectural design phase.
-
While a quick functional decomposition into subsystems allows early division
of work among development teams, it has several drawbacks for reuse. If the
introduction of subsystems is postponed, the maturing class and object designs
allow coupling issues to be more realistically
evaluated, and a better division into loosely-coupled, highly-cohesive
subsystems to be realized.
General
-
Eliminate "switch" statements on object types.
-
Use polymorphism instead. Explicitly checking the type of objects results in
maintainability burdens when new object types are created - because a new
test condition must be added to every switch statement in the application. By
using polymorphism instead, the reuse component will be more adaptable.
-
Avoid "casting down" the inheritance hierarchy.
-
Explicitly casting from a base class to a derived class creates unnatural
dependencies within a class hierarchy. Explicit knowledge of, and dependence
on, object types requires extensive administration and inhibits reuse. If a
component uses downcasting, then the reuser must expand its type administration
if new subclasses are defined.
-
Declare member functions and parameters as "const" when possible.
-
Liberal use of "const" clearly documents developer intentions and allows the
compiler to enforce those intentions. Misunderstanding and misuse on the part
of the reuser are significantly curtailed.
-
Avoid implicit and explicit "inline" in header files.
-
Any "inline" implementations in a header file violate encapsulation and
compromise information hiding. The reuser should not be tempted to "peek
beneath the covers" of the class's implementation. Use explicit "inline"
in the class's source file instead.
-
Implement implicitly-generated class methods.
-
To avoid unexpected behavior, prohibit the compiler from implementing
implicitly-generated methods (constructor, destructor, copy constructor, and
assignment operator) by implementing them yourself. This is especially
important for classes with dynamically allocated memory and "uses-a" relations
with other classes. The explicit definitions show the reuser how to use the
class and how to define these methods if the classes in the component are
inherited. Misuse will be prevented.