LLD
Machine Coding
Interview Course
Java โ€ข Interview Prep
๐Ÿงฑ OOP Fundamentals/

Composition Over Inheritance: Has-A vs Is-A

Lesson 7 of 8

Composition Over Inheritance: Has-A vs Is-A


What is it? (The Analogy)

Imagine you are building a character in a video game -- like a fantasy RPG. The inheritance approach would be to create a rigid class hierarchy: \Character -> Warrior -> SwordWarrior -> FireSwordWarrior\. But what if you want a Warrior who uses a bow sometimes? Or a Mage who also has a sword? The hierarchy locks you in. You cannot be a \FireSwordWarrior\ AND a \BowUser\ because Java only allows single inheritance. You end up with an explosion of classes: \FireSwordBowWarrior\, \IceMaceShieldTank\... it is madness.

Now imagine a different approach: your character HAS equipment slots. A weapon slot, an armor slot, a special ability slot. You can snap in any weapon: sword, bow, staff, hammer. You can swap them at RUNTIME. Want to switch from a fire sword to an ice bow mid-battle? Just swap the equipment. The character does not change -- only what it HOLDS changes. THAT is composition.

Composition means building complex objects by combining simpler objects instead of inheriting from a chain of parent classes. Instead of "a FireWarrior IS-A Warrior IS-A Character," you think "a Character HAS-A Weapon, HAS-A Armor, HAS-A SpecialAbility." The character delegates behavior to its components. This is more flexible, more testable, and less fragile than deep inheritance hierarchies. The Gang of Four said it best: "Favor composition over inheritance."


Why do we need it?

Let's see inheritance go WRONG. This is called the "fragile base class" problem:

java
1// Inheritance approach -- seems fine at first...
2public class Bird {
3    public void fly() {
4        System.out.println("Flapping wings and flying!");
5    }
6
7    public void eat() {
8        System.out.println("Eating...");
9    }
10}
11
12public class Penguin extends Bird {
13    // Uh oh... Penguins cannot fly!
14    @Override
15    public void fly() {
16        throw new UnsupportedOperationException("Penguins cannot fly!");
17    }
18    // This BREAKS the Liskov Substitution Principle!
19    // Any code that expects a Bird to fly() will CRASH on Penguin.
20}
21
22public class Ostrich extends Bird {
23    // SAME problem!
24    @Override
25    public void fly() {
26        throw new UnsupportedOperationException("Ostriches cannot fly!");
27    }
28}
29
30// Now what about a Duck? It can fly AND swim.
31// And a Chicken? It can sort-of fly. And a Bat? Wait, a bat is NOT a bird!
32// The hierarchy is crumbling!

With composition, you decompose behaviors into separate objects:

java
1// Composition approach -- flexible and correct!
2public interface FlyBehavior {
3    void fly();
4}
5
6public class FlapsToFly implements FlyBehavior {
7    public void fly() { System.out.println("Flapping wings and soaring!"); }
8}
9
10public class CannotFly implements FlyBehavior {
11    public void fly() { System.out.println("I stay on the ground."); }
12}
13
14public class GlidesShortDistance implements FlyBehavior {
15    public void fly() { System.out.println("Gliding a short distance..."); }
16}
17
18public class Bird {
19    private String name;
20    private FlyBehavior flyBehavior;  // HAS-A fly behavior
21
22    public Bird(String name, FlyBehavior flyBehavior) {
23        this.name = name;
24        this.flyBehavior = flyBehavior;
25    }
26
27    public void performFly() {
28        System.out.print(name + ": ");
29        flyBehavior.fly();  // DELEGATES to the behavior object
30    }
31
32    // Can swap behavior at RUNTIME!
33    public void setFlyBehavior(FlyBehavior newBehavior) {
34        this.flyBehavior = newBehavior;
35    }
36}
37
38// Now:
39Bird eagle = new Bird("Eagle", new FlapsToFly());
40Bird penguin = new Bird("Penguin", new CannotFly());
41Bird chicken = new Bird("Chicken", new GlidesShortDistance());
42
43eagle.performFly();    // "Eagle: Flapping wings and soaring!"
44penguin.performFly();  // "Penguin: I stay on the ground."
45chicken.performFly();  // "Chicken: Gliding a short distance..."
46
47// Baby eagle learning to fly? Start with CannotFly, upgrade later!
48Bird babyEagle = new Bird("Baby Eagle", new CannotFly());
49babyEagle.performFly();  // "Baby Eagle: I stay on the ground."
50babyEagle.setFlyBehavior(new FlapsToFly());  // It learned!
51babyEagle.performFly();  // "Baby Eagle: Flapping wings and soaring!"

