Trade-offs: When to Use Inheritance vs Composition
The fundamental tension is between simplicity and flexibility. Inheritance provides elegant code reuse through hierarchies but creates tight coupling. Composition provides loose coupling and flexibility but requires more boilerplate and delegation.
First, natural polymorphism. Subclasses automatically work wherever parent is expected.
Second, less boilerplate. No need to write delegation methods.
Third, clear hierarchical relationships in domain modeling.
First, tight coupling. Changes to parent ripple to all children.
Second, fragile base class problem. Parent modifications break subclasses.
Third, inflexible. Cannot change inheritance at runtime or inherit from multiple classes (in most languages).
First, loose coupling. Components are independent and replaceable.
Second, runtime flexibility. Swap components without changing object type.
Third, better testability. Mock composed dependencies easily.
First, more boilerplate. Need explicit delegation methods.
Second, less intuitive for hierarchical domains.
Third, can create complex object graphs if overused.
First, the relationship passes the "is-a" test strictly. A SavingsAccount truly is an Account, not just behaves like one.
Second, the hierarchy is stable and unlikely to change. Core domain concepts like geometric shapes (Circle is a Shape) rarely need restructuring.
Third, you need polymorphic behavior across a clear type hierarchy. Method dispatch should work naturally without manual delegation.
Fourth, the Liskov Substitution Principle (LSP) holds. Any subclass can replace the parent without breaking correctness. If Circle cannot fully substitute Shape in all contexts, inheritance is wrong.
First, you need to change behavior at runtime. A Logger should compose a LogDestination (file, console, network) that can be swapped, not inherit from FileLogger.
Second, the relationship is about capability, not identity. A Bird that has FlyingBehavior is better than a hierarchy where Penguin awkwardly inherits fly() and throws exceptions.
Third, you need multiple sources of behavior. A Smartphone has a Camera, GPS, and Processor. Multiple inheritance is problematic, but composition handles this naturally.
Fourth, you want to follow the Dependency Inversion Principle (DIP). High-level modules should depend on abstractions. Composing interfaces provides this naturally.
If you are modeling a simple, stable domain like geometric shapes for a drawing application, inheritance is appropriate. A Circle is genuinely a Shape, the hierarchy will not change, and polymorphic rendering is exactly what you need. Creating a ShapeBehavior interface and composing it adds unnecessary complexity.
However, if shapes need different rendering strategies (wireframe, solid, textured), then compose a RenderStrategy instead of creating RenderableCircle, WireframeCircle subclasses.
First, you find yourself writing empty methods or throwing NotSupportedException. This violates LSP. A Penguin inheriting fly() from Bird and throwing an error means inheritance was the wrong choice.
Second, you want to change parent behavior for one child but not others. This indicates the parent is too broad or children are not true specializations.
Third, you have a combinatorial explosion of subclasses. If you need HybridCar, ElectricCar, GasCar, LuxuryHybridCar, LuxuryElectricCar, you should compose FuelType and LuxuryLevel instead.