Inheritance and composition
Table of contents
- What is composition?
- Why is there a big push in favour of composition over inheritance?
- What are the disadvantages of composition?
What is composition?
The word composition comes from Latin, compositio, which means “to put together”.
In software, composition is the ability of creating new, possibly more elaborate, classes by putting together other, possibly simpler, classes. We have been using composition throughout the boot camp, without knowing. Take for example the following Person
class.
package demo;
public class Person {
private final String name;
private final int age;
public Person( final String name, final int age ) { /* ... */ }
@Override
public String toString() { /* ... */ }
}
The Person
has a name
and has an age
. The Person
class is composed from a String
and an int
. Note that an emphasis was made on the has a phrase. In the inheritance section, we use the phrase is a instead. For example, a LightBox
is a Box
. The following image shows the difference between inheritance and composition.
Why is there a big push in favour of composition over inheritance?
Effective Java talks about this in great depth in Item 18: Favor composition over inheritance.
When a class inherits from another class, the subclass will inherit all methods that the parent class has. Consider the stack data structure shown next.
A stack is a data structure that follows the Last-In-First-Out rule. A stack data structure, similar to a stack of dishes (or plates). We can put dishes to the top of the stack, we can only task dishes from the top and we cannot see below the top dish.
We can interact with a stack using any of the following three functionalities.
- push where an item is added to the top of the stack
- pop where the last added item is removed from the stack and returned to the caller
- peek (also referred to top) where we can view what’s on the top of the stack without removing it
We can create a stack data structure by extending another collection class, such as the Vector
class. That’s what the Java API did in the past with the Stack
class. The Stack
class inherits all methods defines by the Vector
class, which is incorrect. A stack data structure MUST only provides three methods and definitely MUST not break the Last-In-First-Out rule.
Consider the following example.
package demo;
import java.util.Stack;
public class App {
public static void main( final String[] args ) {
final Stack<String> stack = new Stack<>();
stack.push( "1" );
stack.push( "2" );
stack.push( "3" );
/* This is not a method supported by the stack class */
stack.add( 0, "Squeeze me in" );
System.out.printf( "Stack: %s%n", stack );
}
}
The above example was able to violate the Last-In-First-Out rule as we were able to squeeze an item at the bottom of the stack.
Stack: [Squeeze me in, 1, 2, 3]
This breaks our stack class as we are able to make it behave in a way it was not expected to behave. This example of inheritance breaks encapsulation as we are able to put the object in an invalid state. Here we broke the “all stacks are vectors” rule. A vector is a data structure that allows random access to elements, while stack only allows the consumer to interact with the topmost item of the stack.
An alternative approach would be to use composition instead of inheritance, as shown next.
package demo;
import java.util.Objects;
import java.util.Vector;
import java.util.function.IntFunction;
public class Stack<T> {
private final Vector<T> vector = new Vector<>();
public void push( final T item ) {
vector.add( item );
}
public T pop() {
return withLast( vector::remove );
}
public T peek() {
return withLast( vector::get );
}
private T withLast( final IntFunction<T> handler ) {
final int size = vector.size();
return size == 0 ? null : handler.apply( size - 1 );
}
@Override
public boolean equals( final Object object ) {
if ( this == object ) {
return true;
}
if ( !( object instanceof Stack ) ) {
return false;
}
final Stack<?> stack = (Stack<?>) object;
return Objects.equals( vector, stack.vector );
}
@Override
public int hashCode() {
return Objects.hash( vector );
}
@Override
public String toString() {
return vector.toString();
}
}
Our version of the stack uses the same Vector
as a backing data structure (composition). We are actually storing the Stack
items within the Vector
, but we are shielding the vector
property and we are not exposing it to the outside word.
A side note above the above example.
The
withLast()
method makes use of lambda function and behaviour parameterization, as we are passing behaviour as a parameter.private T withLast( final IntFunction<T> handler ) { final int size = vector.size(); return size == 0 ? null : handler.apply( size - 1 ); }
The
withLast()
checks if the vector is empty, in which case returnsnull
. Otherwise, it provides the given function the last index and expected an object in return.The
peek()
method returns the element at the last position using theVector
’sget()
method as shown next.public T peek() { return withLast( vector::get ); }
While the
pop()
method uses theremove()
method to remove the element at the given index.public T pop() { return withLast( vector::remove ); }
The new version of the Stack
class takes advantage of encapsulation as the outside world does not know about the Vector
class and cannot bypass our Stack
as we did before. We cannot invoke any method defined by the Vector
class as our vector
property is private
and is never returned by our Stack
class.
package demo;
public class App {
public static void main( final String[] args ) {
final Stack<String> stack = new Stack<>();
stack.push( "1" );
stack.push( "2" );
stack.push( "3" );
/* This is not a method supported by our stack class */
// stack.add( 0, "Squeeze me in" );
System.out.printf( "Stack: %s%n", stack );
}
}
We cannot invoke the Vector’s add() method as we did before. We can only interact with our new Stack
using the methods available.
One of the advantages of inheritance is that we can reuse existing code. We agree that composition reuses existing code, without creating a tight coupling between the parent class and its subtypes. Adding new methods to the Vector
class, will not impact our Stack
class. If we inherit from the Vector
instead, adding new methods to the Vector
class will automatically make these methods available to all the vector’s children.
Another advantage of composition is that we can swap our backing collection, Vector
, with a different one, such as LinkedList
, without changing its consumers.
package demo;
import java.util.LinkedList;
import java.util.Objects;
public class Stack<T> {
private final LinkedList<T> linkedList = new LinkedList<>();
public void push( final T item ) {
linkedList.addLast( item );
}
public T pop() {
return linkedList.removeLast();
}
public T peek() {
return linkedList.getLast();
}
@Override
public boolean equals( final Object object ) {
if ( this == object ) {
return true;
}
if ( !( object instanceof Stack ) ) {
return false;
}
final Stack<?> stack = (Stack<?>) object;
return Objects.equals( linkedList, stack.linkedList );
}
@Override
public int hashCode() {
return Objects.hash( linkedList );
}
@Override
public String toString() {
return linkedList.toString();
}
}
Our Stack
class did not gain or lose methods by swapping the backing collection from Vector
to LinkedList
. This gives us the ability to use a better implementation when one becomes available.
What are the disadvantages of composition?
Following are two distinctions between inheritance and composition.
Liskov substitution principle: Composition does not adhere to the Liskov substitution principle. In our version of the stack, we cannot use our
demo.Stack
wherever ajava.util.Vector
is required. Ourdemo.Stack
is not ajava.util.Vector
.Inheritance: With inheritance, the subtypes will inherit all of their parent’s methods without the subtypes needing to do anything. If a new method is added to the parent, this becomes automatically available to all subtypes.
Personally, I do not see these two as disadvantages. Inheritance and compositions are different tools. Composition is used to enrich our classes with properties, such as the vector
within the demo.Stack
class. There are cases where we need to use inheritance too and, in most cases, we will use both.
Say that our application has two types of coins, gold coins and silver coins. Our application should not allow other types of coins. Consider the following example.
package demo;
public class Coin {
private final int quantity;
private Coin( final int quantity ) {
this.quantity = quantity;
}
public static class GoldCoin extends Coin {
public GoldCoin( final int quantity ) {
super( quantity );
}
}
public static class SilverCoin extends Coin {
public SilverCoin( final int quantity ) {
super( quantity );
}
}
}
Given the both the gold and silver coins are coins, we cannot go without inheritance. Also, we need to prohibit new types of coins, therefore we cannot use interfaces as we have no means to prevent an interface from being implemented. The Coin
class itself makes us of composition as it contains properties.