Template Method Trap

Inheritance is often a trap. Its starts off like an adorable lion cub but grows up to be a man-eater. One common use of inheritance is within the Template Method Pattern. It is the Jekyll and Hyde of patterns; half pattern, half anti-pattern.

What is Template Method?

Let me step back for a moment. What is Template Method? In the example below, a base class (CurrencyConverter) has one or more invariant methods (convert()) which wire together other variant methods (sourceCurrencyCode() and targetCurrencyCode()). These variant methods are available to be implemented in subclasses (like UsdToCadConverter). The variant methods may be abstract or null in the base class. These invariant method stubs form a template for subclasses, hence the name Template Method.

class CurrencyConverter():
    def sourceCurrencyCode(self):
        raise NotImplementedError()

    def targetCurrencyCode(self):
        raise NotImplementedError()

    def convert(self, units):
        return units * get_exchange_rate(self.sourceCurrencyCode(),
            self.targetCurrencyCode())

class UsdToCadConverter(CurrencyConverter):
    def sourceCurrencyCode(self):
        return 'USD'

    def targetCurrencyCode(self):
        return 'CAD'

Template Method implementations usually attempt to accomplish one or more of these goals:

  • Vary configuration values
  • Vary behavior
  • Trigger events using Hooks

There may be other goals but these are the ones I see most in the wild. The example above is varying configuration values.

Behavior-modifying template methods don't just return static value; they perform an action for the invariant method. Below you can see that the crop() method varies in SimpleProfilePictureCreator and FaceFindingProfilePictureCreator.

class ProfilePictureCreator():
    def crop(self, image, width, height):
        raise NotImplementedError()

    def getProfilePicture(self, image, width, height):
        self.crop(image, width, height)
        sharpen(image)
        saturate(image)
        add_sweet_mustache_even_on_ladies_dont_judge_me(image)

def SimpleProfilePictureCreator(ProfilePictureCreator):
    def crop(self, image, width, height):
        crop(image, 0, 0, width, height)

def FaceFindingProfilePictureCreator(ProfilePictureCreator):
    def crop(self, image, width, height):
        center = locate_face(image)
        crop(image, center.x - (width/2), 
            center.y - (height/2), width, height)

Hooks are frequently seen in MVC framework Controller classes. The below should look familiar. Subclasses like MyController can vary the behavior of beforeRender() which is called from render().

class Controller():
    def __init__(self):
        self.view = View()

    def beforeRender(self):
        pass

    def render(self, action):
        self.beforeRender()
        getattr(self, action)()
        result = self.view.render(action + ".html")
        return result

class MyController(Controller):
    def beforeRender(self):
        self.view.set('is_logged_in', Authorization().isLoggedIn())

    def doSomething(self):
        self.view.set('message', 'Hello World!')

The Template Method Pattern may indeed save time in some basic cases, but it comes at an immediate cost, and potentially more growing pains as complexity and variation increase. Lets talk about these weaknesses and possible alternatives.

Injecting configuration

The subclasses in a Template Method Pattern either expose those variant methods in the public interface, when they are implementation details not meant for public consumption, or hide them from the interface making them hard or impossible to test in isolation.

This can easily be taken care of by injecting the configuration. The currency example is dead simple. The two configuration variables are passed into the constructor. No implementation details spill into the public interface.

class CurrencyConverter():
    def __init__(self, source_currency, target_currency):
        self.source_currency = source_currency
        self.target_currency = target_currency

    def convert(self, units):
        return units * get_exchange_rate(self.source_currency,
            self.target_currency)

You could argue that the constructors took no parameters in the earlier currency example so you just instantiate the class and you're ready to go. However, imagine supporting 20 currencies with this approach! This code clearly would not grow well.

Strategy pattern

Testing of the variant methods cannot be done without instantiating the class which, depending on what the base class does, may not be desirable and may be overcomplicated. Ever try testing one controller method and all of a sudden you're loading an entire framework?

