Sunday, 30 August 2026

Favour Composition over inheritance

 Yes. This is a very common Java + design interview question, and the notes you pasted are correct but written in a difficult way.

Composition over Inheritance — Simple Explanation

The main idea is:

Prefer "has-a" relationships over "is-a" relationships when you want flexible code reuse.

1. Inheritance = "IS-A"

class AccountHelper {
    void deposit() {
        System.out.println("deposit");
    }
}

class SavingsAccount extends AccountHelper {
}

Here:

SavingsAccount IS-A AccountHelper

The child automatically gets the parent's implementation.

Problem

The relationship is tightly coupled.

SavingsAccount
       ↓ extends
AccountHelper

If AccountHelper changes, SavingsAccount can be affected.

Also, Java inheritance is essentially fixed by the class definition:

class SavingsAccount extends AccountHelper

You cannot decide later:

Today → use AccountHelper
Tomorrow → use EfficientAccountHelper

without changing the class design.


2. Composition = "HAS-A"

Instead of extending the helper, we inject it.

interface AccountHelper {
    void deposit(double amount);
    void withdraw(double amount);
}

Implementation:

class AccountHelperImpl implements AccountHelper {

    public void deposit(double amount) {
        System.out.println("depositing " + amount);
    }

    public void withdraw(double amount) {
        System.out.println("withdrawing " + amount);
    }
}

Now:

class Account {

    private AccountHelper helper;

    Account(AccountHelper helper) {
        this.helper = helper;
    }

    void deposit(double amount) {
        helper.deposit(amount);
    }
}

Now Account HAS-A AccountHelper.


3. The Big Advantage: Replace Implementation

Suppose we create another implementation:

class EfficientAccountHelperImpl implements AccountHelper {

    public void deposit(double amount) {
        System.out.println("efficient depositing " + amount);
    }

    public void withdraw(double amount) {
        System.out.println("efficient withdrawing " + amount);
    }
}

We can choose which implementation we want:

Account account =
    new Account(new AccountHelperImpl());

or:

Account account =
    new Account(new EfficientAccountHelperImpl());

We didn't change Account.

That's the important benefit.

                 Account
                    |
              HAS-A reference
                    |
            AccountHelper
              /        \
             /          \
AccountHelperImpl   EfficientAccountHelperImpl

4. Runtime Flexibility

This is what your book means by:

"Functionality is acquired dynamically at run-time."

For example:

AccountHelper helper;

if (fastMode) {
    helper = new EfficientAccountHelperImpl();
} else {
    helper = new AccountHelperImpl();
}

Account account = new Account(helper);

At runtime, we can select the implementation.

This is polymorphism + composition.


5. Why inheritance can be fragile

Imagine:

class Vehicle {
    void startEngine() {
        System.out.println("Starting engine");
    }
}

class ElectricCar extends Vehicle {
}

Later, the parent changes:

class Vehicle {
    void startEngine() {
        // new behavior
    }
}

That change can affect all subclasses.

             Vehicle
            /      \
           ↓        ↓
        Car       Truck
          ↓
     ElectricCar

A change at the top can potentially affect many classes below it.

This is called tight coupling.


6. Composition gives loose coupling

Instead:

interface Engine {
    void start();
}

Implementations:

class PetrolEngine implements Engine {
    public void start() {
        System.out.println("Petrol engine");
    }
}
class ElectricEngine implements Engine {
    public void start() {
        System.out.println("Electric engine");
    }
}

Car:

class Car {

    private Engine engine;

    Car(Engine engine) {
        this.engine = engine;
    }

    void start() {
        engine.start();
    }
}

Now:

Car petrolCar = new Car(new PetrolEngine());

Car electricCar = new Car(new ElectricEngine());

The Car doesn't care which engine implementation it receives.


7. Interview Answer ⭐

If Barclays interviewer asks:

"Why do you prefer composition over inheritance?"

You can answer:

I prefer composition when I need flexibility and loose coupling. Inheritance creates a strong compile-time relationship between the parent and child, so the subclass is tightly coupled to the parent's implementation. With composition, the class depends on an interface and gets the implementation through an object reference, usually using dependency injection. Therefore, implementations can be replaced without modifying the composed class. Composition also improves testability, because I can inject mock implementations, and it avoids problems caused by deep inheritance hierarchies.

Then give this example:

Inheritance:

Car extends Engine
     ↓
tight coupling


Composition:

Car HAS-A Engine
       ↓
     Engine
     /    \
Petrol   Electric

One-line answer to remember

Inheritance reuses implementation by extending a class; composition reuses behavior by delegating to another object. Composition is generally more flexible because the delegated implementation can be changed without changing the class that uses it.


⚠️ One important correction for interviews

Don't say:

"Never use inheritance."

That's not correct.

Inheritance is appropriate when there is a genuine IS-A relationship and the parent-child contract is stable.

For example:

class Animal {
    void eat() {}
}

class Dog extends Animal {
}

A Dog genuinely IS-A Animal.

Best rule

IS-A + stable relationship
        ↓
   Inheritance

HAS-A / interchangeable behavior
        ↓
    Composition

And a very good design principle to mention in a Barclays interview is:

"Favor composition over inheritance."

This is closely related to the Strategy Pattern, Dependency Injection, and SOLID's Dependency Inversion Principle.

No comments:

Post a Comment