No broken hierarchies. No exceptions. No Liskov violations. Pure flexibility.


How it works -- Step by Step

  1. Identify behaviors that vary -- What changes between objects? Flying, attacking, rendering, sorting? Those are candidates for composition.
  2. Extract behaviors into interfaces -- Create a small interface for each behavior (like \FlyBehavior\, \AttackBehavior\).
  3. Create concrete behavior classes -- Implement the interface in different ways (\FlapsToFly\, \CannotFly\, \JetPoweredFlight\).
  4. Compose objects with behaviors -- Give your main class a FIELD of the interface type. Initialize it in the constructor.
  5. Delegate to the behavior -- When the main class needs to perform that behavior, it calls the method on its behavior object.
  6. Allow runtime swapping -- Provide a setter so the behavior can change dynamically.

Let's Build It Together

Let's build a pizza shop where pizzas are COMPOSED of different components!

java
1// ===== BEHAVIOR INTERFACES =====
2
3// How the dough is made
4public interface DoughStyle {
5    String getDoughName();
6    void prepareDough();
7}
8
9// How the sauce is applied
10public interface SauceStyle {
11    String getSauceName();
12    void applySauce();
13}
14
15// How the pizza is cooked
16public interface CookingMethod {
17    String getMethodName();
18    void cook();
19    int getCookTimeMinutes();
20}
java
1// ===== CONCRETE BEHAVIORS =====
2
3public class ThinCrustDough implements DoughStyle {
4    public String getDoughName() { return "Thin Crust"; }
5    public void prepareDough() {
6        System.out.println("  Rolling dough paper-thin and crispy...");
7    }
8}
9
10public class ThickCrustDough implements DoughStyle {
11    public String getDoughName() { return "Thick Crust"; }
12    public void prepareDough() {
13        System.out.println("  Kneading thick, fluffy dough and letting it rise...");
14    }
15}
16
17public class StuffedCrustDough implements DoughStyle {
18    public String getDoughName() { return "Stuffed Crust"; }
19    public void prepareDough() {
20        System.out.println("  Rolling dough with mozzarella tucked into the edges...");
21    }
22}
java
1public class TomatoSauce implements SauceStyle {
2    public String getSauceName() { return "Classic Tomato"; }
3    public void applySauce() {
4        System.out.println("  Spreading tangy tomato sauce in a spiral...");
5    }
6}
7
8public class PestoSauce implements SauceStyle {
9    public String getSauceName() { return "Basil Pesto"; }
10    public void applySauce() {
11        System.out.println("  Brushing on fresh green basil pesto...");
12    }
13}
14
15public class BBQSauce implements SauceStyle {
16    public String getSauceName() { return "Smoky BBQ"; }
17    public void applySauce() {
18        System.out.println("  Drizzling smoky BBQ sauce in a zig-zag pattern...");
19    }
20}
java
1public class BrickOvenCooking implements CookingMethod {
2    public String getMethodName() { return "Brick Oven"; }
3    public void cook() {
4        System.out.println("  Sliding into a 900F brick oven... charred perfection!");
5    }
6    public int getCookTimeMinutes() { return 3; }
7}
8
9public class ConveyorOvenCooking implements CookingMethod {
10    public String getMethodName() { return "Conveyor Oven"; }
11    public void cook() {
12        System.out.println("  Riding the conveyor belt through a steady 475F oven...");
13    }
14    public int getCookTimeMinutes() { return 12; }
15}
java
1// ===== THE COMPOSED CLASS =====
2// Pizza does NOT inherit from Dough or Sauce.
3// Pizza HAS-A dough, HAS-A sauce, HAS-A cooking method.
4public class Pizza {
5    private String name;
6    private DoughStyle dough;         // HAS-A dough
7    private SauceStyle sauce;         // HAS-A sauce
8    private CookingMethod cooking;    // HAS-A cooking method
9    private List<String> toppings;
10
11    public Pizza(String name, DoughStyle dough, SauceStyle sauce,
12                 CookingMethod cooking) {
13        this.name = name;
14        this.dough = dough;
15        this.sauce = sauce;
16        this.cooking = cooking;
17        this.toppings = new ArrayList<>();
18    }
19
20    public Pizza addTopping(String topping) {
21        toppings.add(topping);
22        return this;  // Fluent API -- for chaining!
23    }
24
25    // Delegates to composed objects!
26    public void makePizza() {
27        System.out.println("\n===== Making: " + name + " =====");
28        System.out.println("Dough: " + dough.getDoughName());
29        dough.prepareDough();
30
31        System.out.println("Sauce: " + sauce.getSauceName());
32        sauce.applySauce();
33
34        System.out.println("Toppings: " + toppings);
35
36        System.out.println("Cooking: " + cooking.getMethodName()
37            + " (" + cooking.getCookTimeMinutes() + " min)");
38        cooking.cook();
39
40        System.out.println("===== " + name + " is ready! =====");
41    }
42
43    // Can change cooking method at runtime!
44    public void setCookingMethod(CookingMethod cooking) {
45        this.cooking = cooking;
46    }
47}
java
1public class PizzaShopDemo {
2    public static void main(String[] args) {
3
4        // Mix and match ANY combination of behaviors!
5        Pizza margherita = new Pizza("Margherita",
6            new ThinCrustDough(),
7            new TomatoSauce(),
8            new BrickOvenCooking()
9        );
10        margherita.addTopping("Fresh Mozzarella")
11                   .addTopping("Basil Leaves");
12        margherita.makePizza();
13
14        Pizza bbqChicken = new Pizza("BBQ Chicken",
15            new ThickCrustDough(),
16            new BBQSauce(),
17            new ConveyorOvenCooking()
18        );
19        bbqChicken.addTopping("Grilled Chicken")
20                   .addTopping("Red Onion")
21                   .addTopping("Cilantro");
22        bbqChicken.makePizza();
23
24        Pizza gourmet = new Pizza("Gourmet Pesto",
25            new StuffedCrustDough(),
26            new PestoSauce(),
27            new BrickOvenCooking()
28        );
29        gourmet.addTopping("Sun-dried Tomatoes")
30                .addTopping("Goat Cheese")
31                .addTopping("Arugula");
32        gourmet.makePizza();
33
34        // Change cooking method at runtime!
35        System.out.println("\nOven broke! Switching to conveyor...");
36        gourmet.setCookingMethod(new ConveyorOvenCooking());
37        gourmet.makePizza();
38
39        // With inheritance, each combination would need its OWN class:
40        // ThinCrustTomatoBrickOvenPizza
41        // ThickCrustBBQConveyorPizza
42        // StuffedCrustPestoBrickOvenPizza
43        // That is 3 x 3 x 2 = 18 classes! With composition: 3 + 3 + 2 = 8 classes.
44        // The more options you have, the more composition WINS.
45    }
46}

