Inheritance vs Composition
Inheritance creates an IS-A relationship: a Dog IS-A Animal. Composition creates a HAS-A relationship: a Car HAS-A Engine. The general guidance is βfavor composition over inheritance,β but thatβs not absolute β inheritance has legitimate uses. Knowing when to use which is what separates good from great OOP design.
Why this matters: In machine coding rounds, reaching for inheritance first often leads to rigid, hard-to-extend hierarchies. But avoiding it entirely means you miss cases where it genuinely simplifies things. Interviewers notice both extremes.
Prerequisites
- SOLID Principles β LSP guides inheritance decisions
- Basic OOP: abstract classes, interfaces, method overriding
The Decision Framework
flowchart TD
A["Is there a genuine IS-A relationship?"]:::client
B["Would Liskov Substitution hold?"]:::service
C["Are there fewer than 3 levels?"]:::data
D["Use Inheritance"]:::service
E["Use Composition"]:::data
F["Use Composition"]:::data
G["Use Composition"]:::data
A -->|Yes| B
A -->|No| E
B -->|Yes| C
B -->|No| F
C -->|Yes| D
C -->|No| G
classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0
When Inheritance Goes Wrong
// PROBLEM: Deep hierarchy with fragile base class
public class Vehicle {
public void startEngine() { /* ... */ }
public void accelerate() { /* ... */ }
public void brake() { /* ... */ }
}
public class Car extends Vehicle {
public void openTrunk() { /* ... */ }
}
public class ElectricCar extends Car {
@Override
public void startEngine() {
// Awkward: electric cars don't have engines
// But we inherit startEngine() from Vehicle
initBattery();
}
}
public class HybridCar extends Car {
// Now we need BOTH engine and battery behavior
// Inheritance can't represent "sometimes gas, sometimes electric"
}
// What about a bicycle? It's a vehicle but has no engine.
// The hierarchy forces us to either:
// 1. Have empty startEngine() implementations (LSP violation)
// 2. Restructure the entire hierarchy
# PROBLEM: class explosion from combining behaviors
class Animal:
def eat(self): pass
class FlyingAnimal(Animal):
def fly(self): pass
class SwimmingAnimal(Animal):
def swim(self): pass
class FlyingSwimmingAnimal(FlyingAnimal, SwimmingAnimal):
# Diamond inheritance -- which eat() gets called?
pass
# Want to add running? Now you need:
# RunningAnimal, FlyingRunningAnimal, SwimmingRunningAnimal,
# FlyingSwimmingRunningAnimal... exponential explosion
// PROBLEM: tight coupling to base class internals
class Stack : public vector<int> {
// Stack inherits ALL of vector's methods
// Users can call push_back(), insert(), operator[] -- breaking stack semantics
// This is Java's actual mistake with java.util.Stack extending Vector
public:
void push(int val) { push_back(val); }
int pop() {
int val = back();
pop_back();
return val;
}
// But nothing stops: stack[3] = 42; or stack.insert(begin(), 99);
};
The Composition Fix
// COMPOSITION: Behaviors as components, assembled freely
public interface PowerSource {
void start();
void stop();
boolean isRunning();
}
public class GasEngine implements PowerSource {
public void start() { /* ignition */ }
public void stop() { /* kill engine */ }
public boolean isRunning() { return ignitionOn; }
}
public class ElectricMotor implements PowerSource {
public void start() { /* activate motor controller */ }
public void stop() { /* deactivate */ }
public boolean isRunning() { return motorActive; }
}
public class HybridPower implements PowerSource {
private final GasEngine gas;
private final ElectricMotor electric;
private PowerMode mode = PowerMode.ELECTRIC;
public void start() {
if (mode == PowerMode.ELECTRIC) electric.start();
else gas.start();
}
// ...
}
// Vehicle composes its capabilities -- no rigid hierarchy
public class Vehicle {
private final PowerSource power;
private final BrakingSystem brakes;
private final Transmission transmission;
public Vehicle(PowerSource power, BrakingSystem brakes, Transmission transmission) {
this.power = power;
this.brakes = brakes;
this.transmission = transmission;
}
public void start() { power.start(); }
public void stop() { brakes.apply(); power.stop(); }
}
// Electric car: just different components, same Vehicle class
Vehicle electricCar = new Vehicle(new ElectricMotor(), new RegenerativeBrakes(), new SingleGear());
Vehicle gasCar = new Vehicle(new GasEngine(), new DiscBrakes(), new ManualTransmission());
Vehicle hybrid = new Vehicle(new HybridPower(), new RegenerativeBrakes(), new CVT());
# COMPOSITION: Capabilities as injected components
from abc import ABC, abstractmethod
class MovementCapability(ABC):
@abstractmethod
def move(self): ...
class FlyingCapability(MovementCapability):
def move(self):
return "flying through the air"
class SwimmingCapability(MovementCapability):
def move(self):
return "swimming through water"
class RunningCapability(MovementCapability):
def move(self):
return "running on land"
class Animal:
def __init__(self, name: str, capabilities: list[MovementCapability]):
self.name = name
self._capabilities = capabilities
def move(self):
for cap in self._capabilities:
print(f"{self.name} is {cap.move()}")
# No inheritance explosion -- just compose capabilities
duck = Animal("Duck", [FlyingCapability(), SwimmingCapability(), RunningCapability()])
fish = Animal("Fish", [SwimmingCapability()])
penguin = Animal("Penguin", [SwimmingCapability(), RunningCapability()])
// COMPOSITION: Stack wraps vector instead of inheriting
template<typename T>
class Stack {
vector<T> data_; // HAS-A, not IS-A
public:
void push(T val) { data_.push_back(std::move(val)); }
T pop() {
if (data_.empty()) throw underflow_error("Stack empty");
T val = std::move(data_.back());
data_.pop_back();
return val;
}
const T& top() const {
if (data_.empty()) throw underflow_error("Stack empty");
return data_.back();
}
bool empty() const { return data_.empty(); }
size_t size() const { return data_.size(); }
// No operator[], no insert, no random access -- stack semantics preserved
};
When Inheritance IS Appropriate
Inheritance works well when:
- True IS-A with Liskov Substitution: A
CheckingAccountIS-ABankAccountand can be used anywhere aBankAccountis expected without surprises. - Shallow hierarchy (2 levels max): Interface -> Concrete implementations.
- Template Method pattern: Shared algorithm skeleton with customizable steps.
- Framework extension points:
HttpServlet,TestCase,Activityβ designed for subclassing.
// GOOD inheritance: genuine IS-A, shallow, LSP-safe
public abstract class BankAccount {
protected Money balance;
public Money getBalance() { return balance; }
public void deposit(Money amount) {
if (amount.isNegative()) throw new IllegalArgumentException();
balance = balance.add(amount);
}
public abstract void withdraw(Money amount);
}
public class CheckingAccount extends BankAccount {
private final Money overdraftLimit;
@Override
public void withdraw(Money amount) {
if (balance.subtract(amount).isLessThan(overdraftLimit.negate())) {
throw new OverdraftLimitExceededException();
}
balance = balance.subtract(amount);
}
}
public class SavingsAccount extends BankAccount {
@Override
public void withdraw(Money amount) {
if (balance.subtract(amount).isNegative()) {
throw new InsufficientFundsException();
}
balance = balance.subtract(amount);
}
}
# GOOD: Interface inheritance (defining a contract)
class PaymentProcessor(ABC):
@abstractmethod
def charge(self, amount: Decimal, card: Card) -> TransactionId: ...
@abstractmethod
def refund(self, transaction_id: TransactionId) -> bool: ...
class StripeProcessor(PaymentProcessor):
def charge(self, amount, card):
# Stripe-specific implementation
pass
def refund(self, transaction_id):
# Stripe-specific refund
pass
class RazorpayProcessor(PaymentProcessor):
def charge(self, amount, card):
pass
def refund(self, transaction_id):
pass
// GOOD: Abstract interface with concrete implementations
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual void draw(Canvas& canvas) const = 0;
};
class Circle : public Shape {
double radius_;
public:
explicit Circle(double r) : radius_(r) {}
double area() const override { return M_PI * radius_ * radius_; }
double perimeter() const override { return 2 * M_PI * radius_; }
void draw(Canvas& canvas) const override { canvas.drawCircle(radius_); }
};
Quick Reference
| Question to Ask | If Yes -> | If No -> |
|---|---|---|
| Is this genuinely IS-A? | Consider inheritance | Use composition |
| Would LSP hold for all subtypes? | Inheritance is safe | Composition |
| Will the hierarchy stay shallow (2 levels)? | Inheritance is manageable | Composition |
| Do subtypes need to combine multiple behaviors? | Composition (avoids diamond) | Either works |
| Is behavior swappable at runtime? | Composition (inject new behavior) | Inheritance is fine |
| Are you modeling a framework extension point? | Inheritance (by design) | Composition |
When to Use vs When to Avoid
| Favor Composition | Favor Inheritance |
|---|---|
| Behaviors need to be mixed and matched | True IS-A relationship, LSP holds |
| You need runtime flexibility | Framework explicitly designed for subclassing |
| Hierarchy would go 3+ levels deep | Only 1 level of specialization (interface -> impl) |
| Multiple independent dimensions of variation | Shared algorithm skeleton (Template Method) |
Interview Questions
-
βWhy do people say βfavor composition over inheritanceβ?β β Inheritance creates tight coupling to the base class, makes hierarchies rigid, and leads to fragile base class problems. Composition is more flexible and avoids these issues.
-
βGive me a case where inheritance is clearly better.β β Implementing a strategy interface.
TokenBucketLimiter implements RateLimitAlgorithmis clean, shallow, and LSP-compliant. You wouldnβt use composition to avoid this. -
βHow do you refactor from inheritance to composition?β β Extract the varying behavior into an interface. The class that was a subclass now HAS the behavior object instead of being one. Replace
extendswith a constructor parameter. -
βWhatβs the diamond problem?β β When a class inherits from two classes that share a common ancestor, ambiguity arises about which parentβs method to use. Composition avoids this entirely.
See It in Action
All LLD problems use this decision. Parking Lot uses composition (strategies), while Vending Machine uses interface inheritance (state objects).