Skip to main content
CleanCodeMastery

Replace Inheritance with Delegation: Rent the Counter, Don't Inherit the Shop

Learn the Replace Inheritance with Delegation refactoring with a sweet-shop inheritance story, the honest meaning of composition over inheritance, the fragile base class problem, and step-by-step conversion in TypeScript and C#.

27 min read Updated June 11, 2026intermediate
refactoringinheritancedelegationcompositioncomposition over inheritancetypescriptcsharp

🍬 The boy who inherited a sweet shop

In Kolkata's Bowbazar lives the Sen family, famous for their sweet shop — Sen Mishtanno Bhandar. Three generations old. The shop is a whole world: the karigars who make rosogolla at four in the morning, the festival order books, the supplier khata, the loans register, the secret sondesh recipe locked in the iron almirah, and — at the front — a very good billing counter with a cash drawer, a card machine, and a printed-bill system the family perfected over decades. Customers praise the bills almost as much as the sweets: itemised, fast, never a mistake.

Now meet Raju, the youngest Sen. Raju does not want to make sweets. He wants to run a small chaat stall near the college gate — pani puri, jhal muri, papdi chaat. His own thing.

His uncle Tarun makes a grand offer over Sunday lunch: "Beta, inherit the family business! Everything becomes yours." But listen carefully to what everything means. If Raju inherits the business, he inherits all of it — the 4 a.m. kitchen shifts, the festival rosogolla orders that customers will now place with him, the supplier debts, the responsibility for the secret recipe. Customers who know the Sen name will walk up to Raju's chaat stall and demand two hundred sondesh for a wedding — because, on paper, he is the sweet shop now. When the wedding party shows up, what can Raju do? Put up a sign: "We don't make sweets here!" — a stall actively fighting its own inheritance. Raju wanted one thing — that excellent billing counter — and inheritance forces the entire business on him.

Raju does the smart thing instead. He goes back to his uncle and says: "Kaku, I don't want to be the sweet shop. Let me rent the billing counter for my stall." Tarun Kaku laughs, shakes his hand, and they settle on a small monthly rent.

Now look at the arrangement. The counter sits inside Raju's stall and handles his payments perfectly. Nobody can order rosogolla from him — that door simply does not exist at his stall. The wedding party never comes, because nothing about the stall claims to be a sweet shop. If next year a better digital billing machine arrives in the market, Raju returns the old counter and rents the new one — Tarun Kaku will not even mind. Raju is not welded to the family business; he just uses one well-made part of it, on his own terms, behind his own counter.

Raju replaced "is-a sweet shop" with "has-a billing counter." That exact move is today's refactoring: Replace Inheritance with Delegation.

Figure 1: Raju's journey from inheriting a whole business to renting the one part he needed

What is Replace Inheritance with Delegation? 🔁

This refactoring undoes an inheritance link that should never have been one. A class extends another only to borrow some of its code — not because it genuinely is a kind of that class. The fix: remove the extends, hold an instance of the former parent in a private field, and write small methods that delegate (forward) to it — but only for the operations you actually want to offer.

Why is the inheritance version so harmful? Because inheriting is an all-or-nothing deal. The subclass receives the parent's entire public surface, wanted or not. Three bad things follow:

  1. Leaked operations. Clients can call any inherited method — including ones that break your class's rules. The classic disaster: class Stack extends ArrayList. Every caller can do stack.add(0, item) or stack.clear() or stack.get(5), smashing stack discipline from outside. The class cannot protect its own invariants, because the dangerous doors were inherited along with the useful ones.
  2. A false promise to readers. extends is a public claim: "I am a kind of my parent — use me anywhere you use it." When that claim is a lie, every reader and every type checker is misled. This is the Refused Bequest smell: accepting the inheritance while quietly refusing most of it.
  3. The fragile base class problem. A subclass is coupled not only to the parent's interface but to its implementation. If the parent's author changes how methods call each other internally — say, addAll stops calling add in a loop — subclass overrides that relied on that hidden choreography silently break, even though the parent's public behaviour never changed. Joshua Bloch's Effective Java (Item 18) demonstrates this with a HashSet subclass that double-counts elements purely because of the parent's internal self-calls — and his conclusion is the famous maxim we are teaching today.

