Flyable: The Interface That Finally Fixes the Bird Hierarchy
Why classes were the wrong tool for splitting flying birds from non-flying ones, and how one small interface solves both remaining requirements at once.
The previous post ended with two requirements: no bird should ever have a fly() method it can't honestly implement, and there has to be one type that means "every bird that can fly." The FlyingBird / NotFlyingBird split met the first and failed the second.
The fix turns out to be a wrong-tool problem, not a missing-piece problem.
Classes Model Things. Interfaces Model Behavior.
Ask one question: what's actually different between FlyingBird and NotFlyingBird?
Nothing. Same name, same color, same age, same everything. Only one thing differs: a behavior.
That's the mistake. A class is supposed to model a real thing, with real attributes. Using a class just to say "this group can do X" is the wrong tool.
The right tool is an interface. Implement it, and you have the behavior. Don't, and you don't. No extra class needed.
The Actual Fix
Go back to one flat hierarchy. Bird stays abstract, holding only what's shared: name, color, type, age, plus eat() and walk().
class Bird {
public:
std::string name;
std::string color;
std::string type;
int age;
void eat() { /* shared across all birds */ }
void walk() { /* shared across all birds */ }
virtual ~Bird() = default;
};Every concrete bird extends Bird directly, no FlyingBird or NotFlyingBird layer in between. Then a separate interface marks the one behavior that actually varies:
class Flyable {
public:
virtual void fly() = 0;
virtual ~Flyable() = default;
};
class Crow : public Bird, public Flyable {
public:
void fly() override { /* crow-specific flying */ }
};
class Sparrow : public Bird, public Flyable {
public:
void fly() override { /* sparrow-specific flying */ }
};
class Pigeon : public Bird, public Flyable {
public:
void fly() override { /* pigeon-specific flying */ }
};
class Penguin : public Bird {
// no Flyable here, and so, correctly, no fly() method at all
};Penguin extends Bird and stops there. It never implements Flyable, so it never has to write a fly() method at all, empty, throwing, or otherwise.
That's the first requirement met, not worked around. A penguin has no flying behavior to fake.
Why Bird Is Still Abstract
Bird stays abstract here even though eat() and walk() don't strictly need to be. The real reason is different: does a plain new Bird() mean anything?
It doesn't. There's no such thing as a generic, unspecified bird, only crows, sparrows, penguins, and every other real kind. A class should be abstract whenever a bare instance of it would be meaningless, not only when it happens to hold an abstract method.
One List, Every Flying Bird, Nothing Else
This is the part that solves the second requirement. Crow, Sparrow, and Pigeon all implement Flyable, so all three can be treated as a Flyable, no matter which concrete bird each one really is.
std::vector<Flyable*> fliers = { &crow, &sparrow, &pigeon };
for (Flyable* bird : fliers) {
bird->fly();
}Penguin can't go in that list. It doesn't implement Flyable, same as you can't hand a UsbPort to code expecting a Database.
That's the missing piece from before. Flyable[] is one single type. It means "every bird that can fly," no matter how many classes actually implement it.
One thing to be clear on: you can never write new Flyable(). An interface has no object of its own. It's just a shape other classes agree to fill.
What's actually in that list are real objects: Crow, Sparrow, Pigeon. Each one is just referenced through the behavior they share. That's polymorphism again, the same trick as before, just on an interface instead of an abstract class.
Flyable, Walkable, Runnable. They name a capability, not a thing. A noun still compiles fine, a verb just reads better.When You Also Need the Opposite List: Marker Interfaces
Everything so far solves "give me every bird that can fly." It says nothing about "give me every bird that can't." There's no interface for not flying, so that absence isn't something you can query directly.
The fix, when this specific need actually shows up, is a marker interface: an interface with no methods at all, existing purely to tag membership in a group.
class NonFlyable {
public:
virtual ~NonFlyable() = default;
};
class Penguin : public Bird, public NonFlyable {
// still no fly(), and now also explicitly tagged as non-flying
};NonFlyable forces nothing. It has no methods to implement. Its only purpose is letting code write NonFlyable[] and mean something by it.
Keep reading