The Big Aha! With inheritance, combining 4 dough types x 3 sauce types x 3 cooking methods = 36 classes (one for each combination). With composition, you need only 4 + 3 + 3 = 10 classes total, and you can create all 36 combinations (and more!) by mixing and matching. This is why composition scales and inheritance does not.


Visual Mental Model

java
1    INHERITANCE APPROACH (rigid, explosive)
2    +--------+
3    | Pizza  |
4    +--------+
5        |
6    +---+---+---+---+
7    |Thin|Thick|Stuffed|
8    +-+--+-+--+-+----+
9      |    |    |
10    Each needs sub-branches for sauces...
11    then each of THOSE needs sub-branches for cooking...
12    = 3 x 3 x 2 = 18 leaf classes. EXPLOSION!
13
14
15    COMPOSITION APPROACH (flexible, scalable)
16    +---------------------------------------------+
17    |                   Pizza                      |
18    |---------------------------------------------|
19    |  +----------+  +-----------+  +-----------+ |
20    |  | dough ---|->| ThinCrust |  | (or Thick)| |
21    |  +----------+  +-----------+  +-----------+ |
22    |                                              |
23    |  +----------+  +-----------+  +-----------+ |
24    |  | sauce ---|->| Tomato    |  | (or Pesto)| |
25    |  +----------+  +-----------+  +-----------+ |
26    |                                              |
27    |  +----------+  +-----------+  +-----------+ |
28    |  | cooking -|->| BrickOven |  | (or Conv.)| |
29    |  +----------+  +-----------+  +-----------+ |
30    +---------------------------------------------+
31
32    Snap in any combination! Swap at runtime!
33    New dough type? Add ONE class. Nothing else changes.