Delegation fixes all three at once. The field is private, so nothing leaks: clients see exactly the methods you wrote, no more. There is no public claim of kinship, so no one is misled. And your class depends only on the delegate's public contract, never its internals — internal reshuffles in the delegate cannot reach you.

💡

One-line summary: when a class extends a parent only to reuse code — not because it truly is a kind of the parent — remove the inheritance, hold the parent in a private field, and forward only the calls you genuinely want to offer. Turn a false is-a into an honest has-a.

Favor composition over inheritance — the honest version 🪞

This refactoring is the hands-on form of the most quoted design maxim in object-oriented programming: favor composition over inheritance. Let us teach it honestly, because the maxim is often repeated without its reasons or its limits.

Why composition is the safer default. Inheritance is the tightest coupling two classes can have: full interface, full implementation, visible protected internals, permanent and decided at compile time. Composition is loose by construction: you pick exactly which operations to expose, the delegate hides behind a private field, and you can swap it — for a subclass, a mock in tests, or a completely different implementation behind an interface — without touching your public face. Raju can change billing machines any year; a true heir cannot change grandfathers.

Why the maxim says "favor", not "always". Inheritance is the right tool when two conditions both hold: the relationship is a true is-a (the substitution test passes — the child can stand anywhere the parent is expected, with zero surprises), and the child genuinely wants essentially the whole contract of the parent. A SavingsAccount that truly is an Account, supports every account operation meaningfully, and adds interest on top — that is inheritance doing exactly its job, with zero forwarding boilerplate. Removing it would be ideology, not engineering. The next lesson, Replace Delegation with Inheritance, exists precisely for the day the seesaw tips the other way.

So the maxim, said in full: default to has-a; pay for is-a only when it is true. Side by side, the two relationships trade in opposite currencies:

PropertyInheritance (is-a)Delegation (has-a)
Surface exposed to callersThe parent's entire public interface, wanted or notExactly the forwarding methods you chose to write
CouplingInterface + implementation + protected internalsThe delegate's public contract only
Can the helper be swapped or mocked?No — you cannot change your parent at runtimeYes — different instance, subclass, or test fake
BoilerplateZero forwarders; everything arrives freeOne small method per offered operation
New parent/delegate methodsAppear on you automatically (even unwanted ones)Appear only when you add a forwarder (even wanted ones)
Claim made to readers"I am a kind of the parent — substitute freely""I use this object as a part or a tool"
Breaks whenParent internals change (fragile base class)Delegate's public contract changes (rare, visible)

College corner: the has-a vs is-a distinction is older than the maxim and worth stating formally. Is-a (inheritance, subtyping) is a claim about substitutability: every instance of the child is, behaviourally, an instance of the parent — this is the Liskov Substitution Principle as a design test, not just an exam answer. Has-a (composition, aggregation) is a claim about structure: the object contains or uses another object as a part or a tool. The trap students fall into is testing is-a with English instead of behaviour. "A square is a rectangle" sounds right in English — but a mutable Square extends Rectangle breaks the moment a caller sets width and height independently. The substitution test is behavioural, always: not "can I say it in a sentence?" but "can every caller of the parent receive the child and never notice?"

Figure 2: The inherit-or-delegate idea map — the seesaw and its two tests

When do we need it? 🔍

