No Principle Is a Must: Common Sense Still Wins
A class doesn't have to represent a physical thing, none of the five SOLID principles are mandatory, and the real test for reaching for one is whether something can actually change.
The full evolution of the bird design ends on CrowOwlFlyingBehavior, a class that exists purely to hold shared flying logic. That class raises one honest question worth settling before treating any of these five principles as a formula.
A Class Doesn't Have to Represent a Physical Thing
Is CrowOwlFlyingBehavior really an entity, the way Crow or Penguin is? No, and that's fine. It has no attributes, no real-world existence, just a way of flying given a home in code.
An interface can describe a behavior, but it can't execute one. Something has to hold the real logic, and that something is still a class, even when it stands for an idea instead of a thing.
Not every class needs to be a noun you could point at.
None of These Five Principles Are Mandatory
After this many versions of one design, it's tempting to treat SOLID as a checklist every class must clear. That's not what these principles are. Each one is a strong default, not a strict rule.
The one thing you can't skip is common sense, applied to the case in front of you. Copying a decision from one part of a codebase to another just because it worked there isn't following a principle. It's skipping the thinking the principle was supposed to prompt.
Some Principles Carry Almost No Downside
The five principles aren't all equally safe to apply by default.
The Real Test: Can This Actually Change?
Here's the test that matters more than any single principle's name: is this relationship something that could realistically change or evolve later?
A booking system's Movie class talks to Actor directly, no interface between them. A movie having actors isn't a relationship likely to change. Wrapping it in an interface buys nothing.
Crow depends on FlyingBehavior through an interface. Different birds fly differently, and a bird's flying style might need to swap at runtime. That's a real reason to expect change.
The interface exists because a specific, real requirement called for it, not the principle for its own sake. A genuine reason to expect change is the test, not a guess that something might change someday.
The Bird Design Was Composition, All Along
One last thread worth pulling. Crow never inherited its flying logic. It held a FlyingBehavior object, and asked that object to do the work.
That's composition: building an object out of other objects it hands work to, instead of inheritance, where a class extends a parent's code.
Every fix from the Dependency Inversion post onward was really the same choice: composition over inheritance. A Crow that inherited its flying logic from some CrowOwlFlyingSuperclass would be stuck with that one way of flying forever.
A Crow that holds a swappable FlyingBehavior object can change what it does at runtime, with nothing more than a constructor argument. That flexibility is the real payoff this whole series was building toward, one small problem at a time.
Keep reading