Composition over Inheritance
IntermediateUnderstand why has-a relationships are more flexible than is-a, illustrated by the classic Duck problem refactored with FlyBehavior Strategy.
Overview
The principle "Favor composition over inheritance" (GoF, Effective Java Item 18) addresses the fragile base class problem: changes to a parent class can break all subclasses, and subclasses inherit methods they may not need. The classic Duck example shows how a RubberDuck that extends Duck inherits a fly() method it should not have. The fix is to extract the varying behaviour (flying) into a FlyBehavior interface, compose it into Duck, and inject the appropriate implementation. This is exactly the Strategy pattern applied to class design. Key rule: use inheritance only for true is-a relationships with stable hierarchies; use composition for behaviour that varies independently.
Requirements Analysis
The Duck hierarchy problem: all ducks quack but not all ducks fly (RubberDuck), and not all ducks quack the same way (MuteDuck). A fly() method in the base Duck class forces all subclasses to either inherit unwanted behaviour or override with an empty/throws body — both are code smells.
// Problem: inheritance forces RubberDuck to deal with fly()
// Solution: extract FlyBehavior interface, compose it into DuckCore Classes & Relationships
FlyBehavior interface: fly(). Implementations: FlyWithWings, FlyNoWay (rubber/wooden duck), FlyRocketPowered. QuackBehavior interface: quack(). Implementations: NormalQuack, Squeak (rubber duck), MuteQuack. Duck abstract class holds FlyBehavior and QuackBehavior fields and delegates performFly() and performQuack() to them.
// ❌ Inheritance problem: RubberDuck should not fly
public abstract class Duck {
public void quack() { System.out.println("Quack"); }
public void fly() { System.out.println("I can fly!"); } // RubberDuck inherits this!
public abstract void display();
}
public class RubberDuck extends Duck {
@Override public void fly() { /* do nothing — rubber ducks don't fly */ }
@Override public void quack() { System.out.println("Squeak"); }
@Override public void display() { System.out.println("I'm a rubber duck"); }
// Problem: every new Duck type requires evaluating which inherited methods to suppress
}
// ✅ Composition solution: extract FlyBehavior and QuackBehavior
public interface FlyBehavior { void fly(); }
public interface QuackBehavior { void quack(); }
public class FlyWithWings implements FlyBehavior {
@Override public void fly() { System.out.println("Flying with wings!"); }
}
public class FlyNoWay implements FlyBehavior {
@Override public void fly() { System.out.println("Cannot fly."); }
}
public class FlyRocketPowered implements FlyBehavior {
@Override public void fly() { System.out.println("Flying with rocket power!"); }
}
public class NormalQuack implements QuackBehavior {
@Override public void quack() { System.out.println("Quack!"); }
}
public class Squeak implements QuackBehavior {
@Override public void quack() { System.out.println("Squeak!"); }
}
public class MuteQuack implements QuackBehavior {
@Override public void quack() { System.out.println("...silence..."); }
}Java Implementation
Duck abstract class composes FlyBehavior and QuackBehavior. MallardDuck gets FlyWithWings + NormalQuack. RubberDuck gets FlyNoWay + Squeak. Behaviours are injectable and swappable at runtime — a duck can learn to fly with a rocket without changing its class.
public abstract class Duck {
protected FlyBehavior flyBehavior;
protected QuackBehavior quackBehavior;
// Inject behaviours via constructor
protected Duck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
this.flyBehavior = flyBehavior;
this.quackBehavior = quackBehavior;
}
// Delegates to composed behaviour — no inheritance of unwanted code
public void performFly() { flyBehavior.fly(); }
public void performQuack() { quackBehavior.quack(); }
// Behaviours swappable at runtime!
public void setFlyBehavior(FlyBehavior fb) { this.flyBehavior = fb; }
public void setQuackBehavior(QuackBehavior qb){ this.quackBehavior = qb; }
public abstract void display();
}
public class MallardDuck extends Duck {
public MallardDuck() { super(new FlyWithWings(), new NormalQuack()); }
@Override public void display() { System.out.println("I'm a Mallard duck"); }
}
public class RubberDuck extends Duck {
public RubberDuck() { super(new FlyNoWay(), new Squeak()); }
@Override public void display() { System.out.println("I'm a Rubber duck"); }
}
public class ModelDuck extends Duck {
public ModelDuck() { super(new FlyNoWay(), new MuteQuack()); }
@Override public void display() { System.out.println("I'm a Model duck (display only)"); }
}
// Usage
Duck mallard = new MallardDuck();
mallard.display(); // I'm a Mallard duck
mallard.performFly(); // Flying with wings!
mallard.performQuack(); // Quack!
Duck rubber = new RubberDuck();
rubber.display(); // I'm a Rubber duck
rubber.performFly(); // Cannot fly.
rubber.performQuack(); // Squeak!
// Runtime behaviour swap — ModelDuck gets a rocket
Duck model = new ModelDuck();
model.performFly(); // Cannot fly.
model.setFlyBehavior(new FlyRocketPowered());
model.performFly(); // Flying with rocket power!Key Points to Remember
- 1Fragile base class: changing a parent method can silently break all subclasses that relied on the old behaviour.
- 2Composition is more flexible: behaviours are injectable, independently testable, and swappable at runtime.
- 3Use inheritance only for stable is-a relationships where the subclass truly is a specialisation of the parent.
- 4This example IS the Strategy pattern: FlyBehavior is the strategy; Duck is the context; FlyWithWings is the concrete strategy.
Interview Questions
Sign in to ask AriaWhat is the fragile base class problem and how does composition solve it?
When is it acceptable to use inheritance instead of composition?
How does the Duck example relate to the Strategy design pattern?
Ask Aria about Composition over Inheritance
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.