I recently wrote a blog post about the Strategy Pattern and have been thinking more about how it’s a neat pattern for composition. That line of thought took the voices in my head to discussing ways we construct classes, with various forms of code reuse and interface definitions, by applying design patterns.

That’s a large topic so let’s narrow focus and talk about two options for code reuse: inheritance vs composition.

Object-oriented systems are characterized, in part, by inheritance. We use polymorphism and dynamic binding as tools for switching about objects and allowing the system to choose the best method implementation for a given call.

This flexibility makes code easy to change. Method calls don’t care about which method they call providing the signature/type look correct — you can dynamically swap out the method implementation behind the scenes without syntactically breaking legacy code. Inheritance makes code changes easy when you’re creating a new subclass.

We’re going to need an example (from our code base, heavily modified for illustration):

public class Artifact {
  protected void initializeDefaultValues(Project project) {
  }

  public boolean isScheduled() {
    return Objects.firstNonNullValue(transientIsScheduled, isScheduled);
  }
}

public class StoryCard extends Artifact {
  @Override
  protected void initializeDefaultValues(Project project) {
    state = getInitialCreationStateReadOnly();
  }
}

Artifact artifact = new StoryCard(); // polymorphism
artifact.initializeDefaultValues(project); // dynamic binding

StoryCard card = new StoryCard();
if (card.isScheduled()) {
 …
}

Superclass–subclass relationships are fragile. If we decide to change the signature of Artifact’s initializeDefaultValues method this forces us to change StoryCard to match the new signature (along with the seven other subclasses which override this method). This was part of the motivation for introducing the @Override annotation.

Inheritance is said to provide weak encapsulation. Look back at if(card.isScheduled()). The card object was declared as type StoryCard but since StoryCard extends Artifact it inherits the isScheduled method. Here be danger: changes to isScheduled() in Artifact must be aware that subclasses, such as StoryCard, are relying on it: subclasses know about the method and can call it. Yes, the method is encapsulated but with a weak abstraction.

So inheritance can make it difficult to change the interface of a superclass. As an alternative we can use composition. Here’s a trimmed down version of our example to illustrate:

public class Artifact {
  public boolean isScheduled() {
    return Objects.firstNonNullValue(transientIsScheduled, isScheduled);
  }
}

public class StoryCard {
  private Artifact artifact = new Artifact();

  @Override
  public boolean isScheduled() {
    return artifact.isScheduled();
  }
}

Building StoryCard by composing in Artifact brings the isScheduled method up-front; it is implemented in the back-end by Artifact. By having StoryCard explicitly call isScheduled in Artifact we’ve more strongly encapsulated the method. This forwarding or delegation means we can alter isScheduled in Artifact without breaking any code calling isScheduled on a StoryCard typed object.

Another advantage of composition is that you may delay the creation of back-end objects until you need them. This can be more efficient if there is a lot of overhead in constructing those back-end objects. And since you’re hiding these back-end objects you can dynamically switch out that back-end object at run time — something you cannot do with inheritance. On the flip-side, when using composition the addition of new subclasses requires more effort.

So how do you choose? Traditionally we say inheritance follows the is-a pattern. A StoryCard is-a Artifact so we use inheritance. I would take this a step further and say that the is-a relationship must be true for the entire lifecycle of an object. If that’s not always true then maybe composition is more appropriate.

TL;DR; don’t automatically choose inheritance for code reuse and polymorphism — there must be an full-lifecycle is-a relationship between objects otherwise composition with interfaces may be a better alternative.