Read This Before OOP-ing Your Project : The Curse of Inheritance.

read-this-before-oop-ing-your-project-:-the-curse-of-inheritance.

7 days in. 7 days of deep work. And OOP nearly killed my project.

The wildest part? I love OOP. I feel a bit betrayed.

Just last week, I wrote three articles celebrating the elegance of “alive” objects—how beautiful it is when data and behavior live in one composite.

So when I saw this tweet, I felt it—because I lived it.

Curse

OOP is the reason version 2 of my project exists…

v2

It got so messy I couldn’t fix it.

So yeah, think twice before going full OOP on a large project.

From Go to C++: Enter the Monster

At the moment I work in Go, a composition-first, interface-driven, lean and predictable. It’s great.

But then I decided to build a UI framework in C++, “C with Classes.”
I thought, hell yeah, baby! OOP time.

I don’t feel that way anymore.

The Curse of OOP: Polymorphism and Inheritance

Until you build a decent-sized project in a strongly typed OOP language, OOP feels like the promised land.

But the trouble starts with something innocent: containers. The humble array.

In JavaScript, you can chuck a chicken and an alligator into the same array, no sweat:

const chicken = new Chicken()
const alligator = new Alligator()
let env = [alligator, chicken]

But in lower-level languages? That’s “naturally” impossible.

OOP steps in like a hero:

“You want a mixed zoo? Just use polymorphism, bro!”

Polymorphism 101

Polymorphism says:

If two classes inherit from the same parent, and behave like that parent, they can exist side-by-side.

class animal {};
class chicken : public animal {};
class alligator : public animal {};
std::vector<animal> zoo;

The compiler doesn’t see a chicken or gator, it just sees animal.

so it’s ok

Image description

But here’s the problem:

As long as you’re in the zoo, you’re not a chicken. You’re just an animal.

class animal {
public:
    virtual ~animal() = default;
    virtual void walk() = 0;
};

class chicken : public animal {
    void walk() override {}
};

class alligator : public animal {
    void walk() override {}
};

std::vector<animal> zoo;

At first, it feels fine. Everyone walks.

But then… some animals evolve. They want specific behaviors.

class chicken : public animal {
    void walk() override {}
    void quack() { std::cout << "quack quackn"; }
};

class alligator : public animal {
    void walk() override {}
    void roll() { std::cout << "rollingn"; }
};

Now looping becomes awkward:

for (auto& c : zoo) {
    c.walk();     // 
    c.roll();     // ❌ I’m just an animal, bro.
}

Animals can only do what the base class says they can.

So what does OOP say?

“No worries! I’ve got a solution… for my own problem.”

Solution 1: Bloat the Base Class

The lazy fix: just shove every behavior into the base class.

class animal {
public:
    virtual void walk() = 0;
    virtual void quack() = 0;
    virtual void roll() = 0;
};

Now every subclass must pretend to implement everything, even if it makes zero sense.

Never do this. It’s insanity.

Solution 2: Premonition Design

Try to predict every behavior your objects might need.

class animal {
public:
    virtual void walk() = 0;
    virtual void makeNoise() = 0;   // covers quack/growl
    virtual void attack() = 0;      // roll/bite
};

Abstract, generic, flexible.

class chicken : public animal {
    void walk() override {}
    void makeNoise() override { std::cout << "quackn"; }
    void attack() override { std::cout << "fly-kick?n"; }
};

Problem is… I’m not a psychic. I don’t know how to premunite (is that a word?) every behavior upfront.

But this is exactly why UML diagrams exist! you’re just not thinking hard enough.

OOP is THE solution.
But then it needs extra, external solutions…
…to help shape a solution from within itself…
…on top of design patterns; which are also solutions for common problems to the original solution,
which itself is the solution!

Image description

may be a skill on my side.

Solution 3: Downcast Hell

The C++ way: casting. Treat the base class like a placeholder, then cast it back to the real class at runtime.

for (auto& c : zoo) {
      if (a->getTag() == "Chicken") {
        Chicken* chick = dynamic_cast<Chicken*>(a);
        chick->quack();
    }
    else if (a->getTag() == "Alligator") {
        // ...and 10 more casts later...
    }
}

Each subclass tags itself:

class chicken : public animal {
    std::string getTag() override { return "Chicken"; }
    void quack() { std::cout << "quackn"; }
};

But now you’ve got branches, casting, and runtime risks.
One or two classes? Fine. Ten+? Good luck.

The Refactoring Nightmare

I changed the base class once; and every subclass screamed at me.
Red squiggly lines everywhere.

That’s when it hit me:

OOP needs so many design patterns… because it keeps causing design problems.

Even some of the most goated engineers hate large-scale OOP. Now I understand why.

What I’ll Use Instead (Future Projects)

For future projects, here’s my hierarchy of sanity:

  1. Entity-Component-System (ECS) – Data-oriented, clean, plug-and-play.
  2. Procedural + “Dead Objects” – No behavior, just state.
  3. Unions / std::variant / Algebraic Types – Like enums on steroids.
  4. (Leaving this spot open for a new savior someday)
  5. OOP Inheritance – Only if I have to.

For my GUI project, I went with a sparse set ECS system.

Absolute game-changer. Like building with LEGO.

TL;DR

If you’re thinking about using OOP for your next big project—pause.
Especially in strongly typed languages like C++.

OOP is elegant in theory, but in practice?
You’ll find yourself solving the problems caused by the very paradigm you chose.

Just don’t say I didn’t warn you.

Total
0
Shares
Leave a Reply

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

Previous Post
say-goodbye-to-try/catch-blocks-with-the-ecmascript-?=-operator

Say Goodbye to Try/Catch Blocks with the ECMAScript ?= Operator

Next Post
future-proofing-your-b2b-web-apps:-why-cookie-consent-is-no-longer-a-‘set-it-and-forget-it’-task.

Future-Proofing Your B2B Web Apps: Why Cookie Consent Is No Longer a ‘Set It and Forget It’ Task.

Related Posts