The signs that an inheritance link deserves this treatment:

  • The subclass uses only a slice of the parent. It calls three inherited methods and ignores thirty. The motive was code reuse, not kinship. This is Refused Bequest in its mildest form — and it rarely stays mild.
  • "Not supported" overrides exist. The loudest alarm bell: the subclass overrides inherited methods to throw NotSupportedException or return nonsense, actively fighting its own parent. The class is shouting that the is-a is false.
  • Inherited methods can break the class's rules. Like Stack extends ArrayList — any caller can violate the invariant through an inherited door. If you find defensive comments like "do not call add() directly on this," the design has already lost.
  • The substitution test fails. Hand a child object to honest code written against the parent type. If anything surprising can happen, the inheritance is lying to the type system.
  • The parent is a utility grab-bag. class OrderService extends BaseHelper just to reach formatDate() and log() — inheritance used as an import statement. Delegation (or plain imports) is the honest shape.
  • Parent upgrades keep breaking you. Every new version of the framework class you extend forces fixes in your subclass — the fragile base class problem visiting monthly.

And when not to use it:

  • The is-a is true and the whole contract is wanted. Genuine specialization with full substitutability is inheritance's home ground. Leave it alone.
  • Only one or two parent methods are misplaced. Maybe the parent is wrong, not the link — Push Down Method may repair the hierarchy more cheaply.
  • The subclass is nearly empty anyway. If the child adds nothing at all, the cure might be Collapse Hierarchy — merging, not delegating. A class that earns nothing is Lazy Class; a class that forwards everything is on its way to Middle Man. This refactoring lives between those two cliffs. And do not fear "losing reuse": Duplicate Code never appears, because the delegate still holds the shared logic exactly once — you reuse it through a field instead of a bloodline.

The audit that decides it can be a literal count. How much of the parent does the subclass really use? Raju's stall used exactly the billing slice of the family business:

Figure 3: Raju's audit of the family business — the slice he actually wanted

And the same count drawn as a comparison — inheriting gives the subclass the whole surface; delegating gives it exactly what it asked for:

Figure 4: Surface area exposed to callers — inheritance opens every door, delegation opens three

Both classes use the same three billing operations. The difference is what else callers can reach. Eighteen doors versus three — and fifteen of those eighteen are doors the class must apologise for.

Before and after at a glance

The classic, in TypeScript. A stack built by extending an array — all of the array's doors left wide open:

// BEFORE: "a stack IS an array" — a lie with consequences
class TicketStack extends Array<string> {
  pushTicket(id: string): void { this.push(id); }
  popTicket(): string | undefined { return this.pop(); }
}
 
const stack = new TicketStack();
stack.pushTicket("T-101");
stack.pushTicket("T-102");
 
// Every inherited door is open. All of these compile and run:
stack.unshift("T-999");     // jumps in from the bottom!
stack.splice(0, 1);         // removes from the middle!
stack[0] = "T-777";         // overwrites by index!
// The "stack" cannot defend its own rules.

And after — the array becomes a private, rented counter:

// AFTER: "a stack HAS an array" — the truth, enforced by the compiler
class TicketStack {
  private items: string[] = [];   // the delegate: held, not inherited
 
  pushTicket(id: string): void { this.items.push(id); }
  popTicket(): string | undefined { return this.items.pop(); }
  peek(): string | undefined { return this.items[this.items.length - 1]; }
  get size(): number { return this.items.length; }
}
 
const stack = new TicketStack();
stack.pushTicket("T-101");
// stack.unshift("T-999");   // compile error — the door does not exist
// stack.splice(0, 1);       // compile error
// stack[0] = "T-777";       // compile error

The whole improvement lies in what is absent. The dangerous operations are not forbidden by documentation or team discipline — they simply do not exist on the type. The compiler now guards the invariant that the inheritance version could only beg readers to respect.

Figure 5: Before, the stack inherits every array door, wanted or not; after, it holds the array privately and exposes only stack operations

Watch a single customer interaction at Raju's stall after the change — the delegation is invisible to the customer, and the dangerous request bounces off a door that does not exist:

Figure 6: Delegation in motion — the stall forwards billing work to the rented counter; sweet orders have no door to knock on

Step-by-step, the safe way 🪜

The conversion can be done without ever breaking the build for long. The trick: while the class still extends the parent, the delegate field and the inheritance can coexist.

Step 1: Add the delegate field. Inside the subclass, create a private field of the parent type. During the transition you can even initialize it with this — the object delegating to itself — so behaviour cannot change yet:

