Image by: Me. I’m a programmer, not a graphic designer…

CODEX

Composition vs Inheritance In the Real World (With Java Examples)

Gavin Killough
CodeX
Published in
12 min readMar 14, 2021

--

As a bright-eyed, bushy-tailed college graduate entering the world of software engineering, I was ready to stop learning and start doing. Kidding, of course! Every developer with a shred of real world experience knows that half the job — if not more — is learning. The languages and tools of the industry are constantly refined; open source libraries emerge, removing the necessity to maintain in-house solutions for common problems; and new hardware frequently shifts the conversation about the tradeoffs engineers can afford to make around maintainability, readability, scalability, and performance. In this changing world of technology, learning isn’t just required to stay ahead of the game, learning is the game.

“You must unlearn what you have learned.” -Yoda

Although evolving technology occupies a large portion of a programmer’s regular learning, the abstract concepts and design philosophies that we often take for granted need just as much attention as our educational routines. Without sharpening the abstract toolset, code smells and antiquated patterns will inevitably matriculate into ones work. Even on teams, it is all too easy to become familiar with each other’s styles and complacent in bad habits such that, despite regular code reviews or use of source code analysis (SCA) tools, they still end up in the main codebase. As the Jedi Master Yoda told Luke Skywalker, we too should unlearn what we have learned and avoid doing things a certain way just because that is how we have historically done them. This will ensure we are always writing code that is expressive and readable.

So what does any of this have to do with composition vs inheritance?

Before we answer that question, we need to define our terms. When we talk about composition in Object Oriented programming, we are referring to Object Composition: the concept of an object being made up of other objects. In contrast we have inheritance. Suppose we have two objects: Droid A and Droid B. To say that Droid B inherits from Droid A is to say that Droid B explicitly expresses a relationship to Droid A such that Droid B has all of the same attributes and functionality as Droid A, but Droid A does not necessarily have the additional attributes and functionality of Droid B. If these are unfamiliar terms, then these aren’t the droids you’re looking for.

Alright, enough Star Wars references.

Recognizing bad habits and unlearning them is particularly relevant to properly using composition and inheritance. Such abuses can lead to poor readability and maintainability of not only the code in question, but also of its inheritors and composers. Of course basic polymorphic principles are usually applied correctly in things like data classes (i.e. those whose primary purpose is to tightly-couple a set of related attributes — data) and in basic application flow, yet (perhaps unsurprisingly) there is a glaring disconnect between the theory found in academia and tech blogs and the real world implementation of these concepts. The real world problems we seek to solve with composition and inheritance are often far from the simple examples we might find in literature. Many Java developers have seen an example like the following:

public class Animal {
. . .
}
public class Dog extends Animal {
. . .
}
public class Bird extends Animal {
. . .
}

Such examples might include attributes relating to the number of legs the animal has or if it can fly (we could argue about whether or not those are even good attributes, but that’s for a different article). The reality, however, is that software engineers frequently encounter objects that do not have obvious hierarchical relationships as animals, cars, or any of the other cliché examples.

Take a look at the following Java code:

public interface TaskScheduler {  schedule(Runnable runnable, CronTrigger cronTrigger);}public abstract class AbstractTask implements Runnable {  private final Logger logger = . . .;  private final String taskName;
private final TaskScheduler taskScheduler;
private Future taskFuture;

public AbstractTask(String taskName, TaskScheduler taskScheduler) {
this.taskName = taskName;
this.taskScheduler = taskScheduler;
}
@Override
public final void run() {
logger.info("Running Task: " + getTaskName());
runTask();
logger.info("Completed Task: " + getTaskName());
}
protected abstract void runTask(); public String getTaskName() {
return taskName;
}
public void schedule(String cronExpression) {
// code to validate cronExpression
CronTrigger cronTrigger = new CronTrigger(cronExpression);
taskFuture = taskScheduler.schedule(this, cronTrigger);
}

public void unschedule() {
if (taskFuture != null) {
taskFuture.cancel();
}
taskFuture = null;
}
}

This example was inspired by an open source project to which I recently contributed. To some this class might seem fine — it’s short, only contains a couple fields, and the methods aren’t overly complex — however, it embodies both virtues and vices of polymorphism.

