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:
Here's what that would look like:
Composition provides an alternative way for
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
Here's how the changed code would look:
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 {When you run the
// 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();
}
}
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 {Code reuse via composition
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();
}
}
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 {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.
// 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();
}
}
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 {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
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();
}
}
Apple
's peel()
method had to be updated to accommodate the change to Fruit
, Example2
required no changes.
No comments:
Post a Comment