Inheritance == Composition

Jun 1, 2019 09:48 · 3202 words · 16 minutes read

Prefer composition over inheritance

This is one of those pieces of advice that gets thrown around but has many different interpretations. Should you ban inheritance from use entirely? Under what circumstances exactly should you prefer composition? Why should you prefer it? If you should prefer it, can you always represent inheritance hierarchies with composition?

In this essay I’ll attempt to answer these questions by giving general definitions of inheritance, composition, and delegation. We will then see a comprehensive set of the forms inheritance can take and how they are directly translatable to composition. From there I will define the benefits of composition and inheritance in direct comparison of one another and answer the question of when each should be preferred.

Definitions

Composition is the simplest concept so we’ll begin there. If you are familiar with OOP (Object Oriented Programming) already, feel free skip this section and move onto Forms.

Composition

public class ComposedObject {
    public ComposedObject(SimpleObject o1, SimpleObject o2) {
        // Construct an instance of ComposedObject
    }
}

We say that ComposedObject is composed of two instances of SimpleObject. In general, composed objects will accept their dependencies in their constructor, they should very rarely instantiate their dependencies themselves.

Delegation

Objects composed of other objects will generally delegate some or all of their behavior to their dependencies and expose an interface that makes working with those dependencies easier:

public class ComposedObject {

    // Omitted for brevity

    public int getResult(int input) {
        return this.o1(input) + this.o2(input); 
    }
}

In this example, an instance of ComposedObject will delegate to its dependencies by returning a result that is derived from calling methods on those dependencies. This is not necessarily a strict definition, but it illustrates the point.

Inheritance

In Java, we can create a simple inheritance hierarchy with:

public class BaseObject {
    public void behavior() {
        // Do something
    }

    public void otherBehavior() {
        // Do something else
    }
}

public class SpecialObject extends BaseObject {
    @Override
    public void behavior() {
        // Override the method in BaseObject
    }
}

When an instance of SpecialObject is constructed, it will call its implementation of behavior. When otherBehavior is called on an instance of SpecialObject, the implementation defined in BaseObject will be used. In general, programmers tend to use Inheritance when they want to share behavior or data between two different objects. Duplicate behavior in is refactored so that the duplication is stored in a super class and the subclasses defer to the definition and/or data in the superclass. In small inheritance hierarchies, this is useful to prevent duplication and encapsulate behavior and data behind few objects while enabling many objects to benefit. In large inheritance hierarchies, this becomes a source of complexity. The c2 wiki page has a good overview of why inheritance can become more trouble than it’s worth.

Forms

Now that we’re familiar with inheritance and its intended uses, lets look at the practical forms that we typically see. These forms will be the building blocks for our proof that all states representable by inheritance are also representable by composition. I’ll use a mix of languages for these examples as inheritance is not supported in the same way in all languages. Composition examples will all be in Java.

Simple extension

Using inheritance

public abstract class Base {

    public int method() {
        // Do some calculation
        return result;
    }
}

public class Extension extends Base {
    public int method() {
        // Add some arbitrary behavior to the base method
        return super.method() + 1;
    }
}

And using composition

interface Delegate {
    int method();
}

public class BaseDelegate implements Delegate {

    public int method() {
        // Do some calculation
        return result
    }
}

public class CompositeDelegate implements Delegate {

    private final Delegate delegate;

    public CompositeDelegate(Delegate delegate) {
        this.delegate = delegate;    
    }

    public int method() {
        // Add some arbitrary behavior to the base
        return this.delegate.method() + 1;
    }
}

// Configure the delegation, this may be done in another object's constructor
// or passed to anywhere that a Delegate instance is required
Delegate delegate = new CompositeDelegate(new BaseDelegate());

Simple overriding

Using inheritance

public class Base {
    public int method() {
        return 1;            
    }
}

public class Extension extends Base {
    @Override
    public int method() {
        return 2;
    }
}

And using composition

interface Delegate {
    int method();
}

public class BaseDelegate implements Delegate {
    public int method() {
        return 1;
    }
}

public class ExtensionDelegate implements Delegate {
    public int method() {
        return 2;
    }
}

In this case, there is no actual dependency between the two delegates, they can in fact be used completely separately but the first example has the flaw of conflating the dependence on interface with the dependence on behavior. This issue becomes resolved by switching to composition and makes the distinction between the classes clearer.

Hooks

Occasionally we will see some code where the abstract class defines a hook that can be overridden by concrete classes so that they inject some behavior into an algorithm (often seen in conjunction with the Template Method pattern).

public abstract class Base {

    public int method() {
        return this.getPartialSum() + 1;
    }

    public abstract int getPartialSum();
}

