The Open-Closed Principle, and a Bird Class That Can't Stop Changing
Why the one-class Bird design breaks a second rule on top of SRP, and how pulling flying out into subclasses fixes both at once.
The previous posts found five problems inside one Bird class trying to hold every kind of flying, and three concrete signs for spotting that same mistake anywhere. All of it traced back to one cause: the class had more than one responsibility.
That same one-class Bird breaks a second, separate rule too. This one is the O in SOLID: the Open-Closed Principle.
Open for Extension, Closed for Modification
The Open-Closed Principle says this: your code should be open for extension, but closed for modification.
Break that into its two halves.
Open for extension means it should be easy to add new features. Closed for modification means adding those new features shouldn't require changing code that's already written and already working.
Put both halves together, and here's what the principle is actually asking for: when you add something new, you should mostly be writing new code, new classes, new methods, not editing old code that other things already depend on.
Why This Rule Exists
Two real problems show up the moment you edit code that's already working, instead of just adding to it.
Avoid editing old code, and you avoid both problems at once. New code gets new tests. Old code, untouched, keeps passing the tests it already had.
The One-Class Bird Fails This Too
Go back to the one-class Bird from the last post. What happens when a new bird gets added to the zoo?
You open Bird. You add a new branch to its fly() method. You just edited code that every other bird's flying already depends on, to add a feature that has nothing to do with them.
That's the Open-Closed Principle broken in one sentence: adding a new bird requires modifying a class that already works, instead of just adding something new next to it.
Building Version One: Move the Flying Out
The fix starts with one question: if fly() shouldn't live inside Bird, where does it go?
It goes into a separate class per bird type. A Crow class holds crow's flying code. A Sparrow class holds sparrow's flying code. Each type owns exactly the one behavior that's actually different about it.
Everything that stays the same across every bird, weight, color, age, how a bird eats, how a bird walks, stays in Bird. What changes per bird moves out. What doesn't, stays put.
class Bird {
public:
double weight;
int age;
std::string color;
std::string type;
void eat() { /* same for every bird */ }
void walk() { /* same for every bird */ }
virtual void fly() = 0;
virtual ~Bird() = default;
};
class Crow : public Bird {
public:
void fly() override { /* crow-specific flying */ }
};
class Sparrow : public Bird {
public:
void fly() override { /* sparrow-specific flying */ }
};
class Owl : public Bird {
public:
void fly() override { /* owl-specific flying */ }
};Bird becomes abstract, and fly() becomes an abstract method inside it. That's not decoration. It does real work: it forces every subclass to write its own fly(), or the code won't compile. Bird can't force a promise like that through a plain attribute. Only an abstract method makes "every bird must define how it flies" a rule the compiler checks for you.
Bird still has to be a class, not an interface, because it needs to hold real data: weight, age, color. Interfaces can't hold that. This is the same shape you'll see again and again in later posts, an abstract class carries shared state, an interface carries a pure contract.
Why This Uses Polymorphism
Here's how you'd actually use it:
Bird* b = new Crow();
b->fly();b is declared as Bird, but it's actually a Crow underneath. Call b.fly(), and the Crow version runs, not some generic default. That's polymorphism: the exact method that runs is decided by the real, underlying type of the object, not by the type written on the variable.
This is what makes the whole design work. Code that only knows about Bird can still call fly() on any bird, crow, sparrow, owl, or one added next year, and always get the right behavior back, without ever needing to know which subclass it's actually holding.
Keeping a type Field, Even With Subclasses
One detail worth calling out: Bird still keeps a plain type attribute, a string, even though every bird is now its own subclass.
Why keep it, when the class hierarchy already tells you what kind of bird an object is? Because sometimes code genuinely needs to branch on type directly, and a type field makes that check simple and readable: if (bird.type === "crow") { ... }.
The alternative is checking with instanceof (if (bird instanceof Crow)), and that's usually a worse choice. Checks like that get harder to read and easier to get wrong as a class hierarchy grows. A plain type attribute is the more common, easier-to-read choice whenever an entity has multiple subtypes, and you'll see this same pattern again in other systems: a User class with a type of student or instructor, for instance.
Checking This Design Against Both Rules
Run this new design against both principles from scratch.
Does it follow the Open-Closed Principle? Add a new bird now, and all you do is write one new class extending Bird. Nothing already written gets touched. Yes, it holds.
Does it follow the Single Responsibility Principle? Bird is now responsible only for what every bird genuinely shares. Each subclass is responsible only for its own specific flying. Yes, it holds too.
One class, two rules, both fixed by the same move: pulling out what varies, and giving it its own home.
That still isn't the end of the interview, though. The next requirement is going to break this design in a completely new way, and it starts with a bird that can't do the one thing fly() promises it can.
Keep reading