Home/Learn/Low Level Design/Composition over Inheritance

Composition over Inheritance

Intermediate
OOP & UML Fundamentals

Understand 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.

Requirements
// Problem: inheritance forces RubberDuck to deal with fly()
// Solution: extract FlyBehavior interface, compose it into Duck

Core 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.

Java — enums & interfaces
// ❌ 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.

Java — core classes
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 Aria
1

What is the fragile base class problem and how does composition solve it?

MediumGoogle
2

When is it acceptable to use inheritance instead of composition?

MediumAmazon
3

How does the Duck example relate to the Strategy design pattern?

EasyMicrosoft

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.

Loading discussion…