Let’s look at each method one at a time:

  1. The run() method is inherited from the Runnable interface. Later on we see a call to taskScheduler.schedule(this, cronTrigger), so we can deduce the reason AbstractTask implements Runnable is likely because it is meant to be consumed by TaskScheduler. run() has another purpose as well. Because it is a final method, it creates consistent behavior for all of its implementers: logging at the beginning and end of a task’s execution.
  2. Following run() is the runTask() method. It is declared abstract and thus left to the implementer to write, but its relationship to run() demonstrates a powerful usage of inheritance in the real world. The run() method guarantees to its subclasses that a consistent message will be logged as the last operation before runTask() is called and the first operation after. No matter how runTask() is implemented, when AbstractTask’s run() method is called polymorphically, runTask() will be called exactly where it needs to be. Although in this example the consistent behavior is logging and runTask() takes no parameters, the spirit of this code demonstrates a virtue of inheritance because it creates consistency for all subclasses, removes a responsibility (logging) from the implementer, and it does not deviate from the purpose of the class (being Runnable).
  3. getTaskName() could appear to be a harmless getter, but it being public creates ambiguity in AbstractTask's purpose. Typically, a getter is a method found in a data class, but this class is Runnable, implying that it does more than store data. The getTaskName() method is called twice in the run() method which is the only usage we can see in this example, but what other usage could it have? If it had been declared protected we could assume that the author of AbstractTask thought it might be useful for subclasses to access, but why it might be useful would still not be clear. The fact that TaskScheduler::schedule is externally defined and consumes Runnable, suggests it has no concern for getTaskName(). This demonstrates a vice of both inheritance and of the class itself: AbstractTask includes both a getter and Runnable‘s run() as part of its Public API making the purpose of the class unclear. Is this a data class or something that is meant to be run?
  4. Next is the schedule() method. It takes its String parameter, validates it (though the code is ignored to simplify the example), constructs a new CronTrigger based on the String, and then passes that CronTrigger along with the instance of AbstractTask (this) to the taskScheduler's schedule method. This should raise a couple of red flags. First, we have already determined that AbstractTask's primary purpose is to be Runnable, so having a public method that does something else should be greeted with skepticism. The second and more important issue is that we are passing an instance of AbstractTask along to an external library that has no idea what an AbstractTask is. Why go through the trouble of creating AbtractTask when there is nothing that explicitly uses it? Of course there are cases where this might make sense, and we discussed the logging consistency created by the run()/runTask() tandem, but in this case it seems that AbstractTask wants to be consumed by something (i.e. TaskScheduler), but the expression of that intention is not clear from its API. In summary, defining how to run a task vs how to schedule it are likely separate concerns that should require at least two objects. Additionally, consuming AbstractTask within its own private method weakens its case for needing to be its own object. Mark this one down as a vice.
  5. Although unschedule() does not interact with all of the same objects schedule() does, its philosophical purpose is to undo the scheduling. As such, the presence of it in AbstractTask is also a polymorphic vice.

Our analyses of these methods break down into two rules: Take advantage of opportunities to abstract redundant code away from subclasses, and avoid expressing multiple purposes through your Public API.

Public, Protected, Final, Abstract

All good developers should strive to avoid repetitive code. It can often seem easy enough when you are the sole maintainer of a repository, but that is hardly ever the case. What might seem obvious to someone who has spent hundreds of hours on the same codebase can befuddle a newcomer to the codebase, even if that newcomer is a veteran programmer. To avoid this, we must take care to write expressive APIs whose purposes are all but self-evident to anyone who must utilize them. Luckily, Java gives us four powerful tools that , when combined thoughtfully, can do exactly that.

When designing an abstract class, there is usually some functionality that all implementing classes will have to perform the same way. This is where public and final come into play. public methods define your API; they answer the question, “What is this class supposed to be used for?” The final keyword on methods is used to express things that should be consistent in every subclass.

protected and abstract often work best together as well. Typically, the reason a class is abstract is because we know that we want to do something, but we don’t know how to do it. Abstract classes declare abstract methods to leave that how to somebody else. Although we sometimes don’t know any details how to perform a task and must resort to public abstract methods, more often than not subclasses will end up sharing many of the same steps to accomplish a given task. This is where protected methods shine.