Injecting methods to modify behavior or define hooks is a bit different. In languages like Python you can inject functions. In stricter object-oriented languages you might make an interface for cropper's and create multiple implementations of it. This injecting of interchangeable objects or functions is the Strategy Pattern in action. In the example below simple_profile_picture_cropper and face_finding_profile_picture_cropper are both strategies.

I created a Factory to handle wiring together the pieces, but, were there more pieces, one could use a Builder Pattern.

class ProfilePictureCreator():
    def __init__(self, cropper):
        self.cropper = cropper

    def getProfilePicture(self, image, width, height):
        self.cropper(image, width, height)
        sharpen(image)
        saturate(image)

def simple_profile_picture_cropper(image, width, height):
    crop(image, 0, 0, width, height)

def face_finding_profile_picture_cropper(image, width, height):
    center = locate_face(image)
    crop(image, center.x - (width/2), 
        center.y - (height/2), width, height)

class Factory():
    def createSimpleCreator(self):
        return ProfilePictureCreator(simple_profile_picture_cropper)

    def createFaceFindingCreator(self):
        return ProfilePictureCreator(face_finding_profile_picture_cropper)

In the first profile picture example the constructor had no parameters so you could easily instantiate the profile picture creator of your choosing and be off to the races. In this one you need to call a factory method before you start to work. The first example requires slightly less typing to use the code. The second example is more testable, more flexible in its creation (consider if more options exist, or will exist), and hides its implementation details. So it's a choice, and the choice is yours.

Common variant methods

With a Template Method implementation you may need to share the behavior of some of the variant methods in multiple subclasses. The typical way to handle this problem is to create a new class with those common variant methods and subclass it. Below is an example illustrating this approach.

class CurrencyConverter():
    def sourceCurrencyCode(self):
        raise NotImplementedError()

    def targetCurrencyCode(self):
        raise NotImplementedError()

    def convert(self, units):
        return units * get_exchange_rate(self.sourceCurrencyCode(),
            self.targetCurrencyCode())

class UsdConverter(CurrencyConverter):
    def sourceCurrencyCode(self):
        return 'USD'

class UsdToCadConverter(UsdConverter):
    def targetCurrencyCode(self):
        return 'CAD'

class UsdToGbpConverter(UsdConverter):
    def targetCurrencyCode(self):
        return 'GBP'

Notice the UsdConverter class isn't to be used alone. Its just a means for UsdTo* classes to get their sourceCurrencyCode() methods for free. So whats the problem? Well, there are a few...

  • Depending on the size of your system there may unnecessarily be lots of common-method classes and they be multiple levels of inheritance deep.
  • Naming common-method classes also becomes difficult since the grouping of methods might not have a logical relationship or single purpose. One consequence of this problem is that your outermost subclasses inherit from common-method classes that give little indication of what they do.
  • At some point a change in the common-method class may be undesirable in some of its subclasses since they may lack that true is-a relationship.

If you don't couple your configuration options, you don't really have this problem. You can vary your more complicated situations using Strategies, Decorators, Chains Of Responsibility - all kinds of patterns depending on what you are trying to build. You can, if needed, wire them all together with Builders. If you find you're repeating a lot of your construction process by calling the same set of methods on your Builder, then perhaps you can combine those calls into a new method on your Builder, or create a Factory which pre-configures your Builder with a set of method calls.

This approach makes you create some really powerful and flexible Builders. Your construction process reads top-down like a recipe rather than a shuffled stack of common-method classes.

My advice is to always consider inheritance as a last option when that is-a relationship is not present. This is much easier if you have a firm grasp on the other patterns which do not rely on inheritance.

On the outside looking in

Abstraction captures only those details about an object that are relevant to the current perspective.

http://en.wikipedia.org/wiki/Abstraction_(computer_science)

Imagine we are writing code which will have the concept of a car in it somehow. Our Car abstraction isn't a real car, of course. Our Car abstraction might not need tires. Our Car abstraction might not even need to exist - maybe any Automobile will do. If we treat them all the same theres no need to make the distinction. In fact, maybe we just want to have a car image that is one of many possible images that a user can select from, so the concept of car or even automobile really isn't something we ever need to actually use in our code.