public class Extension extends Base{
    public int getPartialSum() {
        return 1;
    }
}

So that when an instance of Extension is created and method is called, 2 will be returned. Other base classes can then implement different behavior while the overall skeleton of the algorithm remains the same.

In composition

public class Base {
    public int method(PartialSumDelegate delegate) {
        return delegate.getPartialSum();
    }
}

interface PartialSumDelegate {
    int getPartialSum();
}

public class PartialSumDelegateReturningOne implements PartialSumDelegate {
    public int getPartialSum() {
        return 1;
    }
}

Base base = new Base(new PartialSumDelegate());

This has the advantage of allowing us to instantiate one Base object and modify the behavior at run time by passing in different Delegate objects as it suits us. This is an application of the Strategy pattern.

Mixin pattern

Java famously cannot do true mixins, but Python can, lets see what this looks like

class AddOne:
    def addOne(self):
        return this.getNumber() + 1

class AddTwo:
    def addTwo(self):
        return this.getNumber() + 2

class Mixed(AddsOne, AddsTwo):
    def __init__(self, n):
        self.n = n

    def getNumber(self):
        return self.n


m = Mixed(5)
m.addOne() # 6
m.addTwo() # 7

In this way, we use multiple inheritance to specify that Mixed is a combination of behavior inherited from AddOne and AddTwo. We can actually represent the same ability with Java, it just takes a little more boilerplate. Lets see the example:

public interface AddOne {
    int addOne();
}

public interface AddTwo {
    int addTwo()
}

public class AddOneImpl implements AddOne {
    public AddOneImpl(int n) {
        this.n = n;
    }

    public int addOne() {
        return n + 1;
    }
}

public class AddTwoImpl implements AddTwo {
    public AddTwoImpl(int n) {
        this.n = n;
    }

    public int addTwo() {
        return n + 2;
    }
}

public class Mixed implements AddOne, AddTwo {

    private AddOne addOneDelegate;
    private AddTwo addTwoDelegate;

    public Mixed(n) {
        this.addOneDelegate = new AddOneImpl(n);
        this.addTwoDelegate = new AddTwoImpl(n);
    }

    public int addOne() {
        return this.addOneDelegate();            
    }

    publc int addTwo() {
        return this.addTwoDelegate();
    }
}

Mixed m = new Mixed(5);
m.addOne() # 6
m.addTwo() # 7

This example is a bit contrived in order to make the interface between the Python and Java versions the same. Ideally, we would make AddOne and AddTwo have no state so that they become pure Strategy objects. That would allow us to pass instances of AddOne and AddTwo into Mixed at instantiation.

We could also opt to have Mixed store no state and pass the state to the implementations of AddOne and AddTwo

new Mixed(new AddOneImpl(n), new AddTwoImpl(n))

This is a little weird, but works the same way in practice.

Multiple inheritance

Java bans multiple inheritance and with good reason. Multiple inheritance leads to the famous diamond of death problem in which a method is implemented by two parents and you get deterministic but unexpected behavior. This problem can be solved by composition. Lets take a typical setup in Python that can result in the diamond of death and then implement it in Java with composition:

class A:
    def method(self):
        return 1;

class B(A):
    def method(self):
        return 2;

class C(A):
    def method(self):
        return 3;

class D(B, C):
    def getResultOfMethod(self):
        return self.method() + 1

Using composition in Java

public interface HasMethod {
    default int method() {
        return 1;
    }
}

pubic class B implements HasMethod {
    public int method() {
        return 2;
    }
}

pubic class C implements HasMethod {
    public int method() {
        return 3;
    }
}

public class D implements HasMethod {

    private List<HasMethod> delegates;

    public class D(List<HasMethod> delegates) {
        this.delegates = delegates;
    }

    public int method() {
        // Select a delegate among the list of available delegates
        // to call method on
    }
}

This has two nice things going for it:

  1. We can specify at runtime instead of compile time which delegate we want to use for this particular calculation.
  2. When implementing D, the problem is clearly illustrated instead of being hidden by a language mechanism. We are immediately aware that we have to choose a delegate from the list we were supplied during instantiation. We also have full control over how to choose that delegate instead of whatever algorithm happens to be implemented in our language.

Mix and match

Using these forms, we can take a non-trivial inheritance hierarchy and see what it would look like using composition instead. This illustrates the point that composing these forms together gives us a way to refactor any inheritance hierarchy of any depth and with any complications into a set of one or more composite objects and interfaces.

First the inheritance hierarchy. This comes from pg. 136 of Practical Object Oriented Design in Ruby

public abstract class Bicycle {
    private final String size;
    private final String chain;
    private final int tireSize;

