Inheritance vs. Composition

by: Ethan McCue

This list is the result of a question posed by Mika Moilanen. It's not comprehensive, but I think it's illustrative of a way to break down these choices that's a tad more healthy than "always choose X" or "never do Y." Specifically because you can take these properties and evaluate them against a given set of conditions.

"List all the differences between using an abstract class and using a delegate object when sharing common functionality."

Both let you reuse a method definition

class Adder {
    int add(int x, int y) {
        return x + y;
    }
}

class Composition {
    private Adder a = new Adder();
    
    int add(int x, int y) {
        return a.add(x, y);
    }
}
abstract class Adder {
    int add(int x, int y) {
        return x + y;
    }
}

class Inheritance extends Adder {
    // Implicitly inherits add
}

Composition requires you re-list every method you want to borrow

class MathDoer {
    int add(int x, int y) {
        return x + y;
    }

    int sub(int x, int y) {
        return x - y;
    }

    int mul(int x, int y) {
        return x * y;
    }

    int div(int x, int y) {
        return x / y;
    }
}

class Composition {
    private MathDoer m = new MathDoer();

    int add(int x, int y) {
        return m.add(x, y);
    }

    int sub(int x, int y) {
        return m.sub(x, y);
    }

    int mul(int x, int y) {
        return m.mul(x, y);
    }

    int div(int x, int y) {
        return m.div(x, y);
    }
}
abstract class MathDoer {
    int add(int x, int y) {
        return x + y;
    }

    int sub(int x, int y) {
        return x - y;
    }

    int mul(int x, int y) {
        return x * y;
    }

    int div(int x, int y) {
        return x / y;
    }
}

class Inheritance extends Adder {
    // Implicitly inherits add, sub, mul, and div
}

Inheritance can lead to observing otherwise hidden method relationships

abstract class MathDoer {
    int add(int x, int y) {
        return x + y;
    }

    int sub(int x, int y) {
        return add(x, -1 * y);
    }
}

class Inheritance extends Adder {
    // Will print for both add and sub
    // but brittle if sub is ever redefined
    // to be "return x - y;"
    int add(int x, int y) {
        IO.println("Add called. x=" + x + ", y=" + y);
        return super.add(x, y);
    }
}
class MathDoer {
    int add(int x, int y) {
        return x + y;
    }

    int sub(int x, int y) {
        return add(x, -1 * y);
    }
}

class Composition {
    private MathDoer m = new MathDoer();

    // Resilient to whatever internal relationships
    // add and sub have within MathDoer
    int add(int x, int y) {
        IO.println("Add called. x=" + x + ", y=" + y);
        return m.add(x, y);
    }

    int sub(int x, int y) {
        IO.println("Sub called. x=" + x + ", y=" + y);
        return m.sub(x, y);
    }
}

Inheritance implicitly gives polymorphic dispatch for all exposed methods

abstract class MathDoer {
    int add(int x, int y) {
        return x + y;
    }

    int sub(int x, int y) {
        return x - y;
    }
}

class Inheritance extends MathDoer {
    int add(int x, int y) {
        IO.println("Add called. x=" + x + ", y=" + y);
        return super.add(x, y);
    }
}

void main() {
    MathDoer m = new Inheritance();
    IO.println(m.add(1, 2));
}
// To get dynamic dispatch you need an intermediate interface
interface IMathDoer {
    int add(int x, int y);
    int sub(int x, int y);
}

// This in turn means methods must be public.
//
// If you want "package-private" dynamic dispatch you
// are out of luck.
class MathDoer implements IMathDoer {
    public int add(int x, int y) {
        return x + y;
    }

    public int sub(int x, int y) {
        return add(x, -1 * y);
    }
}

class Composition implements IMathDoer {
    private MathDoer m = new MathDoer();
    
    public int add(int x, int y) {
        return m.add(x, y);
    }

    public int sub(int x, int y) {
        return m.sub(x, y);
    }
}

