DEV Community

Cover image for Composition over Inheritance
Atharv Rawat
Atharv Rawat

Posted on

Composition over Inheritance

C++ has a lot of double-edged swords. These features abstract the process of turning the sharp edge on yourself, so that you can destroy yourself with greater efficiency.

Well, I threw away one of those swords in time, and its name was Inheritance. It's a powerful tool, no doubt, but it was overkill ;) for my needs.

Inheritance showed up neatly as I was building my first proper C++ game. For now, my definition of proper is a console game with gravity, collision detection, and ASCII for those high quality textures. My game is simple - Balls fall, you catch, score up. With powerups and debuffs to spice things up.

Initially, OOP was my best friend. I effortlessly created a Fallable class that my balls and powerups would inherit. This class defined all the common properties and behaviours, like current x and y positions, collision detection logic, etc. My Ball and PowerUp classes inherited this. The reason inheritance attracted me was its intuitiveness. Examples like Mammal-is-Animal and Dog-is-Mammal sat right with me. Unfortunately, I forgot the significance of Car-has-Engine (hint).

Anyway, since the falling and catching logic was fairly straightforward (I owe this confidence to OneLoneCoder), I moved on to creating the actual powerups.

Before I proceed, it'd be best if you see some of the relevant code so that you know exactly what is going on -

The Fallable class:

class Fallable {
protected:
    float coordX;
    float coordY;
    float speedX;
    int speedY;
    bool inScene;
    short skin;

public:
    Fallable(int x, float y, int speedY, short skin)
        : coordX(x), coordY(y), speedX(0), speedY(speed), inScene(true), skin(skin) {}

    virtual void update(float elapsedTime) = 0;
    virtual void draw(wchar_t* screen, int nScreenWidth) = 0;

    // This function triggers something on collision. Effect upon activation.
    virtual void on_collision(GameManager& game, bool hitBar) = 0;

    bool is_collision_bar(int barX, int barLevel, int barWidth) {
        // crazy advanced logic here...

    bool is_in_scene() { 
        // damn bro let him cook..
        // actual code: return this->inScene; (lol)
    }  

    virtual ~Fallable() {}
};
Enter fullscreen mode Exit fullscreen mode
  • on_collision() is virtual here because balls and powerups do different things when they collide with something. So the children will define the actual behaviour.

The PowerUp class that inherits Fallable:

class PowerUp : public Fallable {
protected:
    int duration;
    double elapsedTime;
    bool isActive;

public:
    PowerUp(int speed, short skin, int duration)
        : Fallable(randomNumGenerator(false, 1, nScreenWidth - 2), 
            0, speed, skin), duration(duration), isActive(false), elapsedTime(0) {

    }

    virtual void deactivate(GameManager& game) = 0;

    // More methods go here...
};
Enter fullscreen mode Exit fullscreen mode

It looks quite rosy, right? PowerUp rightfully inherits Fallable while adding some relevant data and behaviour of its own, like duration, elapsedTime and state. Notice that PowerUp, just like Fallable is an abstract class, meaning that this blueprint cannot be applied directly, as there is data and behaviours that haven't been defined yet. PowerUp does nothing about Fallable's on_collision() virtual function because naturally, the effect-upon-collision is totally dependent on the kind of powerup.

It goes without saying, but I want you to understand that this design set up the need for another layer of inheritance. Since each powerup has different behaviours upon activation, I had to write another set of actual powerup classes that inherited PowerUp.

That is what I did here:

class PUdoubleBar : public PowerUp {
public:
    PUdoubleBar() : PowerUp(13, 0x002B, 7) {}

    void on_collision(GameManager& game, bool hitBar) {
        if (hitBar && game.bar.nWidth <= 20) {
            isActive = true; inScene = false; elapsedTime = 0;
            game.bar.transform(2);
        }
    }

    void deactivate(GameManager& game) {
        if (game.bar.nWidth > 10) game.bar.transform(0.5);
    }