class TicketStack extends Array<string> {
  private items: Array<string> = this;   // temporary: delegate IS the object
  pushTicket(id: string): void { this.items.push(id); }
  popTicket(): string | undefined { return this.items.pop(); }
}

Step 2: Route every internal use through the field. Hunt down each place the class calls an inherited method or touches inherited state, and rewrite it as this.items.<method>. Compile and test after each one. Behaviour is still identical — the field is this — but every dependency on the parent is now funneled through one named doorway.

Step 3: Cut the cord. Change the field's initializer to a real, separate instance (= [] or new Parent(...)) and delete the extends clause in the same move.

Step 4: Fix the fallout, one compile error at a time. The compiler now lists every spot that depended on the inheritance: external callers using inherited methods, type annotations expecting the parent, instanceof checks. For each external call you want to keep offering, add a small delegating method. For each one you are happy to lose — that was the whole point — update the caller instead.

Step 5: Restore polymorphism where it was legitimate. If some callers genuinely needed to treat your class and the parent interchangeably, extract a minimal interface (Countable, Billable, ...) that both implement, and retarget those callers at the interface. You keep substitutability for the operations that deserve it, without re-importing the whole contract.

Step 6: Run the full suite, then tighten. With tests green, review your delegating methods: is each one an operation this class should offer? Delete any you wrote out of habit rather than need. The shorter that list, the stronger the design.

Figure 7: The safe states of the conversion — the cord is cut only after every internal call goes through the field
⚠️

Two subtleties bite people here. First, identity: before the cut, this and the delegate were one object; after, they are two. Any code that compared references, used the object as a map key, or registered this as a listener through the parent's machinery needs a careful second look. Second, state duplication: if the subclass mixed inherited state with its own fields, make sure every piece of data ends up living in exactly one place — in the delegate or in the class, never both. A half-migrated value that exists in both places is the classic source of "works in tests, fails in production."

A bigger real-life example 🛺

Let us code Raju's actual situation. Years ago someone modelled the family business like this:

// BEFORE: the whole family business, forced on the chaat stall
class SweetShop {
  makeRosogolla(qty: number): void { /* 4 a.m. kitchen work */ }
  takeFestivalOrder(order: string, qty: number): void { /* wedding-scale orders */ }
  payKarigars(): void { /* staff salaries */ }
  secretSondeshRecipe(): string { return "...three generations of secrets..."; }
 
  // the genuinely excellent part:
  addToBill(item: string, price: number): void { /* ... */ }
  printBill(): string { return "...formatted bill..."; }
  acceptCard(amount: number): boolean { return true; }
}
 
// Raju only wanted the billing counter. He got the rosogolla orders too.
class ChaatStall extends SweetShop {
  servePaniPuri(plates: number): void {
    this.addToBill("Pani Puri", plates * 30);   // uses billing — good
  }
 
  // Forced to fight his own inheritance:
  makeRosogolla(_qty: number): void {
    throw new Error("We don't make sweets here!");   // Refused Bequest, loudly
  }
}
 
// And the type system happily betrays everyone:
function placeWeddingOrder(shop: SweetShop) {
  shop.takeFestivalOrder("sondesh", 200);   // a ChaatStall can be passed in!
}

Every alarm from our checklist is ringing: a "not supported" override, a tiny slice of the parent actually used, and a substitution test (placeWeddingOrder) that compiles but is nonsense. Now the refactoring — extract the part Raju really wanted, and let the stall rent it:

// AFTER: the counter is its own thing; the stall rents it
class BillingCounter {
  private lines: { item: string; price: number }[] = [];
  add(item: string, price: number): void { this.lines.push({ item, price }); }
  print(): string { return this.lines.map(l => `${l.item}: Rs.${l.price}`).join("\n"); }
  acceptCard(amount: number): boolean { /* card machine */ return true; }
  total(): number { return this.lines.reduce((s, l) => s + l.price, 0); }
}
 
