refactoring

Favor Composition Over Inheritance

Today I’d like to expand on what Yohei wrote about with respect to delegation.

We’ve all been there. You’ve just read the Gang of Four Design Patterns book and are ready to do some heavy duty object oriented design. State patterns! Mediators and Flyweights! Deep inheritance hierarchies that account for our entire taxonomy of things!

Stop. Take a deep breath. Go back to chapter 1 and look at the “principles of object oriented programming”. Specifically, the second one:

Favor object composition over class inheritance

(The first principle “Program to an interface, not an implementation” deserves your attention as well but is not the subject of this post)

I’ll make this even simpler:

If you find yourself subclassing, take a walk, clear your head and think very carefully about what you’re doing

Inheritance in object oriented design is another way of saying specialization. Take this extremely contrived example (borrowing from Sandi Metz’s excellent talk at RailsConf 2015).

Say I have a Vehicle class, maybe with the following interface:

class BaseVehicle(object):
    def go(destination):
        # go there

A reasonable base interface. Now, say we want to represent a Car using this base class. Easy:

class Car(BaseVehicle):
    def go(destination):
        takeHighway(destination)

What about a sports car? Easy again:

class SportsCar(Car):
    def go(destination):
         takeHightwayFast(destination)

Great. Now let’s get crazy and say we want a car that takes a completely random path to it’s destination. Well, ok:

class RandomCar(Car):
    def go(destination):
        takeRandomPathToDestination(destination)

Super! Now, of course, we need a sports car that takes a random path to it’s destination! Easy!

Wait. Huh.

class RandomSportsCar(RandomCar) : ... but also SportsCar? Somehow?
    def go(destination):
        goFastOnHighway(destination)
        butAlsoTakeRandomPath(destination)?

and we’re stuck. How do we inherit two things at once? Multiple inheritance? Ugh. But we still need an implementation for go(). What should that look like?

This is going to be a unmaintainable mess. Go back to the principle outlined above:

Favor object composition over class inheritance

So, back up and come at it from the perspective of composition instead of inheritance. First, let’s ask ourselves the question: what are the things that are varying in our model?

  1. The velocity at which we get to our destination (sports car vs normal car)
  2. The way we navigate to our destination (normal highways vs a random path)

Let’s encapsulate that behavior and delegate it! First, define an engine:

class NormalEngine(object):
    def drive():
        driveAtNormalSpeed()

and a navigation system:

class NormalNavigationSystem(object):
    def navigate(destination):
        navigateNormallyToDestination(destination)

And a car:

class Car(object):
    def __init__(engine, navigation_system):
        self._engine = engine
        self._navigation_system = navigation_system
    def go(destination):
        self._engine.drive()
        self._navigation_system.navigate(destination)

A normal car might be constructed like so:

my_normal_engine = NormalEngine()
my_normal_nav = NormalNavigationSystem()
my_car = Car(my_normal_engine, normal_navigation_system)

And now, we don’t even need a sports car class, we need a specialization of the Engine class:

class SportsCarEngine(Engine):
    def drive():
        driveFast()
...
my_sports_engine = SportsCarEngine()
my_car = Car(sports_car_engine, normal_navigation_system)

and our normal random car:

class WonkyNavigationSystem(NormalNavigationSystem):
    def navigate(destination):
         takeRandomTurnsUntilWeGetThere(destination)
...
my_wonky_nav = WonkyNavigationSystem()
my_car = Car(normal_car_engine, my_wonky_nav)

and finally our sports random car is equally straightforward:

my_wonky_sports_car = Car(my_sports_engine, my_wonky_nav)

Now, we’re able to vary these critical components of our car as needed without any hard-to-understand inheritance. Delegating your object’s varying behavior to collaborators will make your code cleaner, easier to test and, perhaps most importantly, flexible in the face of change.

Discussion

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s