Take the example of creating an abstraction, DocumentCreator, which takes in a List of DocumentParts (a data class) and returns a String representing a document formatted a specific way (e.g. HTML, Markdown, PDF, etc.).

public class DocumentPart {  public final int fontSize;
public final boolean bold;
public final boolean italic;
public final String text;
. . .
}
public interface DocumentCreator { /**
* Creates a document by first italicizing (if necessary),
* emboldening (if necessary), and finally resizing each
* DocumentPart's text.
*/
String createDocument(List<DocumentPart> documentParts);
}

Let’s suppose for the sake of this example we know all documents must be formatted in the following order: italicize, embolden, resize. (I know…perhaps not the most realistic example, but not totally unreasonable either.) Rather than leaving the entirety of this interface to a future maintainer, let’s write an expressive abstract class to make it crystal clear what functionality an implementation needs to provide.

public abstract class DocumentCreator {  public final String createDocument(List<DocumentPart> documentParts) {
StringBuilder documentBuilder = new StringBuilder();
for (DocumentPart part : documentParts) {
String formattedText = formatDocumentPart(part);
documentBuilder.append(formattedText);
}
return documentBuilder.toString();
}

private String formatDocumentPart(DocumentPart part) {
String formattedText = part.text;
if (part.italic) {
formattedText = italicize(formattedText);
}

if (part.bold) {
formattedText = embolden(formattedText);
}

return resize(formattedText, part.fontSize);
}

protected abstract String italicize(String text);

protected abstract String embolden(String text);

protected abstract String resize(String text, int fontSize);

}

Now an implementer doesn’t need to think about the order of operations or even the boilerplate string-building, it just needs to know how to take in a String and output an italicized, emboldened, or resized version of that String. Let’s see it in action:

public class HTMLDocumentCreator extends DocumentCreator {

protected String italicize(String text) {
return String.format("<i>%s</i>", text);
}

protected String embolden(String text) {
return String.format("<b>%s</b>", text);
}

protected String resize(String text, int fontSize) {
// We'll use px in this simple example
return String.format("<span style="font-size: %dpx;">%s</span>", fontSize, text);
}

}

That looks a lot better than every subclass having to write a method with a loop, performing its own string-building, and needing to define those formatting methods anyway. Additionally (assuming that there are no bugs in the superclass), the subclass doesn’t need to worry about making a mistake in the string-concatenation logic which any DocumentCreator will want to perform consistently. This exemplifies how using a couple of basic concepts correctly can narrow the margin of error and provide a much clearer API to a maintainer of the codebase.

Despite the fact that many of these rules should be obvious to the seasoned Java Programmer, it is astonishing how frequently I encounter their abuse.

Single Responsibility

Speaking of design principle abuse, you may have thought the design in our last example still didn’t look quite right. If you thought that, you were on to something. Taking a look at our HTMLDocumentCreator again, we notice that there’s actually no document creation code anywhere in the implementation. Even if intuition tells us we will find that in the superclass, that still does not explain why this class seems to be about string formatting even though it is supposed to be creating documents. Although our example still correctly abstracted away the uncommon logic, the way in which it did that created a problem: DocumentCreator is not only responsible for putting the pieces of a document together, it is also responsible for formatting those pieces. I could lazily chalk this up to it being an imperfect example (which admittedly it is), but it is actually a perfect segue into our second conclusion from breaking down the AbstractTask example.

The second problem we discovered in AbstractTask was its ambiguous Public API. DocumentCreator demonstrates a similar issue with its Protected API. While a Public API expresses a contract between class and its consumer, a Protected API expresses a contract between a class and its subclasses. By defining abstract methods based solely around formatting rather than document creation, DocumentCreator came up short in delegating its responsibilities correctly. Now conceptually, formatting is a necessary piece to document creation — especially in our example — yet it has two qualities which flag it as out of place in its current implementation. The first is that its implementation does not affect the steps to create a document; this makes it a candidate for abstraction, but does not necessarily mean it is out of place in DocumentCreator. The decisive quality however is the second: formatting can completely stand on its own. It is this fact that makes HTMLDocumentCreator seem “off”. If you hadn’t already guessed, this is where composition comes into play.