class SweetShop {
  private counter = new BillingCounter();      // the shop has-a counter too
  makeRosogolla(qty: number): void { /* ... */ }
  takeFestivalOrder(order: string, qty: number): void { /* ... */ }
  billSweets(item: string, price: number): void { this.counter.add(item, price); }
}
 
class ChaatStall {
  private counter = new BillingCounter();      // rented, not inherited
 
  servePaniPuri(plates: number): void {
    this.counter.add("Pani Puri", plates * 30);
  }
  serveJhalMuri(cups: number): void {
    this.counter.add("Jhal Muri", cups * 25);
  }
  customerBill(): string { return this.counter.print(); }
}
 
// placeWeddingOrder(new ChaatStall())  -> compile error. The lie is gone.

Look at what improved beyond the obvious. There is no throw new Error("We don't make sweets") anywhere — classes no longer fight their own ancestry. The SweetShop itself got better: it also composes the counter now, so billing logic lives once in a focused class both businesses share. And the stall is testable in isolation: hand it a mock BillingCounter (behind a small interface if you like) and test chaat logic without any sweet-shop machinery. That swap-ability is the gift inheritance could never give — you cannot mock your own parent.

Figure 8: The seesaw between the two delegation refactorings — the is-a test and the fraction of the contract used decide the side

Where do real classes land on this seesaw? Plot them on the two axes the seesaw is made of — how much of the base is used, and how true the is-a is:

Figure 9: The inherit-or-delegate map — the sweet-shop boy lives deep in the delegate corner

Notice SchoolMailer sitting in the inherit-happily corner — a wrapper that forwards almost everything to a delegate it truly is-a kind of. That class is the hero of the next lesson, where the seesaw swings the other way.

The same refactoring in C# 🟣

C# adds two pleasant tools: interfaces to keep polymorphism honest, and sealed/composition idioms the ecosystem already loves. Before:

// BEFORE: report generator inherits a database session for "convenience"
public class DbSession
{
    public void Open() { /* ... */ }
    public void Close() { /* ... */ }
    public List<T> Query<T>(string sql) { /* ... */ return new(); }
    public void Execute(string sql) { /* ... */ }          // writes!
    public void DropTable(string name) { /* ... */ }       // disaster door
}
 
public class SalesReport : DbSession      // a report IS a database session??
{
    public string Generate()
    {
        Open();
        var rows = Query<SaleRow>("SELECT * FROM sales");
        Close();
        return Format(rows);
    }
    private string Format(List<SaleRow> rows) => "...";
    // Inherited and exposed: Execute(), DropTable() — on a REPORT.
}

After — the session becomes an injected, swappable collaborator:

// AFTER: the report has-a session, ideally behind an interface
public interface IReadOnlySession
{
    List<T> Query<T>(string sql);
}
 
public sealed class SalesReport
{
    private readonly IReadOnlySession _db;          // the delegate
    public SalesReport(IReadOnlySession db) => _db = db;
 
    public string Generate()
    {
        var rows = _db.Query<SaleRow>("SELECT * FROM sales");
        return Format(rows);
    }
    private string Format(List<SaleRow> rows) => "...";
    // Execute() and DropTable() simply do not exist here.
}

C#-specific notes:

  • Constructor injection is the natural delivery method for the delegate. The DI container wires a real session in production and your test passes a fake — composition and testability arrive together.
  • The interface narrows the contract. IReadOnlySession exposes one method out of five. The report cannot drop tables even by accident, and the compiler is the enforcer.
  • sealed documents the decision. The report no longer participates in any hierarchy; sealing it tells readers the design is composition on purpose.
  • The BCL itself models this lesson. System.Collections.Generic.Stack<T> and Queue<T> do not inherit from List<T> — they wrap their storage privately, exactly the shape we just built. The standard library chose has-a; follow it.

