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#.
🍬 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.
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:
- 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 dostack.add(0, item)orstack.clear()orstack.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. - A false promise to readers.
extendsis 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. - 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,
addAllstops callingaddin 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 aHashSetsubclass 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:
| Property | Inheritance (is-a) | Delegation (has-a) |
|---|---|---|
| Surface exposed to callers | The parent's entire public interface, wanted or not | Exactly the forwarding methods you chose to write |
| Coupling | Interface + implementation + protected internals | The delegate's public contract only |
| Can the helper be swapped or mocked? | No — you cannot change your parent at runtime | Yes — different instance, subclass, or test fake |
| Boilerplate | Zero forwarders; everything arrives free | One small method per offered operation |
| New parent/delegate methods | Appear 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 when | Parent 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?"
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
NotSupportedExceptionor 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 BaseHelperjust to reachformatDate()andlog()— 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:
And the same count drawn as a comparison — inheriting gives the subclass the whole surface; delegating gives it exactly what it asked for:
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 errorThe 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.
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:
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.
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.
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:
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.
IReadOnlySessionexposes one method out of five. The report cannot drop tables even by accident, and the compiler is the enforcer. sealeddocuments 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>andQueue<T>do not inherit fromList<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 itemsdelegates an entire interface with thebykeyword, 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.
| Benefits | Risks / costs |
|---|---|
| Exposes only operations the class truly supports — no leaked, refused, or stubbed members | Boilerplate: every offered operation is a forwarding method you write and maintain |
| Invariants become enforceable — dangerous inherited doors stop existing | Callers using the old base type break; legitimate polymorphism needs a rescued interface |
| Decouples from the parent's internals — immune to the fragile base class problem | If 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 double | Two 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 misled | Slight indirection cost in reading (and negligibly at runtime) for each forwarded call |
Which smells does it cure? 👃
| Smell | How Replace Inheritance with Delegation helps |
|---|---|
| Refused Bequest | The 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 invariants | Dangerous inherited operations vanish from the type instead of being policed by comments |
| Fragile base class breakage | Internal 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:
- 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.
- Apply the safe sequence: add a private
catalog: BookCatalogfield, routefindByTitlethrough it, remove theextends, and let the compiler list the fallout. - Decide the kiosk's honest public surface. It should end up with
showResults(and perhapsfindByAuthorforwarding) — and nothing else. ConfirmaddBook,removeBook, andexportCatalogno longer exist on the kiosk's type, and delete boththrowoverrides as the design-smell apologies they were. - The librarian's admin screen legitimately uses the full
BookCatalogpolymorphically. Does anything break for it? Why not? - Tighten further: extract a
BookFinderinterface with just the two find methods, haveBookCatalogimplement it, and let the kiosk depend onBookFinderinstead of the concrete catalog. Now write a one-line fakeBookFinderfor the kiosk's unit test. - Bonus thinking: suppose a future
AdminTerminalclass wrapsBookCatalogand 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
Refused Bequest: The Child Who Refused the Sweet Shop Recipes
Learn the Refused Bequest code smell with a family sweet shop story, Liskov violations in TypeScript and C#, and the delegation cure explained step by step.
Middle Man: The Helper Who Only Forwards Your Message to the Principal
Learn the Middle Man code smell with a story of a school helper who only carries messages without adding anything. When a class merely forwards every call, remove it — but learn why Proxy, Facade, and Adapter are middle men ON PURPOSE.
Replace Delegation with Inheritance: When the Helper Should Become the Apprentice
Learn the Replace Delegation with Inheritance refactoring with a tailor-shop helper story, the Middle Man smell, the strict is-a conditions that must hold first, and step-by-step conversion in TypeScript and C#.
Hide Delegate: Ask the Monitor, Let the Monitor Do the Running
Learn the Hide Delegate refactoring with a story about a class monitor who finds your homework for you. Stop writing chains like employee.department.manager — give the first object a simple method and hide the journey inside it. Step-by-step TypeScript and C# examples included.