    ~PUdoubleBar() {}
};

// and again
class PUdoublePoints : public PowerUp {
public:
    PUdoublePoints() : PowerUp(13, 0x0058, 10) {}

    void on_collision(GameManager& game, bool hitBar) {
        if (hitBar) {
            isActive = true; inScene = false; elapsedTime = 0;
            game.nScoreMultiplier = 2;
        }
    }

    void deactivate(GameManager& game) {
        if (game.nScoreMultiplier >= 2) game.nScoreMultiplier /= 2;
    }

    ~PUdoublePoints() {}
};

// aaaand againnn :(
class PUenhancedSpeed : public PowerUp {
public:
    PUenhancedSpeed() : PowerUp(13, 0x2192, 7) {}

    void on_collision(GameManager& game, bool hitBar) {
        if (hitBar) {
            isActive = true; inScene = false; elapsedTime = 0;
            game.bar.nSpeed *= 1.25;
        }
    }
    void deactivate(GameManager& game) {
        game.bar.nSpeed /= 1.25;
    }

    ~PUenhancedSpeed() {}
};
Enter fullscreen mode Exit fullscreen mode

I can guarantee that you had one of these three reactions just now:

  • You either did not see anything wrong with this.
  • Or, you sensed something was wrong. There seems to be a bunch of repetition.
  • Orrrr, you just took your head in your hands and shed a tear or two.

Whatever reaction you just had, you are right. Think about which problems or better, which patterns you can spot here before proceeding.

Firstly, the constructors used in the final powerups add nothing to their data or functionality. I am just calling PowerUp's constructor. In fact, I am initializing different values for speed, skin and duration within the new classes. That's pretty bad, because that is exactly what the PowerUp constructor is meant to do.

Secondly, each powerup has exactly two methods that define custom behaviour - on_collision() (effect upon activation. I know the name sucks), and deactivate().

Finally, and perhaps most importantly, all of them change something within the GameManager class upon activation. And for our purpose, this detail is crucial: that is all they do.

All of these problem patterns point (a lil' alliteration) to the same solution. Let's see how they help us figure it out.

(1) The use of the exact same constructors is the first hint of redundancy. This already tells us that inheritance is not providing us with additional value. Maybe there is a way to use PowerUp directly?

(2) Each powerup having only two functions that define custom behaviour. If only there were a way to use the same class and yet have different behaviour? Wait, isn't that the point of using classes after all? Turns out, I took my PowerUp class for granted in order to apply a shiny concept and be able to claim that I wrote some complicated code.

So let's go back to what the basics taught us: If I create a triangle class, I should be able to create objects with different side dimensions. How will I achieve that? By taking the dimension data as arguments in the constructor.

Now, if I create a PowerUp class, I should be able to create objects with different behaviours. How will I achieve that? By passing behaviour data as arguments in the constructor!

I guess it helps to know (and it is probably time for me to remind you) that functions can also take other functions as arguments. Since we have only two functions (on_collision() and deactivate()) that need to be custom, we only need to pass those in the PowerUp constructor. This is an example of Composition over Inheritance - specifically, the Strategy Pattern, where behaviours are injected as callables instead of being defined in subclasses.

Composition over Inheritance is a software design principle which formally states that complex behaviours should be implemented by combining components rather than inheriting behaviour from the parent class. Informally, it is defined (by me) as Why have children when you can do the crying yourself?

Here is what I am doing in my new PowerUp class:

// #include <functional> is mandatory on top.

class PowerUp : public Fallable {
protected:
    int duration;
    double elapsedTime;
    bool isActive;
    std::function<void(GameManager&)> activateCallback;
    std::function<void(GameManager&)> deactivateCallback;

public:
    PowerUp(int speed, short skin, int duration,
        std::function<void(GameManager&)> onActivate,
        std::function<void(GameManager&)> onDeactivate)
        : Fallable(randomNumGenerator(false, 1, nScreenWidth - 2), 0, speed, skin), 
            duration(duration), isActive(false), elapsedTime(0),
            activateCallback(onActivate), deactivateCallback(onDeactivate) {}

    void on_collision(GameManager& game, bool hitBar) {
        if (hitBar) {
            isActive = true; elapsedTime = 0; inScene = false;
            if (activateCallback) activateCallback(game);
        }
    }

    void deactivate(GameManager& game) {
        if (deactivateCallback) deactivateCallback(game);
    }

    // unimportant methods...
};
Enter fullscreen mode Exit fullscreen mode

Quick overview of what matters in this new class:

  • In order to be able to pass functions as arguments, we need the <functional> header which defines std::function.
  • Through the parameter std::function<void(GameManager&)> onActivate, I am telling the compiler to expect a callable (function, function pointer, or lambda) that takes a GameManager object as reference and returns nothing.
  • Since it is possible that a non-existent function is passed, we will first check if a proper function actually exists before running it. As evident in the lines if (activateCallback) activateCallback(game); and if (deactivateCallback) deactivateCallback(game);.
  • Finally, we can address point (3), which was that each powerup was only changing something within the GameManager class. While this can be attributed to good design, it also means that we are able to define our parameter for onActivate and onDeactivate to just be GameManager&. If a powerup needed access to something more than that, then the design wouldn't have worked without non-trivial refactoring.

That is it. This is what makes our PowerUp class customizable. Instead of having a child class which contains these key behaviours, we directly told the PowerUp class how to behave by handing the behaviour to it outright. It is still important to mention that using std::function is not the perfect solution as it brings its own overhead. But for the purpose of readability and maintainability, this works really well.

There are two common ways to pass the functions. We can either write a normal function like we did in the inheritance code. Or we can make use of lambdas. The approach depends on the rest of your code architecture. Since I used the factory pattern to create objects, it made sense to use lambdas instead (see full game code for a better idea if you're curious). (As I was writing this, I found out that plain function pointers would have been more performant here. Since my lambdas capture nothing, they implicitly convert to function pointers anyways. But I had to spiritually move on from the game.)

This leads to unbelievably succinct code when creating actual powerups:

// NOTE: this is an illustrative example and not exactly how I created objects in my game.

auto doubleBar = PowerUp(12, 0x002B, 7,
                [](GameManager& game) { if (game.bar.nWidth <= 20) game.bar.transform(2); },
                [](GameManager& game) { if (game.bar.nWidth > 10) game.bar.transform(0.5); }
                );

auto doublePoints = PowerUp(10, 0x0058, 10,
                [](GameManager& game) { game.nScoreMultiplier *= 2; },
                [](GameManager& game) { game.nScoreMultiplier /= 2; }
                );

auto enhancedSpeed = PowerUp(12, 0x2192, 7,
                [](GameManager& game) { game.bar.nSpeed *= 1.25; },
                [](GameManager& game) { game.bar.nSpeed /= 1.25; }
                );
Enter fullscreen mode Exit fullscreen mode

Now, some readers may rightfully point to a little hypocrisy involved in the explanations: I claim that composition is better than inheritance, and it is being used anyways (PowerUp inherits Fallable). The point of this blog isn't to "never use inheritance", but to understand when it isn't the best option, and it often isn't. Using composition, I managed to remove an entire layer of inheritance. I gave it the necessary birth control before it spawned tens of powerups/debuffs (depending on the scale of the game), all for me to take care of. In case you didn't notice, each inherited powerup took about 20 lines of code. That is over 200 lines of redundancy and a maintainability nightmare.

And that is how I survived decapitation at the hands of Inheritance. Like always, I hope that you were able to inherit some new skills and ideas from this post. But then again, be careful of what you inherit :)

Here is the commit with the old approach if you are interested.
And this is the link to the latest game code. I plan to write about this game as a whole soon, so keep your eyes peeled for that.

Top comments (0)