Splitting the Hierarchy, and the Class Explosion That Follows
Dropping the canFly flag and splitting Bird into FlyingBird and NotFlyingBird actually fixes the penguin problem, until a second behavior shows up and the class count doubles.
The previous post left the canFly flag idea in a bad spot: it doesn't tell you what to write inside a penguin's fly() method, and nothing forces any caller to check the flag before calling it anyway. Time to try something more structural instead.
The Second Idea: Split Flying Birds From Non-Flying Birds
Drop the canFly flag entirely, and try splitting the hierarchy itself instead.
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 ~Bird() = default;
};
class FlyingBird : public Bird {
public:
virtual void fly() = 0;
};
class NotFlyingBird : public Bird {
// no fly() at all
};
class Crow : public FlyingBird {
public:
void fly() override { /* crow-specific flying */ }
};
class Penguin : public NotFlyingBird {
// nothing about flying to write, because there's nothing to promise
};Bird keeps only what every bird truly shares: eating and walking. Flying moves down one level, into a FlyingBird class that promises it, and Penguin never has to write a fly() method that lies about what it does. This actually solves the penguin problem. No mystery method body, no flag anyone has to remember to check.
Adding One More Behavior Breaks It Again
Then the requirement grows, the way it always does in a real interview. What if some birds can't walk either?
The instinct is to extend the same idea: split further. Four abstract classes now, one for every combination of flying and walking.
class FlyingWalkingBird : public Bird {
public:
virtual void fly() = 0;
virtual void walk() = 0;
};
class FlyingNotWalkingBird : public Bird {
public:
virtual void fly() = 0;
};
class NotFlyingWalkingBird : public Bird {
public:
virtual void walk() = 0;
};
class NotFlyingNotWalkingBird : public Bird {
// neither
};Every bird now picks the one combination class that matches what it can actually do. It works. It also creates two new problems, and they're worse than the one it just solved.
Problem One: Class Explosion
Two independent behaviors, flying and walking, need four classes: two states for flying, times two for walking. Add a third behavior, and the count doesn't grow by one. It doubles. Three behaviors need 2³ = 8 classes. Ten need 2¹⁰ = 1,024.
This has a name: class explosion. Nobody wants a job that's just clicking "New Class" in an IDE, over and over, to cover combinations nobody asked for by name.
Problem Two: There's No Single Type for "Birds That Can Fly"
Even setting the explosion aside, this design breaks something simpler: getting a plain list of "every bird that can fly."
Before splitting by walking too, that was just FlyingBird. Every flying bird, no matter which one, shared that one type. Now flying birds are split across two different classes, FlyingWalkingBird and FlyingNotWalkingBird, and a list can only hold one type at a time.
There's no single class left that means "any bird that can fly." The one thing this split was supposed to make easy is now impossible.
What "Substitutable" Actually Means
Both failed attempts point at the same underlying rule: the Liskov Substitution Principle, the L in SOLID.
The rule: wherever code expects an object of the parent type, any child class's object should work there too, without surprises. No exception the parent didn't declare. No new meaning quietly added to a method the parent already defined.
Here's a simple test: can any child take the parent's place, everywhere the parent is expected, with no surprises? If a caller needs to know exactly which child it has before it can safely call a method, the design has already failed.
The next post tests that same rule against a completely different example, one with no birds in it at all, before coming back to fix this hierarchy for good.
Keep reading