|
|
|
Sponsored Link •
|
|
Advertisement
|
Summary
In this installment of my Design Techniques column, I analyze the flexibility and performance implications of inheritance and composition, and I give guidelines on the appropriate use of each.
One of the fundamental activities of any software system design is establishing relationships between classes. Two fundamental ways to relate classes are inheritance and composition. Although the compiler and Java virtual machine (JVM) will do a lot of work for you when you use inheritance, you can also get at the functionality of inheritance when you use composition. This article will compare these two approaches to relating classes and will provide guidelines on their use.
First, some background on the meaning of inheritance and composition.
About inheritance
In this article, I'll be talking about single inheritance through
class extension, as in:
class Fruit {
//...
}
class Apple extends Fruit {
//...
}
In this simple example, class Apple is related to
class Fruit by inheritance, because
Apple extends Fruit. In this example,
Fruit is the superclass and
Apple is the subclass.
I won't be talking about multiple inheritance of interfaces through interface extension. That topic I'll save for next month's Design Techniques article, which will be focused on designing with interfaces.
Here's a UML diagram showing the inheritance relationship
between Apple and Fruit:
![]() Figure 1. The inheritance relationship |
About composition
By composition, I simply mean using instance variables that are
references to other objects. For example:
class Fruit {
//...
}
class Apple {
private Fruit fruit = new Fruit();
//...
}
In the example above, class Apple is related to
class Fruit by composition, because
Apple has an instance variable that holds a
reference to a Fruit object. In this example,
Apple is what I will call the front-end
class and Fruit is what I will call the
back-end class. In a composition relationship, the
front-end class holds a reference in one of its instance
variables to a back-end class.
The UML diagram showing the composition relationship has a darkened diamond, as in:
![]() Figure 2. The composition relationship |
Dynamic binding,
polymorphism, and change
When you establish an inheritance relationship between two
classes, you get to take advantage of dynamic binding
and polymorphism. Dynamic binding means the JVM will
decide at runtime which method implementation to invoke based on
the class of the object. Polymorphism means you can use a
variable of a superclass type to hold a reference to an object
whose class is the superclass or any of its subclasses.
One of the prime benefits of dynamic binding and polymorphism
is that they can help make code easier to change. If you have a
fragment of code that uses a variable of a superclass type, such
as Fruit, you could later create a brand new
subclass, such as Banana, and the old code fragment
will work without change with instances of the new subclass. If
Banana overrides any of Fruit's methods
that are invoked by the code fragment, dynamic binding will
ensure that Banana's implementation of those methods
gets executed. This will be true even though class
Banana didn't exist when the code fragment was
written and compiled.
Thus, inheritance helps make code easier to change if the needed change involves adding a new subclass. This, however, is not the only kind of change you may need to make.
Changing the superclass
interface
In an inheritance relationship, superclasses are often said to be
"fragile," because one little change to a superclass can ripple
out and require changes in many other places in the application's
code. To be more specific, what is actually fragile about a
superclass is its interface. If the superclass is well-designed,
with a clean separation of interface and implementation in the
object-oriented style, any changes to the superclass's
implementation shouldn't ripple at all. Changes to the
superclass's interface, however, can ripple out and break any
code that uses the superclass or any of its subclasses. What's
more, a change in the superclass interface can break the code
that defines any of its subclasses.
For example, if you change the return type of a public method
in class Fruit (a part of Fruit's
interface), you can break the code that invokes that method on
any reference of type Fruit or any subclass of
Fruit. In addition, you break the code that defines
any subclass of Fruit that overrides the method.
Such subclasses won't compile until you go and change the return
value of the overridden method to match the changed method in
superclass Fruit.
Inheritance is also sometimes said to provide "weak
encapsulation," because if you have code that directly uses a
subclass, such as Apple, that code can be broken by
changes to a superclass, such as Fruit. One of the
ways to look at inheritance is that it allows subclass code to
reuse superclass code. For example, if
Apple doesn't override a method defined in its
superclass Fruit, Apple is in a sense
reusing Fruit's implementation of the method. But
Apple only "weakly encapsulates" the
Fruit code it is reusing, because changes to
Fruit's interface can break code that directly uses
Apple.
The composition
alternative
Given that the inheritance relationship makes it hard to change
the interface of a superclass, it is worth looking at an
alternative approach provided by composition. It turns out that
when your goal is code reuse, composition provides an approach
that yields easier-to-change code.
Code reuse via inheritance
For an illustration of how inheritance compares to composition in
the code reuse department, consider this very simple example:
class Fruit {
// Return int number of pieces of peel that
// resulted from the peeling activity.
public int peel() {
System.out.println("Peeling is appealing.");
return 1;
}
}
class Apple extends Fruit {
}
class Example1 {
public static void main(String[] args) {
Apple apple = new Apple();
int pieces = apple.peel();
}
}
When you run the Example1 application, it will
print out "Peeling is appealing.", because
Apple inherits (reuses) Fruit's
implementation of peel(). If at some point in the
future, however, you wish to change the return value of
peel() to type Peel, you will break the
code for Example1. Your change to Fruit
breaks Example1's code even though
Example1 uses Apple directly and never
explicitly mentions Fruit.
Here's what that would look like:
class Peel {
private int peelCount;
public Peel(int peelCount) {
this.peelCount = peelCount;
}
public int getPeelCount() {
return peelCount;
}
//...
}
class Fruit {
// Return a Peel object that
// results from the peeling activity.
public Peel peel() {
System.out.println("Peeling is appealing.");
return new Peel(1);
}
}
// Apple still compiles and works fine
class Apple extends Fruit {
}
// This old implementation of Example1
// is broken and won't compile.
class Example1 {
public static void main(String[] args) {
Apple apple = new Apple();
int pieces = apple.peel();
}
}
Code reuse via composition
Composition provides an alternative way for Apple to
reuse Fruit's implementation of peel().
Instead of extending Fruit, Apple can
hold a reference to a Fruit instance and define its
own peel() method that simply invokes
peel() on the Fruit. Here's the
code:
class Fruit {
// Return int number of pieces of peel that
// resulted from the peeling activity.
public int peel() {
System.out.println("Peeling is appealing.");
return 1;
}
}
class Apple {
private Fruit fruit = new Fruit();
public int peel() {
return fruit.peel();
}
}
class Example2 {
public static void main(String[] args) {
Apple apple = new Apple();
int pieces = apple.peel();
}
}
In the composition approach, the subclass becomes the "front-end class," and the superclass becomes the "back-end class." With inheritance, a subclass automatically inherits an implemenation of any non-private superclass method that it doesn't override. With composition, by contrast, the front-end class must explicitly invoke a corresponding method in the back-end class from its own implementation of the method. This explicit call is sometimes called "forwarding" or "delegating" the method invocation to the back-end object.
The composition approach to code reuse provides stronger
encapsulation than inheritance, because a change to a back-end
class needn't break any code that relies only on the front-end
class. For example, changing the return type of
Fruit's peel() method from the previous
example doesn't force a change in Apple's interface
and therefore needn't break Example2's code.
Here's how the changed code would look:
class Peel {
private int peelCount;
public Peel(int peelCount) {
this.peelCount = peelCount;
}
public int getPeelCount() {
return peelCount;
}
//...
}
class Fruit {
// Return int number of pieces of peel that
// resulted from the peeling activity.
public Peel peel() {
System.out.println("Peeling is appealing.");
return new Peel(1);
}
}
// Apple must be changed to accomodate
// the change to Fruit
class Apple {
private Fruit fruit = new Fruit();
public int peel() {
Peel peel = fruit.peel();
return peel.getPeelCount();
}
}
// This old implementation of Example2
// still works fine.
class Example1 {
public static void main(String[] args) {
Apple apple = new Apple();
int pieces = apple.peel();
}
}
This example illustrates that the ripple effect caused by
changing a back-end class stops (or at least can stop) at the
front-end class. Although Apple's
peel() method had to be updated to accommodate the
change to Fruit, Example2 required no
changes.
Comparing composition and
inheritance
So how exactly do composition and inheritance compare? Here are
several points of comparison:
Choosing between
composition and inheritance
So how do all these comparisons between composition and
inheritance help you in your designs? Here are a few guidelines
that reflect how I tend to select between composition and
inheritance.
Make sure inheritance models the is-a
relationship
My main guiding philosophy is that inheritance should be used
only when a subclass is-a superclass. In the example
above, an Apple likely is-a Fruit, so I
would be inclined to use inheritance.
An important question to ask yourself when you think you have
an is-a relationship is whether that is-a relationship will be
constant throughout the lifetime of the application and, with
luck, the lifecycle of the code. For example, you might think
that an Employee is-a Person, when
really Employee represents a role that a
Person plays part of the time. What if the person
becomes unemployed? What if the person is both an
Employee and a Supervisor? Such
impermanent is-a relationships should usually be modelled with
composition.
Don't use inheritance just to get code
reuse
If all you really want is to reuse code and there is no is-a
relationship in sight, use composition.
Don't use inheritance just to get at
polymorphism
If all you really want is polymorphism, but there is no natural
is-a relationship, use composition with interfaces. I'll be
talking about this subject next month.
Next month
In next month's Design Techniques article, I'll
talk about designing with interfaces.
A request for reader
participation
I encourage your comments, criticisms, suggestions, flames -- all
kinds of feedback -- about the material presented in this column.
If you disagree with something, or have something to add, please
let me know.
You can either participate in a discussion forum devoted to this material or e-mail me directly at bv@artima.com.
About the author
Bill Venners has been writing software professionally for 12
years. Based in Silicon Valley, he provides software consulting
and training
services under the name Artima
Software Company. Over the years he has developed software
for the consumer electronics, education, semiconductor, and life
insurance industries. He has programmed in many languages on many
platforms: assembly language on various microprocessors, C on
Unix, C++ on Windows, Java on the Web. He is author of the book:
Inside
the Java Virtual Machine, published by McGraw-Hill.
Reach Bill at bv@artima.com.
This article was first published under the name Composition versus Inheritance: Which One Should You Choose? in JavaWorld, a division of Web Publishing, Inc., October 1998.
|
Sponsored Links
|