    public Bicycle(String size) {
        this(size, defaultChain(), defaultTireSize());
    }

    private Bicycle(String size, String chain, int tireSize) {
        this.size = size;
        this.chain = chain;
        this.tireSize = tireSize;
    }

    public List<Spare> spares() {
        List<Spare> spares = new ArrayList<Spare>();
        spares.add("tireSize", tireSize);
        spares.add("chain", chain);
        localSpares().stream().forEach(s -> spares.add(s));
        return spares;
    }

    protected abstract int defaultTireSize();

    public List<Spare> localSpares() {
        return Collections.emptyList(); 
    }

    protected String defaultChain() {
        return "10-speed";
    }

    // Getters excluded for brevity 
}

public class RoadBike extends Bicycle {
    private final String tapeColor;

    public RoadBike(String size, String tapeColor) {
        super(size);
        this.tapeColor = tapeColor;
    }

    public List<Spare> localSpares() {
        return Collections.singletonList("tapeColor", tapeColor);
    }

    public float defaultTireSize() {
        return 23.0;
    }

    // Getters excluded for brevity
}

public class MountainBike extends Bicycle {
    private final String frontShock;
    private final String rearShock;

    public MountainBike(String size, String frontShock, String rearShock) {
        super(size);
        this.frontShock = frontShock;
        this.rearShock = rearShock;
    }

    public List<Spare> localSpares() {
        return Collections.singletonList(new Spare("rearShock", rearShock));
    }

    public float defaultTireSize() {
        return 2.1;
    }

    // Getters excluded for brevity 
}

public class RecumbentBike extends Bicycle {
    private final String flag;

    public RecumbentBike(String size, String flag) {
        super(size);
        this.flag = flag;
    }

    public List<Spare> localSpares() {
        return Collections.singletonList(new Spare("flag", flag));
    }

    public String defaultChain() {
        return "9-speed";
    }

    public float defaultTireSize() {
        return 28;
    }

    // Getters excluded for brevity 
}

public class Spare {
    public Spare(String type, Object value) {
        this.type = type;
        this.value = value;
    }
}

This is much more verbose than the original Ruby version and I’ve elected to omit some things to keep the code listing a bit shorter. The primary concerns we want to refactor away are the hooks and behavior for generating spares. The key behavior we want to preserve is the ability to call spares and get all spares on the Bicycle object.

public class Bicycle {

    private final Component[] components;

    Bicycle(Component... somponents) {
        this.components = components; 
    }

    public List<Spare> spares() {
        return Arrays.stream(components)
                     .filter(component::hasSpare)
                     .map(component::getSpare)
                     .collect(Collectors.toList());
    }

    public interface Component {
        boolean hasSpare();
        Component getSpare();
    }

}

public Tire implements Component {
    private final float size;

    public Tire(float size) {
        this.size = size; 
    }

    public boolean hasSpare() {
        return true;
    }

    public Component getSpare() {
        return new Tire(size); 
    }
}

public Chain implements Component {
    private final String chain;

    public Chain(String chain) {
        this.chain = chain;
    }

    public boolean hasSpare() {
        return true;
    }

    public Component getSpare() {
        return new Chain(chain);
    }
}

public Shock implements Component {
    private final String shock;
    private final ShockType type;

    public Shock(String shock, ShockType type) {
        this.shock = shock;
        this.type = type;
    }

    public boolean hasSpare() {
        return this.type == ShockType.FRONT;
    }

    public Component getSpare() {
        if (this.hasSpare()) {
            return new Shock(shock, shockType);
        }
        return null;
    }
}

public Flag implements Component {
    private final String flag;

    public Flag(String flag) {
        this.flag = flag;
    }

    public boolean hasSpare() {
        return true;
    }

    public Component getSpare() {
        return new Flag(flag);
    }
}

Now if we want to configure the RecumbentBike from the previous example, we initialize a Bicycle as so:

Bicycle recumbentBike = new Bicycle(
    Size.Large,
    new Tire(28),
    new Chain("9-speed"),
    new Flag("tall and orange"));

recumbentBike.spares();

This composition assumes that there are no shared components between all types of bicycles. If for example we want to change our model to make it so that all bikes have Tires (probably a good idea) then we can change the constructor for Bicycle to be:

Bicycle(Tire tire, Component... somponents) {
    // Initialize a tire component always, the rest is unchanged
}

And with a few other modifications we will very clearly delineate that all Bicycles must have tires.

Because we have shown all elemental forms of inheritance can be implemented using composition, and because all complex forms of inheritance are simply combinations of elemental forms, all forms of inheritance can be represented by composition.

Differences between inheritance and composition in this example