void main() {
    IMathDoer m = new Composition();
    IO.println(m.add(1, 2));
}

You can compose behavior from multiple objects. Inheritance is linear

abstract class Adder {
    int add(int x, int y) {
        return x + y;
    }
}

abstract class Subber {
    int sub(int x, int y) {
        return x - y;
    }
}

class Inheritance extends Adder { // Cannot also extend Subber
    
}
class Adder {
    int add(int x, int y) {
        return x + y;
    }
}

class Subber {
    int sub(int x, int y) {
        return x - y;
    }
}

class Composition {
    private Adder a = new Adder();
    private Subber s = new Subber();

    // Resilient to whatever internal relationships
    // add and sub have within MathDoer
    int add(int x, int y) {
        return a.add(x, y);
    }

    int sub(int x, int y) {
        s.sub(x, y);
    }
}

Inheritance lets you define protected members which are public only to classes in the same package and extenders

package a;

abstract class MathDoer {
    protected abstract int addInternal(int x, int y);
    
    public final int add(int x, int y) {
        return addInternal(x, y);
    }

    public final int sub(int x, int y) {
        return addInternal(x, mul(y, -1));
    }
}
package b;

// If extending the class, can see addInternal 
// despite being in a different package
class Inheritance extends MathDoer {
    protected int addInternal(int x, int y) {
        return x + y;
    }
}
package a;

// No way to define a method or field that will
// be visible across package boundaries only to things
// that are "composing".
final class MathDoer {
    int add(int x, int y) {
        return x + y;
    }

    int sub(int x, int y) {
        return add(x, -1 * y);
    }
}

Inheritance requires you directly expose constructors

abstract class MathDoer {
    // Subclasses need to call a super-class constructor
    MathDoer() {}
    
    int add(int x, int y) {
        return x + y;
    }

    int sub(int x, int y) {
        return add(x, -1 * y);
    }
}

class Inheritance extends Adder {
    Inhertiance() {
        super();
    }
}
class MathDoer {
    private MathDoer() {}
    
    public static MathDoer getIt() {
        return new MathDoer();
    }
    
    int add(int x, int y) {
        return x + y;
    }

    int sub(int x, int y) {
        return add(x, -1 * y);
    }
}

class Composition {
    // Meanwhile composition can allow you to keep constructors hidden
    // and only expose static factories or builders.
    private MathDoer m = MathDoer.getIt();
    
    int add(int x, int y) {
        return m.add(x, y);
    }

    int sub(int x, int y) {
        return m.sub(x, y);
    }
}

Inheritance leaves you exposed to new methods with incompatible signatures

abstract class MathDoer {
    int add(int x, int y) {
        return x + y;
    }

    int sub(int x, int y) {
        return add(x, -1 * y);
    }
    
    // int mul(int x, int y) {
    //     return x * y;
    // }
}

class Inheritance extends Adder {
    double mul(int x, int y) {
        // This is fine initially, but poses a problem 
        // if an incompatible method is added to the superclass later
        return ((double) x * y);
    }
}
class MathDoer {
    int add(int x, int y) {
        return x + y;
    }

    int sub(int x, int y) {
        return add(x, -1 * y);
    }

    int mul(int x, int y) {
        return x * y;
    }
}

class Composition {
    private MathDoer m = new MathDoer();

    // Resilient to whatever internal relationships
    // add and sub have within MathDoer
    int add(int x, int y) {
        return m.add(x, y);
    }

    int sub(int x, int y) {
        return m.sub(x, y);
    }

    // Any methods not taken via composition
    // do not pose a problem.
    //
    // A caveat is that this is an issue
    // with interfaces as well, so its more a property
    // of polymorphic dispatch
    double mul(int x, int y) {
        return ((double) x * y);
    }
}

Once I am able to cohere my thoughts without foaming at the mouth and twitching, expect a rant on "mechanics-ism."


<- Index