The two aforementioned qualities are the foundation of the Single Responsibility Principle. This is a principle I’ve been alluding to since the very first example. It essentially states that a class should have exactly one responsibility within an application’s (or even a subsection of an application’s) functionality. Let’s take that idea and apply it to DocumentCreator:

public abstract class DocumentPartFormatter {

public final format(DocumentPart part) {
String formattedText = part.text;
if (part.italic) {
formattedText = italicize(formattedText);
}

if (part.bold) {
formattedText = embolden(formattedText);
}

return resize(formattedText, part.fontSize);
}

protected abstract String italicize(String text);

protected abstract String embolden(String text);

protected abstract String resize(String text, int fontSize);

}
public class DocumentCreator { private final DocumentPartFormatter formatter;

public DocumentCreator(DocumentPartFormatter formatter) {
this.formatter = formatter;
}
public final String createDocument(List<DocumentPart> documentParts) {
StringBuilder documentBuilder = new StringBuilder();
for (DocumentPart part : documentParts) {
String formattedText = formatter.format(part);
documentBuilder.append(formattedText);
}
return documentBuilder.toString();
}

}

That’s more like it. Using composition, DocumentCreator has the single responsibility of converting a list of DocumentParts to a String and DocumentPartFormatter has the single responsibility of the actual formatting. Although formatting takes three forms (as defined by the abstract methods), those three concepts should be tightly coupled to a single formatter (we wouldn’t want a document italicizing using HTML’s format and resizing using Markdown’s). Now let’s see what an implementation of DocumentPartFormatter would look like.

public class HTMLDocumentPartFormatter extends DocumentPartFormatter {

protected String italicize(String text) {
return String.format("<i>%s</i>", text);
}

protected String embolden(String text) {
return String.format("<b>%s</b>", text);
}

protected String resize(String text, int fontSize) {
return String.format("<span style="font-size: %dpx;">%s</span>", fontSize, text);
}

}

It looks almost identical to the implementation of HTMLDocumentCreator, but it makes much more sense to be italicizing, emboldening, and resizing in a “formatter” than in a “document creator”. Now that we have perfected our implementation, let’s take a look at how we might use it.

  1. The standard use-case:
DocumentPartFormatter htmlFormatter = new HTMLDocumentPartFormatter();
DocumentCreator htmlCreator = new DocumentCreator(htmlFormatter);

2. In some cases, like a Spring Framework application, it might still make sense to still have a HTMLDocumentCreator class as a Spring component (this is a naïve example of how to use Spring):

@Component
public class HTMLDocumentPartFormatter extends DocumentPartFormatter {
. . .
}

@Component
public class HTMLDocumentCreator extends DocumentCreator {

@Autowired
protected HTMLDocumentCreator(HTMLDocumentPartFormatter formatter) {
super(formatter);
}

}

Not only does composition provide more flexibility in our implementation of DocumentCreator, it also gives our application a new tool, DocumentPartFormatter, that can now be used (and tested) on its own.

Conclusion

Even for experienced software engineers, complacency in code smells can lead to bad habits. Bad habits, in turn, can cause occasional abuses of basic development principles to balloon into an unmaintainable codebase. In Object Oriented Programming, these problems often manifest themselves as improper usage of composition and inheritance which are two of the most crucially important tools of design. Java developers must remain vigilant in their implementation of these patterns and relearn them when misuse begins to creep into their work.

Used correctly, inheritance and composition can create wonderfully expressive APIs and dramatically improve readability and maintainability of applications. Used the wrong way, they can drag a codebase down by creating enormous amounts of technical debt, and entangle responsibilities to the point that massive chunks of code must be rewritten to get things back on track.

I hope the above examples and analyses can serve to demonstrate both the good and the bad — the “virtues and vices” — of composition and inheritance to help you design better code that others delight in maintaining.

--

--

Gavin Killough
CodeX
Writer for

Gavin is a software developer who is always looking for ways to improve code quality and build expressive APIs.