Extract Subclass: Give the Special Case Its Own Class
Learn the Extract Subclass refactoring with a tailor-shop story about urgent orders, flag-removal in TypeScript and C#, safe step-by-step moves, and when subclassing is the wrong tool.
🧵 The tailor's one fat order book
Masterji Akram runs a famous tailor shop in Lucknow's Aminabad market. For thirty years he has written every order in one thick order book with a brown cover gone soft at the corners. Most orders are normal: take measurements, stitch in seven days, charge the list price.
But some orders are urgent. A wedding is on Sunday. A school function is on Friday. For urgent orders, the rules change: the customer pays a 30% extra charge, the order jumps the stitching queue, and Masterji's assistant Raju must phone the customer one day before delivery.
Here is how the shop handles it today. On every page of the one big book, there is a small box: URGENT? YES/NO. And all over the shop, everyone keeps checking that box. Imran, the billing nephew, checks it before writing the bill — if urgent, add 30%. The stitching team checks it before picking the next job — if urgent, take it first. Raju checks every page each evening — if urgent, phone the customer. The page also has extra columns — "function date" and "phone reminder done?" — that stay empty for normal orders, which is most pages of the book.
One Thursday, Imran forgets to check the box. An urgent sherwani for a Sunday wedding is billed at the normal price, stitched in normal time, and Raju never phones because the page looked ordinary at a glance. On Saturday night, the groom's furious uncle arrives at the shop. Masterji stays up until 3 a.m. finishing the sherwani himself, and gives a discount in apology. One missed YES/NO box nearly cost the shop its thirty-year name.
Masterji's daughter Nazia, who studies commerce, watches all this and suggests a simple fix: keep a separate red order book just for urgent orders. Every page in the red book automatically follows urgent rules — the 30% charge is printed in the price formula, the queue rule is the book's own rule, the reminder column exists on every page and is never blank-for-no-reason. Nobody checks any YES/NO box anymore. Which book the order sits in is the answer.
In code, this fix is called Extract Subclass. When one class is secretly doing two jobs — a normal case and a special case, separated by a flag — we give the special case its own subclass.
📕 What is Extract Subclass?
A class should describe one concept clearly. But classes often grow a second personality. You can recognise the disease by three symptoms:
| Symptom | What it looks like | In Masterji's shop |
|---|---|---|
| A type flag | A field like isUrgent: boolean or kind: "normal" or "urgent" splitting instances into camps | The URGENT? YES/NO box on every page |
| Conditionals everywhere | Many methods contain the same if (this.isUrgent) branch | Imran, the stitching team, and Raju each re-checking the box |
| Sometimes-empty fields | Fields filled only when the flag is on, null the rest of the time | "Function date" and "reminder done?" columns blank on most pages |
That third symptom has its own name — the Temporary Field smell — and it is the quiet one. Nobody crashes because of an empty column; people just slowly stop trusting any column.
Extract Subclass says: create a new class UrgentOrder extends Order. Move the urgent-only fields into it. For every method that branched on the flag, put the normal behaviour in the parent and override it in the subclass with the urgent behaviour. Then delete the flag.
After the refactoring, the question "is this order urgent?" is answered by the type system, not by a runtime check. Calling order.totalPrice() automatically runs the right formula, because the object knows what it is. This is polymorphism doing the if-checking for you, once and forever.
A small note on names: Fowler's first edition called this Extract Subclass; the second edition folds the same idea into Replace Type Code with Subclasses, since extracting a subclass is almost always about retiring a type code or flag. Refactoring Guru keeps the classic Extract Subclass name. Same medicine, two labels on the bottle.
The decision the shop faced fits in one flowchart:
That middle question — is urgency fixed at order time? — matters more than it looks, and we will return to it in the cautions.
One-line summary: Extract Subclass moves a flag-guarded special case into its own subclass, so scattered if-checks become overridden methods and sometimes-empty fields find a home where they are always meaningful.
🔔 When do we need it?
Reach for Extract Subclass when you see:
- The same flag checked in many methods.
if (isUrgent)appearing intotalPrice(),queuePosition(),needsReminder(), and five more places means the variant logic is smeared across the class. Each new urgent rule means hunting down every branch. - A class doing two jobs. A class that models both the common case and a special case is on its way to becoming a Large Class — twice the fields, twice the branches, twice the reading effort.
- Copy-paste between the branches. When the
ifandelsearms repeat most of their code with tiny differences, you are maintaining hidden Duplicate Code inside one class. - Fields that are usually null. Urgent-only data on every normal order is wasted space and a constant "can this be null here?" worry.
How big is the special case, really? Nazia actually counted the brown book before proposing the red one:
Thirty percent of orders were urgent — yet every page carried the urgent columns, and every worker paid the cost of checking the box on every order. A special case does not need to be rare to deserve its own class; it needs to be different.
And know when not to use it:
- The kind changes at runtime. A normal order that becomes urgent on Wednesday cannot change its class — no language allows an object to swap its type mid-life. Use the State/Strategy pattern instead (the "swappable rule card" in Figure 2).
- The variation is tiny. One extra field, no behaviour difference? A simple optional field or parameter is cheaper than a class.
- You would inherit things the child must refuse. If the new subclass would have to throw "not supported" for half the parent's methods, you are manufacturing the Refused Bequest smell. The special case may not really be "a kind of" the parent at all.
The full decision sits neatly on a quadrant:
The urgent order sits top-left — kind decided the moment the customer walks in, behaviour very different — perfect subclass territory. An order status (received, stitching, ready, delivered) sits top-right: it changes constantly during the object's life, so it must be a state object, never a subclass. Gift wrapping, bottom-left, is one boolean with no behaviour — leave it as a field and move on with your day.
College corner: why exactly can an object not change its class at runtime? Because in C#, Java, and most class-based languages, an object's type determines its memory layout and method dispatch table (vtable) at allocation time. "Becoming" an UrgentOrder would mean re-allocating and re-pointing every existing reference — which the runtime cannot do safely. The State pattern sidesteps this by keeping the object's identity stable and swapping an inner reference: order.pricing = new UrgentPricing(). Identity stays, behaviour changes. Exam keyword: favour composition over inheritance when the variation is dynamic.
🔍 Before and after at a glance
Masterji's order book in TypeScript, flag and all:
// BEFORE: one class, two personalities, flag-checks everywhere
class TailorOrder {
constructor(
public garment: string,
public listPrice: number,
public isUrgent: boolean, // the type flag
public functionDate?: Date, // empty for normal orders
) {}
totalPrice(): number {
if (this.isUrgent) {
return this.listPrice * 1.3; // 30% urgent charge
}
return this.listPrice;
}
queuePriority(): number {
return this.isUrgent ? 1 : 10; // urgent jumps the queue
}
needsReminderCall(): boolean {
return this.isUrgent; // assistant phones only urgent customers
}
}After Extract Subclass — the red book exists:
// AFTER: two honest classes, zero flag-checks
class TailorOrder {
constructor(public garment: string, public listPrice: number) {}
totalPrice(): number { return this.listPrice; }
queuePriority(): number { return 10; }
needsReminderCall(): boolean { return false; }
}
class UrgentOrder extends TailorOrder {
constructor(garment: string, listPrice: number, public functionDate: Date) {
super(garment, listPrice);
}
override totalPrice(): number { return this.listPrice * 1.3; }
override queuePriority(): number { return 1; }
override needsReminderCall(): boolean { return true; }
}Count the wins: the isUrgent flag is gone, three if-checks are gone, and functionDate is now always filled in the only class that has it. A normal order physically cannot carry urgent-only baggage.
And watch how billing changes as a conversation. Before, Imran had to interrogate every order; after, the order simply is what it is:
The if-check count tells the same story in one bar chart:
Nine checks in the bigger shop codebase (billing, queueing, reminders, reports, SMS module...) collapsed to zero — well, to one, but that one lives in a single blessed place, as the next section shows.
🪜 Step-by-step, the safe way
Take small steps. Compile and test after each one. The refactoring passes through five compile-clean states:
- Create the empty subclass. Just the shell, extending the original class. Nothing breaks; nothing changed yet.
// INTERMEDIATE STEP: empty subclass, flag still alive
class UrgentOrder extends TailorOrder {
constructor(garment: string, listPrice: number, functionDate: Date) {
super(garment, listPrice, /* isUrgent */ true, functionDate);
}
}- Route creation through a factory. Find every place that constructs the original class. Replace direct construction with one factory function that looks at the raw input and picks the right class. This is the one place a kind-check is allowed to survive.
function createOrder(garment: string, price: number, functionDate?: Date): TailorOrder {
return functionDate
? new UrgentOrder(garment, price, functionDate)
: new TailorOrder(garment, price, false);
}-
Move variant-only fields down.
functionDatebelongs toUrgentOrderalone. Move it, using Push Down Field — the data cousin of Push Down Method. The parent constructor loses a parameter; the subclass keeps it. -
Override one branching method at a time. Pick
totalPrice(). Put the urgent arm of theifinto an override onUrgentOrder; leave the normal arm in the parent; delete the conditional. Run tests. Then doqueuePriority(). ThenneedsReminderCall(). -
Delete the flag. Once no method reads
isUrgent, remove the field and its constructor parameter. The compiler confirms nobody misses it. -
Run the full test suite. Same behaviour, cleaner shape.
Do step 2 before step 4. If you start overriding methods while callers still construct the base class with isUrgent: true, those objects will be base-class instances with urgent data and normal behaviour — a silent bug. Creation must pick the right class first; only then is it safe to move behaviour into overrides.
🧮 A bigger real-life example
Masterji's shop goes digital, and the order class has grown a third concern: bulk school-uniform orders, with their own discount and delivery rules. Two flags now fight inside one class:
// BEFORE: two flags, four possible (and two impossible) combinations
class StitchingOrder {
constructor(
public garment: string,
public listPrice: number,
public quantity: number,
public isUrgent: boolean,
public isBulk: boolean,
public functionDate?: Date,
public schoolName?: string,
) {}
totalPrice(): number {
let price = this.listPrice * this.quantity;
if (this.isBulk) price = price * 0.85; // 15% bulk discount
if (this.isUrgent) price = price * 1.3; // 30% urgent charge
return price;
}
deliveryDays(): number {
if (this.isUrgent) return 2;
if (this.isBulk) return 21;
return 7;
}
}The business rule says an order is either normal, or urgent, or bulk — never urgent and bulk. But the flags cannot express that; isUrgent: true, isBulk: true compiles happily and prices nonsense. Subclasses fix this, because an object has exactly one class:
// AFTER: three kinds, each impossible to mix up
class StitchingOrder {
constructor(public garment: string, public listPrice: number, public quantity: number) {}
totalPrice(): number { return this.listPrice * this.quantity; }
deliveryDays(): number { return 7; }
}
class UrgentOrder extends StitchingOrder {
constructor(garment: string, listPrice: number, quantity: number, public functionDate: Date) {
super(garment, listPrice, quantity);
}
override totalPrice(): number { return super.totalPrice() * 1.3; }
override deliveryDays(): number { return 2; }
}
class BulkOrder extends StitchingOrder {
constructor(garment: string, listPrice: number, quantity: number, public schoolName: string) {
super(garment, listPrice, quantity);
}
override totalPrice(): number { return super.totalPrice() * 0.85; }
override deliveryDays(): number { return 21; }
}Notice how super.totalPrice() reuses the parent's base calculation — the subclasses add only their difference. That is inheritance used exactly as intended: shared code up, special code down.
College corner: this "make illegal states unrepresentable" idea is a big deal in type theory. Two booleans give you 2 × 2 = 4 representable states even when only 3 are legal; the type system happily stores the lie. A closed set of subclasses (or, in languages that have them, a sealed class hierarchy — sealed in Kotlin and modern Java, discriminated unions in TypeScript, abstract sealed patterns in C#) makes exactly the legal states constructible and nothing more. The compiler can then exhaustively check a switch over the kinds and warn you when a new kind is added but not handled — a guarantee no boolean flag will ever give you.
💼 The same refactoring in C#
Here is a billing system where service items hide inside a parts class behind a flag — and the cleaned-up version:
// BEFORE
public class BillItem
{
private readonly decimal _unitPrice;
private readonly int _quantity;
private readonly bool _isService; // type flag
private readonly decimal _hourlyRate; // meaningful only for services
public decimal Total()
=> _isService ? _hourlyRate * _quantity
: _unitPrice * _quantity;
}// AFTER
public class BillItem // the common "parts" case
{
private readonly decimal _unitPrice;
protected int Quantity { get; }
public BillItem(decimal unitPrice, int quantity)
=> (_unitPrice, Quantity) = (unitPrice, quantity);
public virtual decimal Total() => _unitPrice * Quantity;
}
public class ServiceItem : BillItem // the extracted special case
{
private readonly decimal _hourlyRate;
public ServiceItem(decimal hourlyRate, int hours) : base(0m, hours)
=> _hourlyRate = hourlyRate;
public override decimal Total() => _hourlyRate * Quantity;
}C#-specific points: the parent method must be marked virtual and the child override, otherwise the child version silently hides instead of overriding. The factory that decides which class to build is often a static method — BillItem.From(dto) — containing the single surviving switch, and modern C# pattern matching makes it read beautifully: dto switch { { HourlyRate: > 0 } => new ServiceItem(...), _ => new BillItem(...) }.
🐍 The factory in Python
Python has no compiler to confirm the flag's funeral, so the factory becomes the official border post where raw input turns into the right class:
# AFTER: the one surviving kind-check lives in the factory
from datetime import date
class TailorOrder:
def __init__(self, garment: str, list_price: int) -> None:
self.garment = garment
self.list_price = list_price
def total_price(self) -> float:
return self.list_price
def queue_priority(self) -> int:
return 10
class UrgentOrder(TailorOrder):
def __init__(self, garment: str, list_price: int, function_date: date) -> None:
super().__init__(garment, list_price)
self.function_date = function_date # always set, never None
def total_price(self) -> float:
return self.list_price * 1.3
def queue_priority(self) -> int:
return 1
def create_order(garment: str, price: int, function_date: date | None = None) -> TailorOrder:
if function_date is not None:
return UrgentOrder(garment, price, function_date)
return TailorOrder(garment, price)After this, search the project for is_urgent — every hit you find is a leftover box-check that the red book was supposed to retire.
🛠️ IDE support
No mainstream IDE has a literal one-click "Extract Subclass" button, because the move is really a small recipe of supported steps — and the IDEs automate each step:
- IntelliJ IDEA / Rider: create the subclass, then use Refactor → Push Members Down to move the variant fields and methods from the parent into it. The Push Members Down dialog shows checkboxes for each member, exactly mirroring its Pull Members Up twin. Refactor → Replace Constructor with Factory Method automates the creation-site routing from our step 2.
- ReSharper / Rider (C#): Push Members Down plus Change Signature cover the constructor reshuffle; ReSharper also warns if a pushed member is still referenced through the base type.
- VS Code / WebStorm (TypeScript): rely on Rename Symbol and the compiler. TypeScript's
overridekeyword (withnoImplicitOverrideenabled) catches the classic mistake of misspelling an override and silently adding a new method instead.
The honest workflow everywhere: shell subclass by hand, factory by refactoring tool, members down by refactoring tool, flag deleted by hand with the compiler watching.
📊 Benefits and risks
| Aspect | Benefit | Risk / cost |
|---|---|---|
| Conditionals | Flag-checks collapse into polymorphic overrides | One creation-time switch remains in the factory (healthy) |
| Fields | Variant-only fields are always valid in their class | More classes to navigate in the codebase |
| New variants | Add a subclass; existing methods untouched | Over-eager extraction creates thin, pointless subclasses |
| Type safety | Impossible flag combinations cannot be constructed | Subclass per dimension does not scale to two varying dimensions |
| Runtime change | — | An object cannot change class mid-life; changeable kinds need State/Strategy |
| Readability | Each class tells one story | A reader must now look in two files for the full picture |
If you later find a subclass that never grew real differences, the inverse refactoring — Collapse Hierarchy — folds it back into the parent. Extracting and collapsing are both cheap; being stuck with the wrong shape is what is expensive.
College corner: the new subclass must still honour the Liskov Substitution Principle — any code holding a TailorOrder reference must work correctly when handed an UrgentOrder. Our overrides pass this test because they change values (price, priority) within the parent's promised meaning, not the contract itself. An override that, say, made totalPrice() throw for unpaid customers would break LSP and turn your tidy red book into a Refused Bequest factory. Rule of thumb for exams: overrides may be more specific, never more demanding.
🧪 Which smells does it cure?
| Smell | How Extract Subclass helps |
|---|---|
| Temporary Field | Variant-only fields move to a class where they are always filled, never null-by-default |
| Large Class | The overloaded class splits into one focused parent and small focused children |
| Switch Statements | Repeated branching on the same flag dissolves into overrides; one factory switch remains |
| Duplicate Code | Near-identical if/else arms become a shared parent method plus a small override |
| Refused Bequest | Prevented, not cured — extract only when the child truly "is a" parent, or you create this smell |
🧠 The whole idea on one map
📦 Quick revision box
+--------------------------------------------------------------+
| EXTRACT SUBCLASS |
+--------------------------------------------------------------+
| Problem : One class, two jobs. A flag (isUrgent) splits |
| instances; methods if-check it; some fields |
| stay empty for the normal case. |
| Story : Tailor's one fat order book -> separate RED |
| book for urgent orders; red book's own rules |
| replace every YES/NO box check. |
| Fix : 1. Create empty subclass |
| 2. Factory picks the class at creation |
| 3. Push variant fields down |
| 4. Turn each if-branch into an override |
| 5. Delete the flag |
| Cures : Temporary Field, Switch Statements, Large Class |
| Beware : Kind changes at runtime? Use State/Strategy. |
| Tiny variation? A field is cheaper than a class. |
| Inverse : Collapse Hierarchy |
+--------------------------------------------------------------+✍️ Practice exercise
A cinema booking system has this class:
class Ticket {
constructor(
public movie: string,
public basePrice: number,
public isPremium: boolean, // recliner seats, food included
public seatRow?: string, // only premium tickets choose a row
) {}
finalPrice(): number {
if (this.isPremium) return this.basePrice * 2 + 250; // seat + meal
return this.basePrice;
}
entryGate(): string {
return this.isPremium ? "Gate P (lounge)" : "Gate A";
}
includesSnacks(): boolean {
return this.isPremium;
}
}Your tasks:
- List the three symptoms of the disease in this class using the symptom table from this post (flag, branching methods, sometimes-empty field).
- Extract a
PremiumTicketsubclass following the safe steps: empty subclass first, then acreateTicket(...)factory, then pushseatRowdown, then convert the three methods to overrides, then deleteisPremium. Check yourself against the state diagram in Figure 8 — could you compile at every state? - Verify: after your refactoring, can a non-premium ticket ever carry a
seatRow? Can any method still containisPremium? - Bonus thinking question: the cinema plans "morning show" tickets at half price — but a premium ticket can also be a morning show. Why does a
MorningPremiumTicketsubclass start to hurt, and which pattern handles the second dimension better? (Hint: count the classes you would need for 3 seat types × 4 show timings.) - College-level bonus: place "ticket kind" and "show timing" on the quadrant from Figure 4 and justify, in two sentences each, why one is subclass material and the other is not.
If your Ticket class ends up with no flag, no optional field, and no conditionals, the red order book is open — and Masterji would stitch your name into the shop's honour roll.
Frequently asked questions
- How is Extract Subclass different from Extract Superclass?
- They move in opposite directions. Extract Subclass starts with ONE class doing two jobs and splits the special job downward into a new child. Extract Superclass starts with TWO similar classes and lifts their common parts upward into a new parent. Top-down specialisation versus bottom-up generalisation.
- When should I NOT use Extract Subclass?
- When the 'kind' can change during the object's life. An object can never change its own class at runtime — a normal order that suddenly becomes urgent cannot transform from Order to UrgentOrder. For changeable kinds, use State or Strategy objects instead, which can be swapped at any time.
- What happens to the boolean flag after extraction?
- It disappears completely. The information it carried — 'is this urgent?' — is now carried by the object's class itself. An UrgentOrder IS urgent by type. Usually one switch or if remains in a single factory function that decides which class to construct from raw input, and that is healthy.
- Is creating a subclass for one extra field worth it?
- Often not. If the variant differs by a single trivial value and no behaviour changes, a plain field or a small strategy object is cheaper than a whole new class. Extract Subclass earns its keep when the variant has its own fields AND its own behaviour — different calculations, different rules, different validations.
- Can I extract several subclasses from one class?
- Yes, if the type code has several values — for example normal, urgent, and bulk orders could become Order, UrgentOrder, and BulkOrder. But avoid subclassing on two independent dimensions at once, because the combinations multiply. Keep subclasses for one dimension and use composition for the other.
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.
Large Class: The School Bag That Carries Everything
Understand the Large Class code smell — why god classes grow, how to spot low cohesion, and how Extract Class splits them into small, focused classes.
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.
Push Down Method: Move a Method to the Subclass That Really Uses It
Learn the Push Down Method refactoring with a school-office story, honest superclass contracts, safe step-by-step moves in TypeScript and C#, and how it cures the Refused Bequest smell.