Composition vs. Inheritance
You'll learn to
- -Apply "favor composition over inheritance" with a concrete before/after refactor
- -Recognize the fragile-base-class and rigid-hierarchy problems inheritance-heavy designs run into during follow-up questions
"Favor composition over inheritance" is one of the most repeated pieces of OOP advice, and also one of the most commonly misapplied - candidates hear it as "never use inheritance," which is wrong. The real guidance is narrower: use inheritance for genuine is-a relationships with stable hierarchies, and use composition (an object holding a reference to another object, and delegating to it) for everything involving behavior that varies or combines independently.
Where Inheritance Breaks Down
Imagine modeling ducks: a Duck base class with a fly() method. Then a RubberDuck needs to exist, and it cannot fly - so you override fly() to do nothing, or throw an exception. Now every piece of code that calls fly() on a Duck has to worry about which subtype it actually got. This is the fragile base class problem: as new variants appear, the hierarchy either grows exception-riddled overrides or forces a redesign, because a single inheritance axis (Duck) cannot cleanly express two independent dimensions of variation (can-fly vs. cannot-fly, quacks-normally vs. squeaks).
# Before: behavior baked into the hierarchy, breaks for RubberDuck
class Duck:
def fly(self): print("Flying")
def quack(self): print("Quack")
class RubberDuck(Duck):
def fly(self): pass # awkward override - a RubberDuck can't fly
# After: fly and quack behavior are swappable, independent objects
class Duck:
def __init__(self, fly_behavior, quack_behavior):
self._fly_behavior = fly_behavior
self._quack_behavior = quack_behavior
def perform_fly(self): self._fly_behavior.fly()
def perform_quack(self): self._quack_behavior.quack()
rubber_duck = Duck(fly_behavior=NoFly(), quack_behavior=Squeak())When Inheritance Is Still the Right Call
Inheritance still earns its place when the hierarchy is genuinely stable and the is-a relationship is unlikely to need mixing-and-matching later - a CreditCard and DebitCard both being a Payment, for instance, where "how you pay" is not a combination of independent axes. The interview signal is not "did you use inheritance," it is "did you notice when a single hierarchy could not cleanly represent the actual variation in the requirements, and switch to composition instead of forcing it."
A practical smell test: if you find yourself writing an override that does nothing, throws, or exists only to "cancel out" a parent behavior, that is composition's cue to enter, not a sign you need a deeper hierarchy.
You are designing a notification system with Email, SMS, and Push channels, each of which can optionally be "urgent" (different formatting/delivery). How do you avoid an inheritance explosion (EmailUrgent, SMSUrgent, PushUrgent, ...)?
"I'd create a base Notification class and have each channel/urgency combination extend it as needed."
"Channel and urgency are two independent axes of variation, so I'd compose them instead: a Notification holds a DeliveryChannel and a UrgencyFormatter as separate objects rather than baking both into one inheritance chain. That way adding a fourth channel or a second urgency level is one new class, not a multiplication of subclasses."
What problem does the classic Duck/RubberDuck example illustrate?