Introduce Local Extension: Build a Cabin Next to the Rented Shop
Learn the Introduce Local Extension refactoring with a story about building an attached cabin beside a rented shop you cannot modify. When a locked class is missing many methods, gather them into one extension type — a subclass, a wrapper, or a modern C#/Kotlin extension class. Full TypeScript and C# walkthrough.
🏠 Manoj uncle and the cabin beside the rented shop
Manoj uncle runs a tailoring business from a rented shop in Indore. The shop is good — main road, strong walls, regular customers since fifteen years. But it is rented, and the landlord, Mr. Saxena, has one strict rule written into the agreement: no changes to the building. No new shelves drilled into walls, no extra room broken through, no rewiring. Mr. Saxena visits every month and runs his palm along the walls like a doctor checking a patient.
For a while, Manoj uncle manages with small tricks. A hook that hangs over the door without drilling. A folding table he carries home each night on his scooter. A plastic tub of buttons under the counter. These are his "staplers in the bag" — tiny portable fixes, exactly like the foreign methods we met in the previous post.
But the business grows. Wedding season doubles the orders. Now he needs a cutting table, an ironing corner, a customer waiting bench, and storage for fifty suit pieces. Seven portable tricks scattered around a shop he cannot modify? The hook falls every Tuesday, the folding table wobbles mid-cut, his assistant Bablu can never find the measuring book, and a customer once sat on the buttons tub.
So Manoj uncle does the smart thing. There is an open plot beside the shop — his own plot, bought years ago. He builds a small attached cabin on it. The rented shop stays untouched; Mr. Saxena runs his palm along the walls and finds nothing to complain about. The cabin holds everything the shop is missing — cutting table, iron stand, bench, storage — all in one solid, organised place. A connecting door makes the shop and cabin work as one unit. Customers do not even notice where shop ends and cabin begins.
This is exactly the Introduce Local Extension refactoring. When a class you cannot modify is missing not one method but a whole family of methods, you stop scattering helper functions everywhere. You build one new type — your own cabin — that holds all the additions and connects smoothly back to the original. The locked class stays locked. Your extension is fully yours: named, tested, reusable.
🔍 What is Introduce Local Extension?
Introduce Local Extension is the heavier sibling of Introduce Foreign Method, both from Fowler's Refactoring. The situation: a foreign, unmodifiable class needs several additional methods. The recipe: create a new type containing all of them, built so that it behaves like the original plus your additions.
Fowler describes two classic shapes:
- Subclass form — extend the foreign class and add your methods. Works when the class permits inheritance. Instances are usable anywhere the original is expected, automatically.
- Wrapper form — a new class holding the foreign instance in a field, adding your methods, and delegating or converting where needed. Works even when the class is sealed.
Here is the wrapper form, gathering scattered date helpers into one home:
class CalendarDate {
constructor(public readonly value: Date) {}
nextDay(): CalendarDate {
const d = new Date(this.value);
d.setDate(d.getDate() + 1);
return new CalendarDate(d);
}
isWeekend(): boolean {
const day = this.value.getDay();
return day === 0 || day === 6;
}
endOfMonth(): CalendarDate {
return new CalendarDate(
new Date(this.value.getFullYear(), this.value.getMonth() + 1, 0)
);
}
}
// Usage — fluent, chainable, readable:
const settlement = new CalendarDate(invoiceDate).nextDay().endOfMonth();The three operations that were previously private helpers in three different services now live together, chain together, and can be unit-tested as one unit. The value property is the connecting door back to the plain Date whenever an API demands the original type.
When client code uses the extension, the locked type quietly serves from behind the wall — every added method ultimately reads the original's public surface:
Think of the local extension as the answer to a naming question: what IS this cluster of helpers, really? Scattered functions nextDay, isWeekend, endOfMonth are anonymous machinery. Gathered into CalendarDate, they reveal a missing CONCEPT — your domain's idea of a calendar date. Good local extensions often graduate into genuine domain types.
🚦 When do we need it?
Choose Introduce Local Extension when:
- Foreign methods have multiplied. Three or more helpers for the same locked type, especially spread across different client classes, each invisible to the others. The lightweight fix has been outgrown.
- The helpers duplicate. Two services each privately implemented
isWeekend— slightly differently. Scattering breeds divergence; one home enforces one truth. - The operations form a concept. Together they describe something your domain cares about — a calendar date, a money amount, a phone number — that the foreign type only approximates.
- You want the additions tested and reused as a unit. Private helpers buried in services are hard to test alone; a small extension type is trivially testable.
Skip it when:
- You own the class. Just add the methods, or use Move Method. The whole point of this refactoring is the cannot-modify constraint.
- One or two methods suffice. Stay with Introduce Foreign Method; a wrapper for one method is a cabin for one hook.
- The wrapper would mostly forward. If your extension adds two methods but forwards forty, you have manufactured a Middle Man around someone else's class — clients drown in delegation noise. Either choose the subclass/extension-method form (no forwarding needed), or reconsider the boundary. The same dial logic from Hide Delegate territory applies: wrapping should add value, not echo it. And as with Message Chains, the goal is always that clients talk to one sensible object, not to layers of plumbing.
Before the consolidation, where do the date helpers in a typical service codebase actually live? Run the inventory and the answer is usually embarrassing:
And the scatter is not static — it grows with the team. Every new service that needs a date trick re-implements it, because nobody can see the other services' private helpers:
👀 Before and after at a glance
// ---------- BEFORE: the same concept, shattered across services ----------
class BillingService {
private nextDay(d: Date): Date { // foreign method #1
const r = new Date(d); r.setDate(r.getDate() + 1); return r;
}
}
class PayrollService {
private isWeekend(d: Date): boolean { // foreign method #2
return d.getDay() === 0 || d.getDay() === 6;
}
private endOfMonth(d: Date): Date { // foreign method #3
return new Date(d.getFullYear(), d.getMonth() + 1, 0);
}
}
class ReportService {
private isWeekend(d: Date): boolean { // DUPLICATE of #2!
return [0, 6].includes(d.getDay());
}
}// ---------- AFTER: one local extension, one truth ----------
class CalendarDate {
constructor(public readonly value: Date) {}
nextDay(): CalendarDate { /* as above */ return this; }
isWeekend(): boolean { /* one definition */ return true; }
endOfMonth(): CalendarDate { /* one definition */ return this; }
}
// Every service now:
const payday = new CalendarDate(rawDate).endOfMonth();
if (payday.isWeekend()) { /* shift to Friday */ }🪜 Step-by-step, the safe way
Step 1 — Inventory the scattered helpers. Search the codebase for every foreign method targeting the type. List them, note duplicates, and note behavioral differences between duplicates (these matter!).
Step 2 — Choose the form.
| Question | If yes → |
|---|---|
| Is the class open to inheritance, and do callers need to pass your instances where the original is expected? | Subclass |
| Is the class sealed/final, or do you want added state and enforced rules? | Wrapper |
| Does your language have extension methods/functions, and do you need no new state? | Extension class (the modern third form) |
The same decision as a map — find your situation, read off the form:
Step 3 — Create the empty extension type with its bridge. For a wrapper, store the instance and expose it; for a subclass, forward the constructors. No behavior yet:
class CalendarDate {
constructor(public readonly value: Date) {}
// bridge back to the foreign type = the .value door
}Compile, test. Green, trivially.
Step 4 — Move ONE helper onto the extension. Take isWeekend from PayrollService, re-home it on CalendarDate, with the old first-parameter becoming this.value. Leave the old helper in place for a moment, delegating:
class PayrollService {
// temporary shim during migration:
private isWeekend(d: Date): boolean {
return new CalendarDate(d).isWeekend();
}
}Test. Then redirect the service's call sites to use CalendarDate directly, and delete the shim.
Step 5 — Repeat per helper, resolving duplicates deliberately. When two copies differ (ReportService vs PayrollService versions of isWeekend), pick the correct behavior consciously and write a test pinning it down before unifying.
Step 6 — Wire the seams. Wherever code holds a CalendarDate but an API wants a Date, pass .value. If the back-and-forth gets noisy, add a static factory (CalendarDate.of(raw)) and keep conversions at module boundaries.
Step 7 — Test the extension as a unit. The big payoff: CalendarDate now gets its own small, fast test file — something the scattered private helpers could never have.
The whole migration, seen as states with safe pause points between them:
Move one helper at a time and run the tests between moves. The riskiest moment is unifying duplicated helpers whose behavior silently differs — like two isWeekend implementations that disagree about a locale. A unification that changes behavior is not refactoring; it is a feature change hiding inside a cleanup. Pin current behavior with tests first, then decide which behavior wins, openly.
💰 A bigger real-life example
Your app receives Money-like values from a payment SDK as plain paise integers attached to a locked SdkPayment type. Over months, the team has grown five scattered helpers: format in rupees, add two payments, GST portion, is-refundable, comparison. Let us build the cabin:
// Locked SDK type — regenerated on every update:
class SdkPayment {
constructor(
public readonly paise: number,
public readonly capturedAt: Date
) {}
}
// The local extension — wrapper form, with added rules:
class Rupees {
private constructor(public readonly paise: number) {
if (!Number.isInteger(paise)) throw new Error("paise must be integer");
}
static fromPayment(p: SdkPayment): Rupees { return new Rupees(p.paise); }
static of(paise: number): Rupees { return new Rupees(paise); }
plus(other: Rupees): Rupees { return new Rupees(this.paise + other.paise); }
gstPortion(ratePercent: number): Rupees {
return new Rupees(Math.round((this.paise * ratePercent) / (100 + ratePercent)));
}
isMoreThan(other: Rupees): boolean { return this.paise > other.paise; }
toString(): string { return `₹${(this.paise / 100).toFixed(2)}`; }
}
// Client code — five old helpers replaced by one fluent type:
const bill = Rupees.fromPayment(payment1).plus(Rupees.fromPayment(payment2));
console.log(`Total: ${bill}, GST inside: ${bill.gstPortion(18)}`);Two things to admire. First, the wrapper added state rules the foreign type never had — the integer check in the constructor — something a plain extension method can never do. Second, this local extension is quietly curing another smell too: Primitive Obsession. The number-with-meaning (paise) became a real type with behavior. Wrapper-form local extensions around primitive-ish foreign values often double as domain modeling.
College corner: The wrapper form raises a question every design course should ask: what happens to equality and identity? Two Rupees objects wrapping the same paise value are different objects — reference equality says they differ, which breaks sets, maps, and innocent-looking comparisons. A serious wrapper therefore defines value semantics deliberately: an equals based on the wrapped value (and a matching hash), ideally immutability so the value can never drift after hashing. The subclass form has a different theoretical risk: Liskov substitution. A subclass extension is passed wherever the parent is expected, so your additions must never weaken the parent's contract — and if the library constructs and returns parent instances (factories, deserialisers), your subclass methods are unreachable on those objects. This "you do not control instantiation" problem is the main practical reason wrappers and extension classes beat subclassing for framework types.
💻 The same refactoring in C#
C# is the star of this section, because the language absorbed this refactoring twice.
Form 3, classic: the extension-method class. Since C# 3.0, you can group all the missing operations in one static class and call them as if they were on the foreign type — no subclass, no wrapper, no conversion seams:
public static class DateTimeCalendarExtensions
{
public static DateTime NextDay(this DateTime d) => d.AddDays(1);
public static bool IsWeekend(this DateTime d) =>
d.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
public static DateTime EndOfMonth(this DateTime d) =>
new(d.Year, d.Month, DateTime.DaysInMonth(d.Year, d.Month));
}
// Call sites — indistinguishable from native members:
var payday = invoiceDate.EndOfMonth();
if (payday.IsWeekend()) payday = payday.NextDay().NextDay();This is the entire "local extension" with zero wrapper boilerplate: one named class gathers the family, IntelliSense advertises the methods on DateTime itself, and chaining works naturally. Kotlin does the same with extension functions (fun LocalDate.endOfMonth(): LocalDate = ...), which compile down to plain static functions taking the receiver as a parameter.
Form 4, modern: C# 14 extension members. With .NET 10, C# 14 added extension blocks that support extension properties, operators, and even static extension members — closing most of the remaining gap with real members:
public static class DateTimeCalendarExtensions
{
extension(DateTime d)
{
public DateTime NextDay => d.AddDays(1); // property!
public bool IsWeekend =>
d.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
public DateTime EndOfMonth =>
new(d.Year, d.Month, DateTime.DaysInMonth(d.Year, d.Month));
}
}
// Reads exactly like the type always had these properties:
if (invoiceDate.EndOfMonth.IsWeekend) { /* shift settlement */ }The old and new forms are source- and binary-compatible, so teams adopt the block syntax gradually.
When does C# still need the wrapper form? When you need new state or enforced invariants — extension members cannot add fields or constructor checks. Then the classic Fowler wrapper returns:
public readonly struct Rupees
{
public int Paise { get; }
public Rupees(int paise) =>
Paise = paise >= 0 ? paise : throw new ArgumentOutOfRangeException();
public Rupees Plus(Rupees other) => new(Paise + other.Paise);
public override string ToString() => $"₹{Paise / 100m:F2}";
public static implicit operator int(Rupees r) => r.Paise; // the door back
}Rule of thumb in modern C#/Kotlin: behavior only → extension class; behavior plus state or rules → wrapper; substitutability into the original's APIs → subclass (when allowed).
College corner: Why are C#/Kotlin extension classes immune to the Middle Man trap that haunts wrappers? Because they add without intercepting. A wrapper stands between the client and the original, so every original operation the client still needs must be forwarded — that forwarding is the middle-man tax. An extension class stands beside the original: clients keep calling the real DateTime members directly, and only the additions route through the static class. The compile-time rewrite (d.NextDay() becomes DateTimeCalendarExtensions.NextDay(d)) costs nothing at runtime after JIT inlining and forwards nothing. The trade-off is the same one foreign methods have: no access to non-public state, no polymorphism, and resolution depends on imports — which is why discoverability conventions (one well-named extensions file per type, predictable namespace) matter as much as the code itself.
🐍 And once in Python
Python has no extension methods, and monkey-patching built-ins is blocked (and unwise elsewhere). So the wrapper is the idiomatic Python cabin:
class CalendarDate:
def __init__(self, value: date):
self.value = value # the connecting door
def next_day(self) -> "CalendarDate":
return CalendarDate(self.value + timedelta(days=1))
def is_weekend(self) -> bool:
return self.value.weekday() >= 5 # Sat=5, Sun=6
def end_of_month(self) -> "CalendarDate":
last = calendar.monthrange(self.value.year, self.value.month)[1]
return CalendarDate(self.value.replace(day=last))
def __eq__(self, other): # value semantics, deliberately
return isinstance(other, CalendarDate) and self.value == other.valueNote the __eq__: that is the equality lesson from the College corner, applied. Without it, two CalendarDate objects holding the same date would compare unequal, and a set of paydays would silently contain duplicates.
⚖️ The balance: how much machinery for a locked class?
This refactoring sits on a dial with its lighter sibling — the same kind of dial Hide Delegate shares with Remove Middle Man:
- A couple of missing methods → Introduce Foreign Method. The stapler in the bag.
- A growing, duplicating family of helpers → Introduce Local Extension. The attached cabin.
- And the overshoot to watch for: a wrapper that forwards forty methods to add two — that is the Middle Man failure mode, and the cure is to slim the wrapper or switch to the extension-class form.
Build the smallest structure that gives the concept one home. Upgrade only when the count and the duplication tell you to.
✅ Benefits and risks
| Benefit | Why it matters |
|---|---|
| All missing operations in one named home | The concept becomes visible, findable, and reusable |
| Independently unit-testable | Scattered private helpers never were |
| Duplicates collapse into one truth | Divergent copies (and their bugs) disappear |
| Wrapper form can add state and rules | Invariants the foreign type never enforced |
| Native forms (C#/Kotlin) erase the boilerplate | Extension classes need no forwarding at all |
| Risk | How to handle it |
|---|---|
| Heavier than the problem deserves | For 1–2 methods, stay with Introduce Foreign Method |
| Wrapper forwarding noise → accidental Middle Man | Forward only what is used; prefer extension classes when no state is needed |
| Equality and identity surprises with wrappers | Define equals/conversions deliberately; prefer value semantics (readonly struct, ===-safe designs) |
| Subclass form blocked by sealed types | Most framework types are sealed — default to wrapper or extension class |
| Conversion seams at API boundaries | Keep conversions at module edges; expose a single obvious door (.value / implicit operator) |
🧪 Which smells does it cure?
| Smell | How Introduce Local Extension helps |
|---|---|
| Duplicate Code | Merges repeated helpers for one foreign type into one definition |
| Scattered foreign methods | Gathers the whole family into a single named type |
| Primitive Obsession | Wrapper form turns a raw value into a real domain type with rules |
| Shotgun Surgery (for that concept) | Changes to the concept's rules touch one type, not many services |
| Middle Man | ⚠️ Does NOT cure it — an over-forwarding wrapper creates it |
🛠️ IDE support
- Rider / ReSharper: Extract Class and Make Static plus the "Convert to extension method" context action turn scattered helpers into an extension class quickly; Move Static Members gathers helpers from multiple classes into one. Full refactoring support for the C# 14
extensionblock syntax arrived alongside .NET 10 tooling. - Visual Studio: Quick Actions can convert eligible static methods to extension methods; Find All References maps every scattered helper before consolidation; IntelliSense surfaces finished extensions on the foreign type, which is what makes the consolidated home discoverable to the whole team.
- IntelliJ IDEA (Kotlin): first-class support for extension functions, including Convert receiver to parameter and back, and Move refactorings to collect extensions into one file — the Kotlin idiom for a local extension is literally "a file of extensions on the type."
- VS Code (TypeScript): no automated class-extraction across files; use Find All References for the inventory step and lean on the compiler during migration. TypeScript has no native extension methods (and module augmentation of library prototypes is discouraged), so the wrapper form remains the idiomatic choice.
📦 Quick revision box
+-----------------------------------------------------------------+
| INTRODUCE LOCAL EXTENSION — CHEAT SHEET |
+-----------------------------------------------------------------+
| Situation : locked class missing SEVERAL methods |
| Move : build ONE extension type holding them all |
| Form 1 : SUBCLASS - class is open, need substitutability |
| Form 2 : WRAPPER - class is sealed, or need state/rules |
| Form 3 : EXTENSION CLASS - C# / Kotlin native support |
| Bridge : keep a clear door back to the original type |
| Cures : Duplicate Code, scattered helpers, Primitive |
| Obsession (wrapper form) |
| Too small? : 1-2 methods -> Introduce Foreign Method instead |
| Overshoot? : forwarding-heavy wrapper -> Middle Man, slim it |
| Own it? : your class -> just add methods (Move Method) |
+-----------------------------------------------------------------+✏️ Practice exercise
Your project depends on a locked SDK type for courier tracking:
// Generated SDK type. DO NOT EDIT.
class TrackingEvent {
constructor(
public readonly pincode: string, // "560001"
public readonly status: string, // "IN_TRANSIT" | "OUT_FOR_DELIVERY" | "DELIVERED"
public readonly timestamp: number // epoch millis
) {}
}Scattered across four modules you find these helpers (some duplicated, one pair inconsistent):
// notifications module:
const isFinal = event.status === "DELIVERED";
// dashboard module:
const city = event.pincode.startsWith("5600") ? "Bengaluru" : "Other";
const when = new Date(event.timestamp).toLocaleString("en-IN");
// sla module:
const isLate = Date.now() - event.timestamp > 48 * 3600 * 1000;
// audit module — different lateness rule!
const isLate2 = Date.now() - event.timestamp > 72 * 3600 * 1000;Your tasks:
- Inventory the missing operations on
TrackingEvent. Which ones are duplicates? Which pair disagrees? Draw your own Figure 4 pie of where they live. - The two lateness rules differ (48 vs 72 hours). Investigate which is correct — then design the method so the difference is explicit (hint:
isOlderThan(hours: number)keeps both callers honest). - Use the Figure 7 map to choose your form: in TypeScript you will land on the wrapper; on a C# project, decide between
TrackingEventExtensionsand a wrapper, and justify with the state-or-rules test. - Build a
Shipmentwrapper (or extensions class) holding:isDelivered(),cityFromPincode(),localTime(),isOlderThan(hours). Migrate one module at a time, tests between moves, pausing at each state from Figure 8. - Write a dedicated unit-test file for your new type — the thing the scattered helpers never had. Five tiny tests minimum, including one equality test if you chose the wrapper.
- Verdict question: your wrapper has four added methods and zero forwarded ones. Is it in Middle Man danger? Why not? Write one sentence.
When your one-sentence answer mentions "it adds value instead of echoing the SDK," you have connected all four posts in this series — chains, middle men, staplers, and cabins — into one picture of where behavior should live. Manoj uncle's cabin, by the way, now has a small signboard of its own. Mr. Saxena still runs his palm along the rented walls every month and finds them exactly as they were. Both of them are happy — which is the whole point of extending what you cannot change.
Frequently asked questions
- What is the Introduce Local Extension refactoring?
- When a class you cannot modify is missing SEVERAL methods, you create one new type — a subclass or a wrapper — that contains all the missing methods and otherwise behaves like the original. Instead of scattering helper functions across client classes, all the additions live in one named, reusable, testable place.
- Should I choose a subclass or a wrapper for my local extension?
- Subclass when the foreign class allows inheritance and you want instances to be usable anywhere the original is expected. Wrapper when the class is sealed or final (very common for framework types), or when you want tighter control. The wrapper holds the original instance in a field and delegates or converts as needed. Many modern teams pick a third form: a class of extension methods, which needs neither inheritance nor wrapping.
- When is Introduce Foreign Method enough instead?
- For one or two missing methods, a foreign method — a simple helper taking the foreign object as a parameter — is lighter and faster. Graduate to a local extension only when the helpers multiply, start duplicating across client classes, or form a concept that deserves its own name.
- How do C# and Kotlin change this refactoring?
- They give it native syntax. C# extension methods (and C# 14 extension members, including extension properties) let you group all the missing operations in one static class and call them as if they were on the original type — no subclass, no wrapper, no conversion seams. Kotlin extension functions do the same. This third form is now the default choice in those languages, with wrappers reserved for when you need new state or enforced invariants.
- What are the main risks of a wrapper-style local extension?
- Identity and equality surprises (two wrappers around equal values are not automatically equal), conversion boilerplate at every boundary where the plain type is expected, and the wrapper drifting into a Middle Man if you end up forwarding dozens of methods. Define equality and conversions deliberately, and keep the wrapper focused on its added value.
Further reading
Related Lessons
Introduce Foreign Method: The Stapler You Keep in Your Own Bag
Learn the Introduce Foreign Method refactoring with a story about a school photocopy machine that has no stapler. When a class you cannot modify is missing a method, write that method in your own class with the foreign object as a parameter. TypeScript and modern C# extension-method examples included.
Duplicate Code: Writing the Same Address on 50 Wedding Cards
Learn the Duplicate Code smell with a wedding card story. Understand DRY, the Rule of Three, and how Extract Method removes dangerous copy-paste code.
Move Method: Shift Work to the Class Where It Truly Belongs
Learn the Move Method refactoring through a simple school story. Shift a method into the class whose data it uses most so behaviour and data stay together.
Primitive Obsession: When Everything Is Just a String or a Number
Primitive Obsession explained simply — why plain strings and numbers hide bugs, and how value objects like Money and Address make code safe and clear.