Using this example I want to highlight several differences between inheritance and composition. Both can be used to create the same result, but in two distinctly different ways.

Inheritance is implicit, composition is explicit. I mean this with relation to what you see when an object is instantiated. With the inheritance example, you would create the RecumbentBike with new RecumbentBike("tall and orange") which does not tell you whether or not there is a chain on your Recumbent bike, and if so what type. For that you need to look at the implicit dependency of Bicycle to find out what the default chain is.

Inheritance is automated, composition is manual. In order to initialize the RecumbentBike with inheritance, you get a bunch of dependencies like the chain and the wheel automatically pulled in. Using composition, you need to specify all of these dependencies manually.

Inheritance is difficult to reason about when the hierarchies get large because you can never see the behavior and dependencies easily. Everything is hidden behind the language abstraction which automatically and implicitly handles which classes depend on what and where behavior is derived from. Composition makes this clearer because you must configure the objects when they are initialized. There’s less hidden because everything is explicit.

Inheritance resolves dependencies at compile time, composition resolves at run time. In order to see this is true, consider the multiple inheritance example from earlier where we provide multiple delegates and decide which one to take:

    public class D(List<HasMethod> delegates) {
        this.delegates = delegates;
    }

    public int method() {
        // Select a delegate among the list of available delegates
        // to call method on
    }

With inheritance, you would need to somehow specify which of your parent objects you would like to derive behavior from. This is generally done by the compiler/interpreter itself when the objects are built. Composition allows you to pick which object you would like to delegate behavior to based on criteria at runtime. This makes it much more flexible and competent at handling complex scenarios.

Inheritance is inherently rigid, composition is flexible. If you make an assumption in a root object about what behavior it’s children should implement, you cement that behavior for all members going forward. Take the hypothetical example of a Duck

public abstract class Duck() {
    public abstract String quack();
    public abstract String fly();
}

All ducks must implement both methods, but what about a RubberDuck? It should quack, but it should not be able to fly. In this case, you probably want to delegate and provide a GroundedFlyingStrategy object which does nothing when called.

new Duck(new Quack("quack"), new NaturalFlyingStrategy("a flying duck"));
new Duck(new Quack("squeak"), new GroundedFlyingStrategy());

You can of course implement the same thing with inheritance by throwing a NotImplemented exception or leaving the method blank but overriding it, however this is a violation of the Liskov Substitution Principle whereas delegating to a NullObject strategy is not.

Inheritance is a language mechanism and thus is supported in different ways across different languages. We’ve already seen varying examples between Java and Python. C++ and JavaScript both implement inheritance in different ways as well. Composition on the other hand is incredibly consistent. Even non-OO languages support this concept (functional languages for example).

Inheritance is usually less up front work because you don’t need to create delegating methods or interfaces. You can see this clearly in the mixin and hooks examples. In order to access behavior from objects we pass in as delegates, we need to support the interfaces of those objects and generally add more boilerplate.

Why inheritance?

Given that we can represent all inheritance hierarchies as compositional in nature, why bother with inheritance at all? Consider the most primitive inheritance hierarchy in Java: Object. Object implements a few important default methods such as toString and equals (among others) which every object inherits. We can of course eliminate this inheritance hierarchy so that every class must take two positional constructor arguments:

public class AnyClass implements Object {
    public AnyClass(ComparisionStrategy comparisonStragegy, ToStringStrategy toStringStrategy) {
        // Omitted for brevity
    }
}

AnyClass can then delegate to ComparisonStrategy for equals and to ToStringStrategy for toString. The downside here is that you need to include this verbose boilerplate on every object you create which is complete overkill.

You’ll also notice a similar case in our Component classes from the Bicycle example. While we can implement them with composition, we can save a lot of boilerplate if we simply make an abstract Component class and move the spare related behavior up to that class.

This is the power of inheritance and why it’s useful. The compiler is taking care of all this work for us and generating behavior for our objects behind the scenes. There are two bullet points from Code Complete 2 (pg 147) that indicate this use case:

If multiple classes share common behavior but not data, derive them from a common base class that defines the common routines

Which is the case for Object and all other objects and

Inherit when you want the base class to control your interface;

Which is also the case with Object. All instances of Object should have some methods in common (equals, hashCode, etc) and that interface should be controlled by the parent, not the subclasses. Cases like this are ideal uses for inheritance because we get the best features (automatic, easy, implicit, etc) without any of the downsides of complex inheritance hierarchies or maintenance concerns.

The key takeaway is that some implementations such as Object require inheritance to easily attach common behavior to a huge number of children classes without boilerplate. For other collaborating objects, reach for composition before inheritance.