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 { // 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.
No comments:
Post a Comment