The Penguin That Can't Fly, and a Boolean That Doesn't Fix It
The zoo now has birds that can't fly. The obvious fix, a canFly flag on the base class, creates two new problems instead of solving one.
The previous post left Bird in decent shape. Adding a new bird means writing one new class, nothing else. Every bird is responsible only for its own flying, nothing more.
Then the interviewer adds one more requirement. Some birds in this zoo can't fly at all.
Not Every Bird Flies
Penguins are birds. So are ostriches. Neither one flies. The zoo needs to hold them too, using the exact same Bird hierarchy already built.
This isn't a new attribute to bolt on. It's a bird missing something every other bird in the hierarchy has. Every subclass so far has been required, by the compiler itself, to implement fly(). Now there's a bird that has no real flying behavior to put there.
The First Idea: Add a canFly Flag
The obvious fix looks like this: give Bird a canFly boolean.
class Bird {
public:
double weight;
int age;
std::string color;
std::string type;
bool canFly;
void eat() { /* same for every bird */ }
void walk() { /* same for every bird */ }
virtual void fly() = 0;
virtual ~Bird() = default;
};
class Penguin : public Bird {
public:
void fly() override {
// ...what actually goes here?
}
};Adding canFly doesn't remove the need for fly(). Bird still declares it as abstract, so Penguin still has to write a fly() method, whether or not a penguin can fly. The question just moves. It doesn't go away: what code actually goes inside Penguin.fly()?
Problem One: The Method Body Is Still a Mystery
A canFly flag tells you whether a bird can fly. It says nothing about what fly() should do for a bird that can't.
Leave the method empty? Throw an exception? Neither one is actually a good answer. Each one just trades this problem for a new one.
Adding the flag didn't answer the question. It just made the question easier to postpone.
Problem Two: Nothing Forces Anyone to Check the Flag
The second problem is worse, and it shows up on the calling side, not inside Penguin.
for (Bird* bird : birds) {
bird->fly(); // should this have checked bird->canFly first?
}For this to be safe, every single place in the codebase that calls fly() now has to remember to check canFly first.
Nothing enforces that check. It lives only in a comment, or in someone's memory, or in documentation nobody reads before calling the method. Forget the check once, anywhere in a large codebase, and you're calling fly() on a bird that was never meant to fly. Whatever placeholder code got left inside Penguin.fly() runs anyway.
A Bird class that has fly() as abstract was built to guarantee every bird can fly. Adding a canFly flag doesn't get that guarantee back. It just makes the lie official, and hands the risk to every single caller instead of the compiler.
A method existing isn't the same as a method behaving the way its parent class promised.
Penguin.fly() exists. The code compiles fine. But it breaks the one promise that method was supposed to keep. That gap, between code that compiles and code that actually keeps its promise, is exactly what the Liskov Substitution Principle exists to catch.
The next post drops the flag entirely and tries something more structural, before landing on why that attempt has its own, worse problem.
Keep reading