College corner: students often worry about the forwarding cost — does every delegated call pay a runtime penalty? Honestly: a forwarding method is one extra call frame, and modern JIT compilers inline tiny forwarders away almost every time; in TypeScript and Python the difference disappears into interpreter noise. The real cost of delegation is not in nanoseconds but in keystrokes and upkeep: each forwarder is a line a human writes, reviews, and keeps in sync. That cost is real, which is exactly why the inverse refactoring exists for the case where you find yourself forwarding an entire interface. Pay the forwarding tax when it buys you a guarded boundary; stop paying it when the boundary guards nothing.

IDE support 🛠️

This is one of the few refactorings with first-class, one-click automation:

  • IntelliJ IDEA / Rider: Refactor → Replace Inheritance with Delegation is a dedicated, dialog-driven refactoring. You choose which inherited members the class should keep offering; the IDE removes the extends, introduces the delegate (as a field or inner instance), and generates all forwarding methods automatically. It can also implement an interface to preserve polymorphic callers.
  • Kotlin: the language bakes the destination in — class TicketStack(private val items: MutableList<String>) : List<String> by items delegates an entire interface with the by keyword, zero hand-written forwarders.
  • ReSharper / Visual Studio (C#): no single-click version, but the recipe is well supported: Extract Interface on the parent, change the base list, then use Generate → Delegating Members to produce the forwarding methods over the new field.
  • VS Code (TypeScript): manual, guided by the compiler — delete extends, then fix the reported errors one by one, exactly as in our step-by-step.

Benefits and risks ⚖️

This table and the one in the next lesson are mirror images — the same seesaw read from opposite ends. The is-a test plus the fraction of the contract used tell you which side of the seesaw your class belongs on.

BenefitsRisks / costs
Exposes only operations the class truly supports — no leaked, refused, or stubbed membersBoilerplate: every offered operation is a forwarding method you write and maintain
Invariants become enforceable — dangerous inherited doors stop existingCallers using the old base type break; legitimate polymorphism needs a rescued interface
Decouples from the parent's internals — immune to the fragile base class problemIf the is-a was real and nearly the whole contract was used, you traded free inheritance for busywork — the inverse refactoring undoes this
The delegate is swappable: different implementation, subclass, or test doubleTwo objects now exist where one did — identity comparisons and listener registrations need review
States the truth: has-a instead of a false is-a, so readers and type checkers are not misledSlight indirection cost in reading (and negligibly at runtime) for each forwarded call

Which smells does it cure? 👃

SmellHow Replace Inheritance with Delegation helps
Refused BequestThe class stops inheriting members it refused — it now offers only what it honestly supports
Inappropriate Intimacy (subclass–parent)The class loses sight of the parent's protected internals; only the public contract remains in reach
Leaky abstraction / broken invariantsDangerous inherited operations vanish from the type instead of being policed by comments
Fragile base class breakageInternal changes in the former parent can no longer silently break overrides — there are none
Middle Man (watch-out, not cure)Over-applying this refactoring creates Middle Man — if forwarding covers nearly everything and is-a is true, swing back with the inverse refactoring

Quick revision box 📦

+------------------------------------------------------------------+
|   REPLACE INHERITANCE WITH DELEGATION - REVISION CARD            |
+------------------------------------------------------------------+
| Problem  : class EXTENDS a parent only to reuse some code.       |
|            False is-a -> leaked doors, refused bequest,          |
|            fragile base class. (Raju forced to inherit the       |
|            whole sweet shop for one billing counter.)            |
|                                                                  |
| Solution : 1. add private field of parent type (start = this)    |
|            2. route all internal calls through the field         |
|            3. delete extends; give the field its own instance    |
|            4. add forwarding methods ONLY for wanted operations  |
|            5. rescue real polymorphism with a small interface    |
|                                                                  |
| Maxim    : FAVOR COMPOSITION OVER INHERITANCE                    |
|            default to has-a; pay for is-a only when TRUE         |
| Is-a test: child substitutes for parent with ZERO surprises      |
|            and wants essentially the WHOLE contract              |
| Inverse  : Replace Delegation with Inheritance (next lesson)     |
+------------------------------------------------------------------+

Practice exercise ✏️

Your turn. A school library system contains this hierarchy, written "to reuse the search code":

class BookCatalog {
  protected books: Book[] = [];
  addBook(b: Book): void { this.books.push(b); }
  removeBook(isbn: string): void { /* ... */ }
  findByTitle(t: string): Book[] { /* good search logic */ return []; }
  findByAuthor(a: string): Book[] { /* good search logic */ return []; }
  exportCatalog(): string { /* full catalog dump */ return ""; }
}
 
// The kiosk only SEARCHES. But it inherited everything.
class StudentSearchKiosk extends BookCatalog {
  showResults(title: string): string {
    return this.findByTitle(title).map(b => b.title).join("\n");
  }
 
  addBook(_b: Book): void {
    throw new Error("Students cannot add books!");   // fighting the parent
  }
  removeBook(_isbn: string): void {
    throw new Error("Students cannot remove books!"); // fighting the parent
  }
  // exportCatalog() is still inherited and exposed. Oops — privacy leak.
}

Work through it:

  1. Run the two-part test: is a kiosk truly a kind of catalog (substitution with zero surprises)? What fraction of the contract does it honestly support? Write your verdict in one sentence before touching code, then place the kiosk on Figure 9's map.
  2. Apply the safe sequence: add a private catalog: BookCatalog field, route findByTitle through it, remove the extends, and let the compiler list the fallout.
  3. Decide the kiosk's honest public surface. It should end up with showResults (and perhaps findByAuthor forwarding) — and nothing else. Confirm addBook, removeBook, and exportCatalog no longer exist on the kiosk's type, and delete both throw overrides as the design-smell apologies they were.
  4. The librarian's admin screen legitimately uses the full BookCatalog polymorphically. Does anything break for it? Why not?
  5. Tighten further: extract a BookFinder interface with just the two find methods, have BookCatalog implement it, and let the kiosk depend on BookFinder instead of the concrete catalog. Now write a one-line fake BookFinder for the kiosk's unit test.
  6. Bonus thinking: suppose a future AdminTerminal class wraps BookCatalog and ends up forwarding every single method, adding nothing. Which refactoring does that situation call for, and what two conditions must it verify first? (One sentence — and it is the exact subject of the next lesson.)

If your step 1 verdict was "false is-a: the kiosk refuses half the contract and leaks the rest, so it must have a catalog, not be one," you have internalized the deepest design rule in this series. Raju would shake your hand over a plate of pani puri. Well done.

Frequently asked questions

What exactly does 'favor composition over inheritance' mean?
It means: when you only want to reuse another class's behaviour, hold that class in a field and forward calls to it, instead of extending it. Reserve extending for true is-a relationships where the child genuinely is a kind of the parent and honestly supports the parent's whole contract. It is 'favor', not 'always' — composition is the safer default, inheritance the special case.
What is the fragile base class problem in simple words?
When you inherit, your class is welded to the parent's internal implementation, not just its public promises. If the parent's author changes how its methods call each other internally — without changing any public behaviour — your overrides can silently break. Your class becomes fragile because of code you never wrote and cannot see changing.
How do I test whether a relationship is a true is-a?
Use the substitution test: can an object of the child be handed to any code expecting the parent, and behave with zero surprises, supporting every inherited operation meaningfully? If even one inherited method would be nonsense, dangerous, or need a 'not supported' stub on the child, the is-a is false and delegation is the honest design.
Doesn't delegation create a lot of boring forwarding methods?
Yes, and that is the deliberate price. Each forwarding method is also a decision point: you expose exactly the operations you choose and nothing else. If you ever notice you are forwarding nearly the parent's entire interface and the is-a is genuinely true, the inverse refactoring — Replace Delegation with Inheritance — exists precisely for that case.
Will I lose polymorphism when I remove the extends clause?
Code that treated your class as the old base type stops compiling, yes. If callers legitimately needed substitutability, extract a small interface that both your class and the delegate implement, and let callers depend on that interface. You keep polymorphism while still escaping the inherited implementation.

Further reading

Related Lessons