Now imagine designing this system without knowing the "current perspective" or, as I'll refer to it, the context. Which abstraction is the best one? It seems impossible to know, but, in my experience, this is how many programmers design their code.

Feel the pain

We must be aware of the context in which an abstraction exists before we design it. We won't know how to capture the essential complexity without adding accidental complexity until we understand the context.

Consider a couple of all-too-familiar experiences...

  • You try to integrate backend abstractions into a frontend UI, which was specified by a product manager at the beginning of the project, but things aren't going well. Features are missing, or there are extra features you don't need, or your abstraction's interface is really hard to work with in this particular context.
  • You tried to replace some messy code with something new and clean. You haven't really studied the messy code in depth, so you don't really know all of what it does... its just messy. You take the concept that the messy code represents (like car for example) and you just build the best one of those, as if that were possible to determine without context. Its time to integrate your code and things are going badly. The interface is hard to work with. Its methods don't answer the queries your client code has. You create mess at your integration points just so your new abstraction is usable.

There are many more scenarios like these, but the result is always the same: Your abstractions come face-to-face with your requirements for the first time and they don't get along. Your abstractions aren't useful in the context in which they are used. This mistake costs both time and money.

Inside-out, meet Outside-in

The previous examples use what is called Inside-out software development. You are starting in the middle of the code and working your way out to the edges of the code, where the integration happens. The alternative here is Outside-in software development.

If you google this phrase you get a lot of random results about BDD and Cucumber or some Agile practices. I'm not here to sell you those things. Outside-in is a useful concept even without those tools and practices attached.

The Outside-in approach is very basic. Start at your integration points and work inward from it so you can understand the context in which your abstractions will exist. You don't necessarily need to write your code in order from outermost to innermost, you just need to start by acknowledging the outermost. You can do this with or without acceptance tests. The outcome should be obvious - your code integrates cleanly because you had the integration in mind from the very start.

This approach is very beneficial if you're working with other developers who will be creating code that will use your new abstraction even before you've finished implementing it. Since you started with the outermost part of your abstraction you should have at least an interface that other developers can begin to use as a stub.

Abstractions are soft amorphous things. Contexts are made of requirements. They are hard and immovable. Allowing your context to form your abstraction early on removes the pain of integration and allows your team to easily work around your "piece of the puzzle" while you're building it. This translates into saved time and opens the door for concurrent work, all of which enhances your ability to deliver your project on time.

I'd recommend reading Growing Object-Oriented Software, Guided by Tests if you're looking to find a good set of practices using Outside-in software development.

Problems vs Solutions: Putting the cart before the horse

Design patterns are common solutions to common problems. They are a problem-solution pair. If you were to ask your programmer friends to describe an Observer Pattern solution (implementation) I bet they'd nail it in a matter of seconds. Yet, if you asked them describe the problem it aims to solve I bet even if they got it right the speed and quality of their answer would be less impressive. The language would be a little fuzzy. Maybe they'd resort to giving an example of when they used it and work backwards from there.

The solution is what we do in code. We repeat it over and over. It is deeply engrained in our memories. Yet, a solution cannot exist without its problem. How have we been managing to take full advantage of these solutions without a firm understanding of the problems they aim to solve?

Similarly, have you ever introduced another programmer to unit testing? These programmers can crank out production code like a pro, yet struggle to write tests before writing the production code to be tested. They say they don't know what to test, but tests are simply statements of the problem to be solved, so how can that be? Once again it would seem we are solution experts despite a poor understanding of the problem we're solving. How do we do it?

Answer: We don't.

I think programmers often get by with a pinch of luck and a gallon of denial.

  • Our general understanding gets us in the ballpark.
  • We spend most of our time solving the same problems in slightly different ways so repeating solutions doesn't present us with an immediate burden too large to deny.
  • We see bad code as if it were the result of a single disaster rather than a series of small mistakes that we make every day.
  • We believe the complexity of our code is essential, but it is actually accidental.
  • We judge our code in terms of beauty but we have a poor understanding of how beauty should be measured.

