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#.
✂️ The helper who forwarded everything
In a lane in Ludhiana sits Master Joginder's tailoring shop, famous for wedding sherwanis. Master Joginder is the craftsman — thirty years at the machine, hands that measure cloth by feel. And at the front of the shop sits Chhotu — the helper, five years in the shop, sharp-eyed and quick.
Watch Chhotu work for one morning. A customer comes for measurement. Chhotu turns around: "Masterji, measurement!" Master Joginder takes the measurement. Another customer asks about fabric. Chhotu turns around: "Masterji, fabric question!" Masterji answers. A button needs stitching — "Masterji, button!" Alteration — "Masterji, alteration!" Bill — "Masterji, bill!"
Count what Chhotu actually does himself: nothing. Every single task — every one — is turned around and passed to the master. Chhotu is not lazy; the shop is simply arranged so that he is a pure message-passer. And the arrangement has real costs. Every job takes one extra hop. When Masterji learned kurta embroidery last winter, customers asking Chhotu about it got a blank stare for two whole weeks — until Masterji remembered to teach Chhotu to forward that too. The forwarding list lives in Chhotu's head, and someone must maintain it by hand, forever. Mrs. Gill from the corner house still tells the story of asking Chhotu for embroidery and being told "we don't do that here" while Masterji sat embroidering ten feet behind him.
One evening Master Joginder watches all this and makes a decision: "Chhotu, you have been beside me for five years. You have seen every stitch. From tomorrow, you are my apprentice. You don't pass tasks to me — you are a tailor of this shop. Whatever I can do, you now do as your own."
Notice what changed and what did not. The customers still come to the same boy at the same counter. But the relationship was upgraded from message-passing to being: Chhotu is-a tailor now. Every skill the master has flows to him automatically — including new skills, the day Masterji learns them, with nobody maintaining a forwarding list. The only things Chhotu keeps as "his own special way" are the few tasks he genuinely does differently — his famously neat buttonholes, tighter than even Masterji's.
And one thing matters enormously: Masterji promoted Chhotu only because, after five years, the boy genuinely could stand in for him on everything. Promoting a newcomer who can only handle billing would be a disaster — he would "inherit" expectations he cannot meet, and Mrs. Gill's wedding sherwani would come back with crooked sleeves. Keep that warning; it is the heart of today's lesson: Replace Delegation with Inheritance.
What is Replace Delegation with Inheritance? 🔁
This refactoring is the exact inverse of Replace Inheritance with Delegation. The previous lesson rescued a class trapped in a false is-a. This lesson rescues a class trapped in pointless message-passing.
The setup: a class holds another object in a field and forwards call after call to it. getX() returns inner.getX(). setY(v) calls inner.setY(v). Method after method, the whole file is one-line pass-throughs. The wrapper supports essentially the delegate's entire interface, and almost nothing it does differs from plain forwarding. This is the Middle Man smell: a class whose main occupation is passing messages along.
That forwarding has costs, just like Chhotu's:
- Boilerplate that must be hand-maintained. Every method on the delegate needs a matching forwarder, written by a human, reviewed by a human, kept in sync by a human.
- Silent gaps. When the delegate gains a new method, the wrapper does not — until someone remembers to add the pass-through. Clients of the wrapper live one update behind, and nobody is notified. (The embroidery gap.)
- Noise that buries the signal. The two methods that genuinely do something different drown among twenty that do nothing. Readers must check every forwarder to find the real behaviour.
Open a typical middle-man class and weigh its contents honestly:
The fix, when — and only when — the relationship is honestly an is-a: delete the field, make the class extend the delegate, delete every pure forwarder (inheritance now supplies them for free), and keep only the methods that genuinely differ, converted into proper overrides.
One-line summary: when a class forwards essentially the delegate's whole interface and truly is a kind of that delegate, replace the field with extends — pure forwarders vanish, new parent methods arrive automatically, and only the genuine differences remain as overrides.
The seesaw, read from the other end ⚖️
These two delegation refactorings form a seesaw, and serious codebases ride it in both directions over the years. The balance point is decided by two questions asked together:
- The is-a test. Can the wrapper substitute for the delegate anywhere, with zero surprises, every operation meaningful? (Could Chhotu truly stand in for Masterji on every task?)
- The coverage test. Does the wrapper support essentially the whole contract — not a curated slice?
Two yes answers → the forwarding is waste; inheritance is honest and cheaper — this lesson. Any no → the forwarding is the design: it is precisely how you expose a chosen slice and keep the delegate swappable — the previous lesson. "Favor composition over inheritance" still stands as the default; this refactoring is not a rebellion against the maxim but its fine print: when is-a is proven true and the whole contract is wanted, stop paying the composition tax.
Note who appears at the bottom-left: the sweet-shop boy from the previous lesson. Same map, opposite corners, opposite refactorings. That is the whole seesaw on one picture.
College corner: the forwarding cost deserves precise accounting, because it is this refactoring's entire motivation. The runtime cost of a forwarder is nearly nothing — one extra call that JIT compilers usually inline away. The true cost is a maintenance invariant: the wrapper's interface must be kept equal to the delegate's interface by hand, forever, with no tool checking it. Every delegate release is a chance to fall behind; every gap is invisible until a caller needs the missing method (the late refundPartial below). Inheritance replaces that hand-maintained invariant with a language-enforced one — the compiler itself guarantees the subclass offers everything the parent does. You are not saving nanoseconds; you are deleting an entire category of human error. The price: you re-enter the fragile base class world, because subclassing couples you to the parent's internals again. That trade — human-error category deleted, implementation coupling accepted — is what the two verification tests are really weighing.
When do we need it? 🔍
The signs:
- A wall of one-line forwarders. Open the class: fifteen methods, thirteen of them shaped exactly like
foo(a) { return this.inner.foo(a); }. That wall is the Middle Man smell in its purest form. - Every delegate update creates work here. The delegate's team added two methods last sprint; your sprint board now contains "add pass-throughs." Maintenance that produces nothing.
- Clients keep "tunnelling". Callers who needed the delegate's newest method gave up waiting and wrote
wrapper.getInner().newMethod()— the wrapper is now both noise and bypassed. (If tunnelling is everywhere, also weigh Remove Middle Man: maybe clients should simply hold the delegate.) - The genuine differences are tiny and drowning. One cached method, one logged method — and twenty forwarders around them. After this refactoring, the class body would contain only those two interesting methods.
- The wrapper passes both tests. Is-a true, coverage near-total. Without both, stop — see below.
When not to use it:
- Partial coverage. If the wrapper forwards only part of the delegate's interface, inheriting would drag in the unwanted remainder — manufacturing a fresh Refused Bequest. The previous refactoring exists precisely to create such curated slices; do not bulldoze one because forwarding looks boring. The forwarders are the fence, and the fence is load-bearing.
- You valued swap-ability. If tests inject a fake delegate, or production swaps implementations, the field is doing invisible, valuable work. Inheritance welds the choice down permanently — the fragile base class problem and compile-time coupling return the moment you extend.
- Multiple delegates. Single inheritance absorbs at most one. The others stay as fields regardless.
- The delegate is
sealed/final, or its constructor cannot be satisfied. Unsubclassable means undoable — and often means the author told you not to. - The wrapper itself barely deserves to exist. If after inheriting the subclass would add nothing, the honest end state is no class at all — Collapse Hierarchy or Remove Middle Man finishes the job. A do-nothing class is just Lazy Class wearing a new costume. And if the forwarders contain little copied snippets of delegate logic, clean that Duplicate Code first so the diff stays honest.
Three look-alike situations, three different cures — keep them straight:
| Situation | Key observation | Right move |
|---|---|---|
| Wrapper forwards everything, is-a true, adds a few real overrides | Forwarding is pure waste | Replace Delegation with Inheritance (this lesson) |
| Wrapper forwards everything and adds nothing, clients could go direct | The wrapper type itself is pointless | Remove Middle Man |
| Wrapper forwards a deliberate slice, hides dangerous operations | The fence is load-bearing | Keep delegation (previous lesson's output) |
| Wrapper inherited and now empty after conversion | One concept, two classes | Collapse Hierarchy |
Before and after at a glance
A notification sender that wraps the team's standard Mailer, forwarding nearly everything, differing in exactly one method:
// BEFORE: a middle man — five methods, four pure forwarders
class Mailer {
connect(): void { /* SMTP handshake */ }
disconnect(): void { /* ... */ }
send(to: string, subject: string, body: string): void { /* ... */ }
sendBulk(to: string[], subject: string, body: string): void { /* ... */ }
status(): string { return "ready"; }
}
class SchoolMailer {
private inner = new Mailer(); // the delegate
connect(): void { this.inner.connect(); } // pure forwarding
disconnect(): void { this.inner.disconnect(); } // pure forwarding
sendBulk(to: string[], s: string, b: string): void {
this.inner.sendBulk(to, s, b); // pure forwarding
}
status(): string { return this.inner.status(); } // pure forwarding
send(to: string, subject: string, body: string): void {
// the ONE genuine difference: every mail gets the school footer
this.inner.send(to, subject, body + "\n\n— Sunrise Public School");
}
}After — four forwarders deleted, one honest override remains:
// AFTER: a true subclass — only the genuine difference survives
class SchoolMailer extends Mailer {
override send(to: string, subject: string, body: string): void {
super.send(to, subject, body + "\n\n— Sunrise Public School");
}
// connect(), disconnect(), sendBulk(), status(): inherited, zero upkeep.
// When Mailer gains scheduleSend() next quarter, SchoolMailer
// has it the same day — no forwarding list to maintain.
}The class shrank from five methods to one, and the one that remains is pure signal: the only thing a SchoolMailer does differently. Note the honesty check passed first: a school mailer truly is a mailer — every operation meaningful, substitutable anywhere a Mailer is expected, whole contract wanted.
Watch one bulk email travel through the before-world — every arrow on the left half is a line of code somebody had to write and keep in sync:
Step-by-step, the safe way 🪜
The order matters: verify first, rewire second, delete last.
Step 1: Verify the two conditions — in writing. List the delegate's public methods in one column and the wrapper's in another. Coverage: is essentially everything forwarded? Is-a: would every inherited operation be meaningful on the wrapper, called by any client, in any order? Also confirm the delegate is subclassable (not sealed/final) and that no test or config swaps the delegate for another implementation. If anything fails, stop — the forwarding is the design.
Step 2: Make the wrapper a subclass — while keeping the field. Add extends Delegate without deleting anything. The class temporarily has both relationships; everything still compiles and behaves identically.
Step 3: Point the field at this. Make the delegate field refer to the object itself. Every forwarder like this.inner.connect() now effectively calls the inherited method. Behaviour is unchanged — run the tests — but the separate inner object is gone from the picture:
class SchoolMailer extends Mailer {
private inner: Mailer = this; // transitional: forwarding to ourselves
connect(): void { this.inner.connect(); } // = inherited connect()
send(to: string, s: string, b: string): void {
this.inner.send(to, s, b + "\n\n— Sunrise Public School");
}
// ...
}(One language wrinkle: in this transitional state a pure forwarder like connect() overrides the inherited method and calls itself through this.inner — TypeScript and C# resolve this fine because the body delegates upward once deleted, but do not linger here; the state exists only to keep the diff reviewable.)
Step 4: Delete the pure forwarders, one at a time. Each method whose body only called the field gets removed; inheritance immediately supplies it. Compile and test after each deletion. The diff tells the story method by method.
Step 5: Convert the genuine differences into overrides. The methods that did more than forward — added a footer, cached a value, logged a call — stay, rewritten to call super.method(...) instead of this.inner.method(...). Mark them override so the compiler verifies the signatures.
Step 6: Remove the field and fix construction. Delete the inner field and any constructor wiring that created or accepted the delegate. This is the step with real fallout: code that passed in an existing delegate instance (new SchoolMailer(existingMailer)) can no longer do so — the behaviour now lives in-place, not around a captured object. Update those call sites to construct the subclass directly, and run the full suite.
Step 6 hides the one genuine behaviour change of this refactoring: the wrapper and the delegate stop being two objects. Before, SchoolMailer and its inner Mailer had separate identities — other code might have held a reference to that same inner Mailer and observed shared state. After, there is one object. If the old delegate was shared (a singleton connection, a registered listener, an instance handed to you from outside), inheriting silently un-shares it. Audit every constructor call site for an injected delegate before deleting the field — injected delegates are the strongest sign that you should keep composition after all.
A bigger real-life example 💳
A payments team wrapped their gateway client years ago "to add logging someday." Someday never fully came: one method got logging, eleven became forwarders. Every gateway SDK update means new pass-throughs; last month, refunds shipped late because the wrapper was missing refundPartial and nobody noticed until QA.
// BEFORE: twelve methods, eleven of them noise
class GatewayClient {
authorize(card: Card, amount: number): AuthResult { /* ... */ return ok(); }
capture(authId: string): void { /* ... */ }
refund(paymentId: string): void { /* ... */ }
refundPartial(paymentId: string, amount: number): void { /* ... */ }
voidAuth(authId: string): void { /* ... */ }
balance(): number { return 0; }
// ... six more operations
}
class LoggingGatewayClient {
private gateway = new GatewayClient();
authorize(card: Card, amount: number): AuthResult {
console.log(`AUTH attempt: Rs.${amount}`); // the real behaviour
const result = this.gateway.authorize(card, amount);
console.log(`AUTH result: ${result.status}`);
return result;
}
capture(id: string): void { this.gateway.capture(id); } // forward
refund(id: string): void { this.gateway.refund(id); } // forward
refundPartial(id: string, amt: number): void {
this.gateway.refundPartial(id, amt); // forward
}
voidAuth(id: string): void { this.gateway.voidAuth(id); } // forward
balance(): number { return this.gateway.balance(); } // forward
// ... six more forwarders, added whenever someone remembers
}Run the checklist: is-a — a logging gateway client is a gateway client, every operation meaningful, substitutable anywhere (logging is invisible to callers). Coverage — total; the wrapper exists to be the gateway, plus one logged method. Delegate subclassable, never swapped in tests (they fake the network layer below it instead). Both tests pass; the seesaw says inherit:
// AFTER: one override; eleven methods inherited; zero maintenance
class LoggingGatewayClient extends GatewayClient {
override authorize(card: Card, amount: number): AuthResult {
console.log(`AUTH attempt: Rs.${amount}`);
const result = super.authorize(card, amount);
console.log(`AUTH result: ${result.status}`);
return result;
}
// capture, refund, refundPartial, voidAuth, balance, and the
// other six: inherited. The "missing refundPartial" bug class
// is extinct — new SDK methods arrive automatically.
}The class is now eleven methods shorter, and the one method left is exactly the class's reason to exist. The bug class that delayed refunds — forwarder not yet written — is structurally impossible now. Plot the team's quiet drain over the SDK's release history, and what this refactoring deletes becomes visible:
One honest postscript: if next year the team decides logging should also apply to a second gateway, or be toggled at runtime, the seesaw tips back — a decorator (composition again, ideally over an extracted interface) becomes the better shape, and Replace Inheritance with Delegation performs the return journey. Riding the seesaw in both directions across years is not indecision; it is design tracking reality.
The same refactoring in C# 🟣
The classic clock example. A test-friendly clock wrapper that ended up forwarding everything except one cached method:
// BEFORE: a middle man around the system clock
public class SystemClock
{
public virtual DateTime Now() => DateTime.Now;
public virtual DateTime UtcNow() => DateTime.UtcNow;
public virtual long Ticks() => DateTime.Now.Ticks;
public virtual TimeZoneInfo Zone() => TimeZoneInfo.Local;
public virtual DateTime Today() => DateTime.Today;
}
public class CachedClock
{
private readonly SystemClock _inner = new();
private DateTime? _today;
public DateTime Now() => _inner.Now(); // pure forwarding
public DateTime UtcNow() => _inner.UtcNow(); // pure forwarding
public long Ticks() => _inner.Ticks(); // pure forwarding
public TimeZoneInfo Zone() => _inner.Zone(); // pure forwarding
public DateTime Today() // the one real method
=> _today ??= _inner.Today();
}// AFTER: a true subclass; the cache is the whole class body
public class CachedClock : SystemClock
{
private DateTime? _today;
public override DateTime Today()
=> _today ??= base.Today();
// Now(), UtcNow(), Ticks(), Zone(): inherited — zero boilerplate.
}C#-specific notes:
- The parent's methods must be
virtualfor the genuine differences to becomeoverrides. If the delegate's methods are non-virtual, you can inherit the forwarder-free interface but cannot vary behaviour — a sign the class may not have been designed for subclassing. sealedis a hard stop. A sealed delegate cannot be extended, full stop — and sealing is frequently the author's explicit "compose, don't inherit" message. Respect it.- Constructor chaining returns. If
SystemClockhad constructor parameters,CachedClockmust satisfy them via: base(...). If you cannot construct the parent (it comes only from a factory or DI container), inheritance is off the table — composition stays. - Watch your DI registrations. If the container previously registered
SystemClockandCachedClockseparately and injected one into the other, the registration becomes a singleCachedClock(optionally exposed asSystemClock). This is Step 6's "two objects become one" rule wearing dependency-injection clothes.
And the same shape in Python, where the conversion is even shorter because there is no override keyword to update — just the method resolution order doing the work:
class GatewayClient:
def authorize(self, card, amount): ...
def capture(self, auth_id): ...
def refund(self, payment_id): ...
# ... nine more operations
class LoggingGatewayClient(GatewayClient):
"""Was a wrapper with eleven forwarders. Now: one override."""
def authorize(self, card, amount):
print(f"AUTH attempt: Rs.{amount}")
result = super().authorize(card, amount)
print(f"AUTH result: {result.status}")
return result
# Everything else: inherited. No forwarding list to maintain.The Python warning is the same as everywhere: super() re-welds you to the parent's internals. If GatewayClient.authorize internally starts calling self.capture(...), your subclass is now part of that hidden choreography — the fragile base class problem, on schedule.
IDE support 🛠️
Unlike its inverse, this direction has no one-click automation in mainstream IDEs — but the ingredient moves are all assisted:
- IntelliJ IDEA / Rider: change the class declaration by hand, then let the IDE do the heavy lifting: Safe Delete on each pure forwarder confirms inheritance covers its callers, and Inline Method collapses any forwarder that other code referenced directly. The Find Usages view on the old delegate field flushes out every constructor and injection site before you delete it.
- ReSharper / Visual Studio (C#): the Generate → Overriding Members dialog rewrites the genuine differences against
base, and ReSharper's "Member hides inherited member" / redundancy inspections highlight forwarders that became identical to what inheritance now provides — delete-with-confidence markers. - VS Code (TypeScript): the compiler is the tool. Add
extends, mark kept methodsoverride(withnoImplicitOverrideswitched on intsconfig, every accidental shadow becomes a compile error), delete forwarders one by one, and let red squiggles list the construction sites still passing a delegate.
Whichever editor: the verification (is-a, coverage, swap-ability, subclassability) is yours alone. No tool can test honesty.
Benefits and risks ⚖️
Read this table beside the one in Replace Inheritance with Delegation — they are the same seesaw photographed from opposite ends. The is-a test and the coverage test decide which photograph matches your class.
| Benefits | Risks / costs |
|---|---|
| Deletes an entire wall of hand-maintained forwarding methods | Valid only for a true is-a with near-total coverage — partial use inherited becomes Refused Bequest |
| New methods on the parent arrive automatically — the "forgotten forwarder" bug class goes extinct | Re-welds tight compile-time coupling: parent internals can now break you (fragile base class returns) |
| The class body shrinks to pure signal: only genuinely different behaviour remains | The delegate can no longer be swapped, mocked at this seam, or chosen at runtime |
| One object instead of two — less allocation, no identity confusion between wrapper and inner | Two objects becoming one breaks callers that injected or shared the old inner instance |
| Resolves the Middle Man smell while keeping the wrapper as the public face | Single inheritance: only one delegate can ever be absorbed; sealed/final parents refuse entirely |
Which smells does it cure? 👃
| Smell | How Replace Delegation with Inheritance helps |
|---|---|
| Middle Man | The class stops being a message-passer; forwarding methods are replaced by free inheritance |
| Duplicate Code (structural) | Dozens of identically-shaped one-line forwarders — duplication of pattern — vanish outright |
| Lazy Class (borderline cases) | A wrapper that was mostly ceremony either shrinks to meaningful overrides or reveals it should not exist at all |
| Shotgun Surgery (delegate updates) | Adding a method to the delegate no longer requires a matching edit in the wrapper |
| Refused Bequest (warning, not cure) | Applied without the coverage test, this refactoring manufactures Refused Bequest — the inverse move undoes the damage |
Quick revision box 📦
+------------------------------------------------------------------+
| REPLACE DELEGATION WITH INHERITANCE - REVISION CARD |
+------------------------------------------------------------------+
| Problem : class forwards (nearly) EVERY call to a held |
| delegate. Pure boilerplate, silent gaps when the |
| delegate grows, real behaviour buried in noise. |
| (Chhotu turning every task around to Masterji.) |
| |
| Verify : 1. IS-A: substitutable with zero surprises |
| 2. COVERAGE: essentially the WHOLE contract wanted |
| 3. delegate subclassable, single, never injected/ |
| swapped |
| |
| Solution : extends the delegate -> point field at this -> |
| delete pure forwarders one by one -> |
| convert real differences to overrides (super.x) -> |
| remove field + fix construction sites |
| |
| Inverse : Replace Inheritance with Delegation (the seesaw) |
| Cousins : Remove Middle Man (clients go direct), |
| Collapse Hierarchy (if the subclass ends up empty) |
| Trap : partial coverage inherited = NEW Refused Bequest |
+------------------------------------------------------------------+Practice exercise ✏️
Your turn. An inventory service wraps the team's storage client, and the wrapper has become everyone's least favourite file:
class StorageClient {
get(key: string): string | null { /* ... */ return null; }
put(key: string, value: string): void { /* ... */ }
delete(key: string): void { /* ... */ }
exists(key: string): boolean { /* ... */ return false; }
listKeys(prefix: string): string[] { /* ... */ return []; }
flush(): void { /* ... */ }
}
class InventoryStore {
private client: StorageClient;
constructor(client?: StorageClient) {
this.client = client ?? new StorageClient(); // sometimes injected!
}
get(key: string): string | null { return this.client.get(key); }
delete(key: string): void { this.client.delete(key); }
exists(key: string): boolean { return this.client.exists(key); }
listKeys(prefix: string): string[] { return this.client.listKeys(prefix); }
flush(): void { this.client.flush(); }
put(key: string, value: string): void {
if (key.startsWith("sku:") === false) {
throw new Error("Inventory keys must start with sku:"); // real behaviour
}
this.client.put(key, value);
}
}Work through it:
- Run the coverage test (count forwarders versus total interface) and the is-a test. One of them deserves a long pause:
putrejects keys the parent would accept. Is a store that refuses some valid parent inputs still substitutable "with zero surprises"? Write your verdict in two sentences — this is the hardest and most valuable question on the page. Then placeInventoryStoreon Figure 3's map. - Investigate the constructor:
clientis sometimes injected. Search the (imaginary) codebase: the unit tests pass a fakeStorageClient. What does that tell you about which side of the seesaw this class belongs on? - Suppose the team decides the validation in
putshould move down into a newStorageClient.putValidatedhook, tests will fake the network layer instead, and injection is removed. Now run the conversion through Figure 7's states:extends, field tothis, delete the five forwarders one test-run at a time, convertputto anoverridecallingsuper.put, remove the field and constructor. - After conversion,
InventoryStorecontains exactly one method. Ask the follow-up question this series has trained you to ask: does a one-override subclass earn its existence, or is the next refactoring Collapse Hierarchy? Defend your answer in one sentence. - Bonus thinking: your teammate says "let's also make
ReportStoreandUserStoreinheritStorageClient, they forward a lot too."UserStoreforwards onlygetandputand deliberately hidesdelete. Which refactoring does each store actually need, and why are the answers different?
If your step 1 verdict wrestled honestly with "throwing on inputs the parent accepts weakens substitutability — so the is-a is questionable until that rule moves into the parent's own contract," you have understood not just two refactorings but the entire seesaw between them. That judgement — not the mechanics — is the skill. Masterji promoted Chhotu only after five years of watching; promote your wrappers with the same patience. Well done.
Frequently asked questions
- Isn't this refactoring against the 'composition over inheritance' rule?
- No — it completes it. The maxim says favor composition, not always use composition. When a wrapper genuinely is a kind of its delegate, supports essentially the whole contract, and gains nothing from the indirection, inheritance is the honest and cheaper design. The maxim picks the default; this refactoring handles the proven exception.
- How do I know the wrapper qualifies for inheritance?
- Two conditions must both hold. First, the is-a test: the wrapper can substitute for the delegate anywhere, with zero surprises. Second, coverage: the wrapper forwards essentially the delegate's entire public interface, not a curated slice. If it forwards only part, inheriting would drag in the unwanted rest as Refused Bequest — keep the delegation.
- What is the difference between this refactoring and Remove Middle Man?
- Both attack a class drowning in forwarding methods. Remove Middle Man deletes the wrapper's pass-throughs and lets clients talk to the delegate directly — the wrapper steps aside. Replace Delegation with Inheritance keeps the wrapper as the public face but makes it a subclass, so the forwarding becomes free inheritance. Choose by asking whether the wrapper deserves to exist as a type at all.
- What if the class delegates to two different objects?
- Single inheritance lets you absorb only one. Pick the delegate whose entire contract the class genuinely supports — if any — and inherit from it; the other collaborator stays as a field. And if neither delegate passes the full is-a-plus-coverage test, the forwarding, however boring, is the correct design.
- The delegate class is sealed or final. Can I still do this refactoring?
- No — a sealed or final class cannot be subclassed, and that is often a deliberate message from its author that inheritance was not part of its design contract. Keep the delegation, or if the boilerplate truly hurts, look at Remove Middle Man or code-generation for the forwarders instead.
Further reading
Related Lessons
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.
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.
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#.
Remove Middle Man: When the Peon Only Forwards, Meet the Principal Directly
Learn the Remove Middle Man refactoring with a story about a school peon who forwards every question to the principal without adding anything. When a class only forwards calls to its delegate, delete the forwarding and let clients talk to the delegate directly. Step-by-step TypeScript and C# walkthrough.