Inheritance, composition and polymorphism
Part of the free Low-Level Design course on LogicWiz, in the chapter: Object-oriented basics, the way interviewers check them. The class-diagram exercises run in the app.
Week 2, Day 4: The subclass that broke by itself
"You did not touch AuditedCart. Someone changed Cart, and AuditedCart broke anyway."
Priya, reading a failed test report
Is a, or has a
Inheritance lets one class (the child, or subclass) take the fields and methods of another class (the parent, or superclass). In Java you write extends. Composition means one class holds another object in a field and calls its methods. Both let you reuse code, but they say different things about your design.
- Inheritance says is a. An
SmsNotificationis aNotification. Anywhere the code expects aNotification, anSmsNotificationmust work. - Composition says has a. An
Orderhas aDeliverySlot. A slot is not an order; the order uses one.
Arjun had a different rule: "Inheritance is how you reuse code. If two classes share methods, give them a parent." Priya said this rule causes most of the bad class hierarchies she reviews. Shared code is a reason to share an object. It is not a reason to claim that one class is a kind of another.
Her test is to say "every X is a Y" out loud. Then check that it holds everywhere a Y is used. "Every rider is a customer" fails: a rider does not have a cart or a wallet. If riders and customers both need a name and a phone number, give each of them a Contact object.
A warning sign appears in code too. If a subclass has to throw "not supported" for a method of its parent, the is-a test has failed. Module 4 gives this rule a name.
The fragile base class
Arjun wanted every cart change counted for an audit. He extended Cart.
public class Cart {
protected final List<Item> items = new ArrayList<>();
public void add(Item item) { items.add(item); }
public void addAll(List<Item> more) {
for (Item i : more) add(i); // calls add() once per item
}
}
public class AuditedCart extends Cart {
private int changes = 0;
@Override
public void add(Item item) {
changes++;
super.add(item);
}
}
At first he also overrode addAll to add more.size() to the count. Three items then counted as six. His addAll counted three, and Cart.addAll called add, which Java sends to the overriding add in AuditedCart. That counted three more. So Arjun deleted his addAll and kept the code above. It worked, but only because Cart.addAll happened to call add.
Two months later a teammate changed Cart.addAll to items.addAll(more), which is faster. Now bulk adds were not counted at all. Nobody touched AuditedCart, yet it broke.
This is the fragile base class problem. A subclass depends on how its parent works inside. So a change to the parent that looks safe breaks the child. Inheritance exposes those inside details, such as the protected list and which method calls which, to every subclass.
Favour composition
Priya's fix: hold a Cart instead of being one.
public class AuditedCart {
private final Cart cart;
private int changes = 0;
public AuditedCart(Cart cart) { this.cart = cart; }
public void add(Item item) {
cart.add(item);
changes++;
}
public void addAll(List<Item> more) {
cart.addAll(more);
changes += more.size();
}
}
AuditedCart now uses only the public methods of Cart. When Cart.addAll calls add, it calls its own add, not ours. So the count stays right however Cart works inside. This is called delegation: an object passes work to another object that it holds.
"Favour composition over inheritance" is a common guideline. It does not mean never inherit. Inheritance fits when the is-a test holds, when the parent was designed to be extended, and when your team owns both classes. A deep chain such as Order, then GroceryOrder, then ExpressGroceryOrder, then ExpressFragileGroceryOrder is the shape to avoid.
Composition also lets behaviour change while the program runs. An object's class is fixed when it is created. A field can be replaced. You will see this with the delivery fee below.
⚠️ Warning: A Java class can have only one parent class. If you inherit only to reuse code, you spend that one parent on reuse, and cannot use it later for a real is-a.
Polymorphism removes the switch on type
Now the delivery fee. Here is Arjun's code.
public long deliveryFeePaise(Order order) {
switch (order.getDeliveryType()) {
case "EXPRESS": return 4900;
case "SCHEDULED": return order.totalPaise() > 19900 ? 0 : 2500;
case "PICKUP": return 0;
default: throw new IllegalArgumentException("Unknown type");
}
}
The same switch appears again in the ETA screen, the invoice and the rider app. Adding "night delivery" means finding and editing all four. Miss one, and customers hit the default exception in production.
Polymorphism means one method call that runs different code depending on the object's actual class, chosen while the program runs. Each delivery type becomes a class behind one interface.
public interface DeliveryOption {
long feePaise(Order order);
int etaMinutes();
}
public class ExpressDelivery implements DeliveryOption {
public long feePaise(Order order) { return 4900; }
public int etaMinutes() { return 10; }
}
public class ScheduledDelivery implements DeliveryOption {
public long feePaise(Order order) { return order.totalPaise() > 19900 ? 0 : 2500; }
public int etaMinutes() { return 120; }
}
The order holds a DeliveryOption in a field, which is composition again, and calls delivery.feePaise(this). Night delivery becomes one new class. All the rules for one delivery type now sit together in one place.
How does Java know which feePaise to run? It looks at the class of the actual object, not the type of the variable. A DeliveryOption variable that holds an ExpressDelivery runs the express code. This choice at run time is called dynamic dispatch. If a customer switches from express to scheduled, the order replaces its DeliveryOption field, and no if is needed.
In an interview, say the reason for your choice: "I used composition because a delivery option is not an order." Interviewers grade the reason, not only the arrow on the board.
A switch is not always wrong. A switch on an enum, written in one place, over values that rarely change, is clear and short. Replace it when the same switch on type appears in several places, or when new types arrive often.
Try it: replace subclasses with a delivery option
The starter models express and scheduled orders as subclasses of Order, plus a type string. Redesign it so that Order has a delivery option, and each kind of delivery is a class behind one interface.
📌 Summary: Inherit only for a true is-a, because subclasses break when parent internals change. Prefer holding an object (composition). Replace a repeated switch on type with classes behind one interface.
What Arjun learns next
Arjun now designs with interfaces and composition, but his diagrams are messy sketches. Next week: how to draw a class box that any interviewer can read in seconds.
Check your understanding
- Arjun makes Rider extend Customer because both have a name, a phone and an address. What is wrong?
- AuditedCart overrides add() and counts changes. A teammate changes Cart.addAll() to stop calling add(), and bulk adds stop being counted. What is this called?
- The same switch on delivery type appears in the fee, ETA and invoice code. SwiftKart adds night delivery. Which design handles this best?
- When is a switch on an enum a reasonable choice?