Real-World Analogy Recap

Composition is like building with LEGO bricks. Each brick is a small, self-contained piece with a standard interface (the studs on top, the holes on bottom). You can combine bricks in infinite ways to build castles, spaceships, or cities. You do not need a pre-designed "CastleBrick" or "SpaceshipBrick" -- you just combine general-purpose bricks creatively. If a piece breaks, you swap it out without rebuilding everything. Compare this to a carved wooden sculpture (inheritance) where changing one part means re-carving the whole thing. LEGO wins for flexibility every time.


Common Mistakes & Gotchas

  • Thinking composition means NEVER using inheritance: That is not true! Inheritance is great for genuine IS-A relationships and when you want to share implementation. The rule is "favor" composition, not "exclusively use" composition. Use inheritance when the relationship is truly hierarchical and stable.
  • Over-composing trivial things: Not every field is "composition." A \String name\ field in a Person class is just a field, not a design decision. Composition as a pattern matters when you are extracting *behaviors* that might vary.
  • Forgetting to delegate: If you compose a behavior but then re-implement it inside the main class anyway, you have gained nothing. The whole point is to DELEGATE to the component.
  • Tight coupling to concrete types: Always type your composition fields as INTERFACES, not concrete classes. Write \private FlyBehavior fly\, not \private FlapsToFly fly\. Otherwise you lose the flexibility.
  • When inheritance IS the right choice: If the relationship is truly "is-a" and will not change (a Car IS-A Vehicle), inheritance is simpler and more intuitive. Do not force composition where inheritance naturally fits.

Interview Tip

"Favor composition over inheritance" is one of the most quoted design principles. When an interviewer asks about it, explain the trade-offs clearly: "Inheritance creates a tight coupling between parent and child -- changes to the parent ripple through all children (fragile base class problem). Composition creates loose coupling -- components can be swapped, tested independently, and combined freely. Inheritance is compile-time fixed; composition allows runtime flexibility. I use inheritance for stable IS-A hierarchies and composition for varying behaviors that need to be mixed and matched." Then show the class-count math: N*M combinations via inheritance vs N+M classes via composition.


Quick Quiz

  1. You are designing a game character with a movement style (walk, fly, swim, teleport) and an attack style (melee, ranged, magic). Using inheritance, how many leaf classes would you need for all combinations? Using composition, how many total classes?
  2. What is the "fragile base class" problem? Give a concrete example of how changing a base class could break a subclass.
  3. Can you use BOTH inheritance AND composition in the same design? Give an example where they complement each other.

Summary -- Key Takeaways

  • "Favor composition over inheritance" means preferring HAS-A relationships (a character HAS-A weapon) over IS-A hierarchies (a SwordCharacter IS-A Character) for behaviors that vary.
  • Composition uses delegation: the main class holds a reference to a behavior interface and calls methods on it, rather than inheriting the behavior.
  • Composition is more flexible (swap at runtime), more scalable (N+M vs N*M classes), and more testable (test components in isolation).
  • Inheritance is still valuable for genuine IS-A relationships that are stable and do not explode into combinations.