CODEX
Composition vs Inheritance In the Real World (With Java Examples)
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:
- The
run()
method is inherited from theRunnable
interface. Later on we see a call totaskScheduler.schedule(this, cronTrigger)
, so we can deduce the reasonAbstractTask
implementsRunnable
is likely because it is meant to be consumed byTaskScheduler
.run()
has another purpose as well. Because it is afinal
method, it creates consistent behavior for all of its implementers: logging at the beginning and end of a task’s execution. - Following
run()
is therunTask()
method. It is declaredabstract
and thus left to the implementer to write, but its relationship torun()
demonstrates a powerful usage of inheritance in the real world. Therun()
method guarantees to its subclasses that a consistent message will be logged as the last operation beforerunTask()
is called and the first operation after. No matter howrunTask()
is implemented, whenAbstractTask’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 andrunTask()
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 (beingRunnable
). getTaskName()
could appear to be a harmless getter, but it beingpublic
creates ambiguity inAbstractTask's
purpose. Typically, a getter is a method found in a data class, but this class isRunnable
, implying that it does more than store data. ThegetTaskName()
method is called twice in therun()
method which is the only usage we can see in this example, but what other usage could it have? If it had been declaredprotected
we could assume that the author ofAbstractTask
thought it might be useful for subclasses to access, but why it might be useful would still not be clear. The fact thatTaskScheduler::schedule
is externally defined and consumesRunnable
, suggests it has no concern forgetTaskName()
. This demonstrates a vice of both inheritance and of the class itself:AbstractTask
includes both a getter andRunnable‘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?- Next is the
schedule()
method. It takes itsString
parameter, validates it (though the code is ignored to simplify the example), constructs a newCronTrigger
based on theString
, and then passes thatCronTrigger
along with the instance ofAbstractTask
(this
) to thetaskScheduler
'sschedule
method. This should raise a couple of red flags. First, we have already determined thatAbstractTask
's primary purpose is to beRunnable
, so having apublic
method that does something else should be greeted with skepticism. The second and more important issue is that we are passing an instance ofAbstractTask
along to an external library that has no idea what anAbstractTask
is. Why go through the trouble of creatingAbtractTask
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 therun()
/runTask()
tandem, but in this case it seems thatAbstractTask
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, consumingAbstractTask
within its own private method weakens its case for needing to be its own object. Mark this one down as a vice. - Although
unschedule()
does not interact with all of the same objectsschedule()
does, its philosophical purpose is to undo the scheduling. As such, the presence of it inAbstractTask
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.
- 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.