Using Composition Over Inheritance Makes You Think Better

In your quest to become a better software engineer, if your journey is anything like mine, you’ve heard it many, many times: “Composition Over Inheritance”.

Early on, it may seem counter-intuitive. You just learned all this “Object Oriented” stuff, and you want to make big pretty object graphs to solve all the world’s problems. The pros tell you to pump the breaks, you don’t listen because oh my god look at the beautiful fractal of inheritance I just created. Then your project becomes impossible to modify or maintain, and it’s not immediately obvious why.

Well, because inheritance kinda sucks. I mean, it doesn’t actually suck, it’s really awesome, but it’s often overused, and when it is, it causes more problems than it solves. So smart people will discourage you from overusing it, and when they do, you’ll see lots of examples similar to the following (I’m using C++ for this demonstration):

class Walker {
public:
    void walk() { std::cout << "Walking" << std::endl; }
};

class Taker {
public:
    void take() { std::cout << "Taking" << std::endl; }
};

class OkRobot: public Walker, public Taker {

};

“Ew, no, stop, bad,” they’ll say when they see this. “It’s all about ‘has-a’ as opposed to ‘is-a’.”

And they’re right. This is a pretty great example of a time when inheritance is not the way to go. That may seem confusing, as the above object graph seems to perfectly exemplify an “is-a” situation. “An OkRobot is a Walker, and an OkRobot is a Taker“. But just because you can make a sentence with the words “is a” that makes sense doesn’t mean that’s the relationship you want. There’s a couple giveaways that you can look for that will quickly let you know if inheritance is the wrong idea.

  • The name of the parent class is an agent noun, usually ending in -er. In other words, the entire parent class revolves around a single action or behavior: Walker. Driver. Printer. Saver.
  • The parent class is very short.
  • The child class is inheriting from multiple classes, especially if the others also fit these criteria.
  • The child class could not be conversationally explained in terms of being a special kind of the parent class. For example, a motorcycle can be defined as a special kind of vehicle. Calling a robot a special kind of walker or taker doesn’t make much sense.

There’s many more examples of indications of this sort of thing all over the internet. People much smarter than me have sorted this out, so if you’re still not clear on this, I recommend seeking other people’s explanations. The real crux of this article is later, and assumes you understand this part.

Once the above code has been explained as being bad, something resembling the below code is posted:

class Walker {
public:
    void walk() { std::cout << "Walking" << std::endl; }
};

class Taker {
public:
    void take() { std::cout << "Taking" << std::endl; }
};

class BetterRobot {
public:
    Walker w;
    Taker t;
};

Much better. We’re less tightly coupled, we have more say in how our child class works without breaking our parent classes, and we’ve done away with multiple inheritance, helping us to avoid the dreaded diamond. This is certainly a huge improvement.

But this is where the forum and question threads inevitably end, with one of the most profound aspects of this refactor left on the table: Why did we create these Walker and Taker classes? What do they encapsulate?

See, if we’re building a bunch of different kinds of automatons, we probably don’t want to just create a single walker module that we stick on every single one. What if one needs more or fewer than normal legs? What if one needs to have wheels? If the Walker class is our movement module, and we need something different, we have to implement a completely new class that handles that case and inherit from that. Not awesome.

So what did we really want here? Probably Arm and Leg classes that we can then arrange however we want. Now the construction of our Robot class is whatever we need. Plus, new functionality only requires its own implementation, not the re-imagining of other code.

class Leg {
public:
    void step() { std::cout << "Stepping" << std::endl; }
};

class Arm {
public:
    void grasp() { std::cout << "Grabbing" << std::endl; }
};

class BestRobot {
public:
    std::vector<Leg> legs{4};
    std::vector<Arm> arms{2};
};

Now we can easily make a robot with four legs and two arms just for funsies.

This is one of the oft-overlooked benefits of composition, and it’s one of the most important ones: It makes you think more intelligently about your design, and encourages flexible, extensible interfaces. It saves you headaches you never knew you might’ve had.

Use classes to build those cool interfaces. Only tie types together when you’re sure you won’t need to untie them later.

Leave a Reply

Your email address will not be published. Required fields are marked *