Object orientation, inheritance and the template pattern (oh my!)
Monday 30 November 2009 19:43
Recently, I've been discussing with a variety of people the merits of various styles of programming. One person's complaint about object orientated programming is that it leads to huge hierarchies of code, where you constantly have to move up and down the class hierarchy to find out what's going on. I've encountered such code on plenty of occasions, but I disagree that it's a problem with object orientation. Instead, I believe it's a problem with inheritance.
Of course, this all depends on what we mean by object orientation. So, let's take a simple example in Java. In this case, we're going to make a class that generates names.
public class NameGenerator { public String generateName() { return generateFirstName() + " " + generateLastName(); } protected String generateFirstName() { // Generate a random first name ... } protected String generateLastName() { // Generate a random last name ... } }
So, NameGenerator
will generate names using the default implementations of generateFirstName
and generateLastName
. If we want to change the behaviour of the class, say so that we only generate male names, we can override the generateFirstName
method to only generate male names. This is known as the template pattern, the idea being that by putting the overall algorithm in one method that delegates to other methods, we avoid code duplication while still allowing different behaviours in different subclasses.
However, in my experience, the template pattern is a terrible way to write code. In some piece of code, if we see that the NameGenerator
is being used, we might take a quick look at the class to see how it behaves. Unfortunately, the fact that subclasses may be overriding some methods to change behaviour isn't immediately obvious. This can make debugging difficult, since we start seeing behaviour we don't expect according to the class definition we have right in front of us. This becomes especially confusing if one of the methods affects control flow, for instance if we use one of the protected methods in a conditional, such as an if statement.
Testing also becomes more difficult. When we subclass NameGenerator
, what methods should we be testing? We could always test the public method, but then we'll be testing the same code over and over again. We could test just the protected methods, but then we aren't testing the public method fully since it's behaviour changes depending on the protected methods. Only testing the public method if we think its behaviour will have changed is error-prone and brittle to changes made in behaviour in NameGenerator
.
The solution is to use composition rather than inheritance. In this case, the first step is to pull out the two protected methods into two interfaces. The first interface will generate first names, and the second interface will generate last names. We then use dependency injection to get a hold of some implementation of these classes. This means that, rather than creating classes themselves, we have them passed in via the constructor. So, our NameGenerator
now looks like this:
public class NameGenerator { private final FirstNameGenerator firstNameGenerator; private final LastNameGenerator lastNameGenerator; public NameGenerator(FirstNameGenerator firstNameGenerator, LastNameGenerator lastNameGenerator) { this.firstNameGenerator = firstNameGenerator; this.lastNameGenerator = lastNameGenerator; } public String generateName() { return firstNameGenerator.generate() + " " + lastNameGenerator.generate(); } }
Now we've made the dependencies on the generation of first and last names clear and explicit. We've also made the class easier to test, since we can just pass in mocks or stubs into the constructor. This also encourages having a number of smaller classes interacting, rather than a few enormous classes. The code is also more modular, allowing it be reused more easily. For instance, if there's somewhere in the code where we just want to generate first names, we now have an interface FirstNameGenerator
that we can use straight away.
For more on dependency injection, Misko Hevery has a good blog post on the subject, as well as a tech talk on dependency injection.
Notice how the new version of our code is completely devoid of inheritance. That's not to say that we have a completely flat class hierarchy -- we need implementations of FirstNameGenerator
and LastNameGenerator
, but it's important to keep inheritance and implementation distinct. Despite the lack of inheritance, I believe that this is still an example of object orientated programming in action. So, if not inheritance, what characterises object orientation?
I would first suggest encapsulation -- in this case, we don't care how the first and last name generators work, we just call their methods to get a result out. In this way, we can concentrate on one small part of the program at a time.
This doesn't tell the whole story though. If we didn't allow any subtyping, or rather polymorphism, then our example would not be able to use different implementations of FirstNameGenerator
. By having polymorphism, we allow different implementations to be used so that we can use the same algorithm without rewriting it.
So, there we have my view of object orientation -- composition of small objects using encapsulation and polymorphism, while avoiding inheritance and, in particular, the template pattern.
Topics: Software design, Testing