Eventually a problem comes along that doesn't fit neatly into our limited list of solutions and we don't notice until its too late. We ignore all the signs that something is going wrong. I call these signs tension.

Tension

My simple definition of tension is "Given the existing design, how hard is it to add a new requirement?" Releasing the tension in a design leaves you with abstractions that have the flexibility you need for the system you must build.

It may seem natural to view tension as an attribute that would be exposed long term as new requirements are added over months and years, but tension is also important in micro-iterations - the ones you might have many of in an hour - as you write code for a larger project. Sure, you might be given a full list of requirements up front but as you code you are likely adding support for one requirement, or even one piece of one requirement, at a time. You aren't getting new requirements, but your code is. You can begin to sense the tension in your design using this perspective.

If you are sitting alone at your desk quietly cursing "the code is slowing me down", "the code is in my way", or "I'm sick of fighting with this code" you likely have a tense design. You are frustrated by your code. You react to your code like it is a teammate who isn't pulling their weight. These are telltale signs of tension.

Building tension

A programmer who has invested little time in understanding design patterns but has a high desire to use them is bound to produce some really stellar tension. Lets follow just such an eager idiot named Eddy Iddy.

Eddy is working on a game where the user can choose their own character and give it a bit of personality. The characters can introduce themselves and those introductions can be modified at runtime with alternate speech styles. Internally we also need to get the character ID from any given character object so we can assign it points as it completes different challenges in our game.

Eddy starts with the introduction part.

interface Character {
    public function introduce();
}

class Bob implements Character {
    public function introduce() {
        return "Hi, I'm Bob.";
    }
}

Great! How about those alternate speech styles...

class SlangDecorator implements Character {
    // ...
    public function introduce() {
        return str_replace(
            'Hi', 'Sup',
            $this->_decorated_character->introduce()
        );
    }       
}

class FeignExcitementDecorator implements Character {
    // ...
    public function introduce() {
        return strtoupper(
            $this->_decorated_character->introduce()
        );
    }       
}

Its flexible where we need it to be flexible. OK Edddy, lets get the character's ID.

interface Character {
    public function introduce();
    public function id();
}

class Bob implements Character {
    // ...
    public function introduce() {
        return "Hi, I'm Bob.";
    }

    public function id() {
        return this->_id;
    }
}

class SlangDecorator implements Character {
    // ...
    public function introduce() { /* ... */ }

    public function id() {
        return $this->_decorated_character->id();
    }
}

class FeignExcitementDecorator implements Character {
    // ...
    public function introduce() { /* ... */ }

    public function id() {
        return $this->_decorated_character->id();
    }
}

Not so hot. Do you feel the tension? These decorators don't really care about id(). They only care about introduce(). If Eddy needs to add more speech styles the problem gets compounded because id() gets repeated even more. If the Character interface needs more methods added then all the decorators also need to have those methods added. This is tension. If I want to add something simple its harder than it needs to be with this design and this extra work provides no benefit whatsoever. More boilerplate. More waste.

The Decorator Pattern is a solution Eddy has in his tool belt and he uses it whenever he can. Initially it works out pretty well. At that point I can see Eddy patting himself on the back saying "You did it, you sly fox." Next Eddy's autopilot kicks in and he just starts tacking on requirements as if all the designing was done and all the problems were solved. It was solutions time. He would have caught the tension immediately if he stopped to think about how the new requirement (the new problem) would play out in the context of the existing design.

Really, this design started to go wrong even before he added id(). He didn't look back at those decorators and say "A Slang is not a Character". These little language cues give you a subtle hint that something has gone awry.

You can think about how to fix the design, but I won't distract you with a solution. This article is about seeing problems before solutions. There is no solution without a problem, so when writing code try to understand the problem first in order to pick the right solution.

Page 1 / 4 »