|
|
|
Sponsored Link •
|
|
Advertisement
|
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.
|
Sponsored Links
|