Extract Superclass: One Common Rulebook for Twin Classes
Learn the Extract Superclass refactoring with a science-lab/computer-lab story, pull-up moves in TypeScript and C#, the superclass-vs-interface decision table, and how it removes Duplicate Code.
🔬 Two labs, two registers, one headache
Our school in Pune has a science lab, run by Mrs. Joshi, and a computer lab, run by Mr. Kulkarni. They sit in different corridors, holding completely different equipment — test tubes and burners on one side, keyboards and a humming server rack on the other. The two teachers barely meet, except at the tea table in the staff room.
But watch how each lab is managed. Mrs. Joshi keeps a room booking diary — which class uses the lab at which period. She keeps a key register — who took the key, when it came back. She keeps a cleaning rota — which day the lab is deep-cleaned. Now walk to the computer lab. Mr. Kulkarni keeps... a room booking diary. A key register. A cleaning rota. The same three systems, written separately, in two different handwritings, in two different cupboards. Neither teacher copied the other; both simply faced the same job of running a special room, and both invented the same paperwork.
Last month this caused real trouble. The principal announced a new rule in the Monday assembly: "Keys must be returned within one hour, and late returns must be noted in red ink." Mrs. Joshi updated her key register that very day. Mr. Kulkarni was on leave that Monday and never heard about it. Two weeks later, a surprise inspection found the computer lab's register following the old rule, and the school got a written remark. Same school, same rule, two copies — and the copies drifted apart.
The vice-principal, Mrs. Rao, fixes it the sensible way: she writes one common "Lab Rulebook" — booking rules, key rules, cleaning rules — printed once and followed by both labs. Each lab keeps only its own special pages: chemical safety for Mrs. Joshi's lab, software licence checks for Mr. Kulkarni's. The shared rules now live in exactly one place. Change them once, and both labs change together — even when somebody is on leave.
This is Extract Superclass. When two (or more) classes that grew up separately turn out to share the same fields and the same methods, we create a common parent class above them and move the shared implementation up into it.
📖 What is Extract Superclass?
Classes often evolve like our two labs: built by different people at different times, never told about each other, yet solving overlapping problems. Each class works, but together they hide Duplicate Code — the most expensive smell of all, because every shared rule must be fixed in every copy, and one missed copy becomes a bug.
Extract Superclass is the bottom-up repair:
- Create a new (usually
abstract) class — the common rulebook. - Make the existing classes extend it.
- Pull up the shared fields and the identical methods into the parent (the moves Pull Up Field and Pull Up Method).
- For methods that share a shape but differ in one detail, pull up the skeleton and leave the differing step
abstractfor children to fill in (Form Template Method). - Whatever is genuinely unique stays in each child.
The crucial word is implementation. The new superclass holds real code: actual fields, actual method bodies, even constructor logic. Children inherit working machinery, not just a list of promises. That is exactly what makes Extract Superclass the right cure for duplication — and exactly what separates it from its lighter cousin, Extract Interface, which shares no code at all.
Fowler calls this discovering the abstraction from below: you do not design Lab on day one; you notice, years later, that ScienceLab and ComputerLab were always two versions of one idea, and you let the type system finally record it.
Here is the rule-change problem as a conversation. Before the rulebook, the principal must personally reach every lab — and one miss means drift. After, the rulebook is the single line of command:
One-line summary: Extract Superclass collects the duplicated fields and method bodies of sibling classes into one new parent, so shared rules live in exactly one place and the compiler knows the classes are the same kind of thing.
Before pulling anything up, walk this small decision path — it stops the two classic mistakes (faking a family, and using an interface where real code is duplicated):
🔔 When do we need it?
Look for these signs:
- Twin code in unrelated classes. Two classes declare the same fields (
bookings,keyHolder,cleaningDay) and near-identical methods. Classic Duplicate Code across siblings with no parent. - Rules that must change together. When one business rule ("late keys in red ink") forces edits in two classes, the classes are secretly one concept.
- Parallel growth. Every feature added to
ScienceLabsoon needs adding toComputerLab. You are maintaining the same class twice. - Both classes are getting fat. Each one carries its own copy of the shared machinery plus its special logic — a slow march toward Large Class in stereo.
Mrs. Rao actually measured the two paper registers before printing the rulebook, and the software version of her count is worth doing too — what fraction of each class is a copy of the other?
When the shared slice is the majority, the parent is not a nice-to-have — it is the real class, hiding in two costumes.
And the cautions:
- Spend the inheritance slot wisely. In C#, Java, and TypeScript, a class extends only one base class. If the classes share only a role and no real code, an interface costs nothing — see the comparison table below.
- Do not force a false "is-a". A
ScienceLabis aLab— true. But if two classes merely happen to share one utility method, gluing them under an invented parent asserts a relationship that does not exist. Prefer composition (Extract Class) for shared helpers. - Pull up only what is truly common. If you hoist a method that only one child uses, you have created the Refused Bequest smell, and you will be back next week applying Push Down Method to undo it.
The choice between the two big sharing tools sits on this quadrant — keep it pinned above your desk:
The labs case sits deep in the top-right: real duplicated code, and the classes genuinely are the same kind of thing. Bottom-left — unrelated classes sharing only a role — is Extract Interface territory (the electrician and plumber live there). The bottom-right corner is the trap: real shared code between classes that are not family. Do not invent a fake parent for them; move the shared code into a helper class both can contain (composition).
College corner — slot economics: why all this care? Because in C#, Java, and TypeScript the extends clause is a scarce resource: exactly one per class, forever. Interfaces are unlimited (class ComputerLab : Lab, IBookable, IAuditable is fine in C#), so promising costs nothing, but inheriting implementation is a monopoly. Spending the slot on Lab means ComputerLab can never also extend, say, NetworkedRoom. Languages chose single inheritance deliberately — multiple implementation inheritance creates the famous diamond problem, where one class inherits two competing copies of the same field. C++ allows it and pays in complexity; Java, C#, and TypeScript banned it and pay in slot scarcity. Your job as a designer is simply to make sure the one slot buys real, lasting shared machinery.
🔍 Before and after at a glance
The two labs in TypeScript, duplication and all:
// BEFORE: twin code, no shared parent
class ScienceLab {
private bookings = new Map<string, string>(); // period -> class
private keyHolder: string | null = null;
bookSlot(period: string, className: string): void {
if (this.bookings.has(period)) throw new Error("Slot already booked");
this.bookings.set(period, className);
}
issueKey(teacher: string): void { this.keyHolder = teacher; }
returnKey(): void { this.keyHolder = null; }
runSafetyDrill(): string { return "Chemical spill drill done"; } // unique
}
class ComputerLab {
private bookings = new Map<string, string>(); // period -> class (copy!)
private keyHolder: string | null = null; // (copy!)
bookSlot(period: string, className: string): void { // (copy!)
if (this.bookings.has(period)) throw new Error("Slot already booked");
this.bookings.set(period, className);
}
issueKey(teacher: string): void { this.keyHolder = teacher; } // (copy!)
returnKey(): void { this.keyHolder = null; } // (copy!)
auditLicences(): string { return "All software licences valid"; } // unique
}After Extract Superclass — one rulebook, two thin labs:
// AFTER: shared implementation lives once, in Lab
abstract class Lab {
private bookings = new Map<string, string>();
private keyHolder: string | null = null;
bookSlot(period: string, className: string): void {
if (this.bookings.has(period)) throw new Error("Slot already booked");
this.bookings.set(period, className);
}
issueKey(teacher: string): void { this.keyHolder = teacher; }
returnKey(): void { this.keyHolder = null; }
}
class ScienceLab extends Lab {
runSafetyDrill(): string { return "Chemical spill drill done"; }
}
class ComputerLab extends Lab {
auditLicences(): string { return "All software licences valid"; }
}The principal's red-ink rule is now a one-line, one-place change inside Lab.returnKey() — and both labs obey instantly, forever. Count the maintenance cost of every future shared rule:
Two versus one looks like a small saving until you remember the failure mode: with two places, the real cost is not the second edit — it is the forgotten second edit, the inspection remark, the bug report. With one place, forgetting is impossible.
🪜 Step-by-step, the safe way
Move one member at a time, with the compiler and tests as your safety rope. The journey has clean, compile-green stations:
- Create the empty parent and connect the children. An empty
abstract class Lab {}plusextends Labon both classes. Nothing else changes; everything still compiles.
// INTERMEDIATE STEP: parent exists but is still empty
abstract class Lab {}
class ScienceLab extends Lab { /* all original code still here */ }
class ComputerLab extends Lab { /* all original code still here */ }-
Pull up fields first. Methods you want to lift will need the data they touch. Move
bookingsandkeyHolderintoLab(asprivate, orprotectedif children genuinely need direct access). Delete the copies from both children. Compile. -
Pull up identical methods.
bookSlot,issueKey,returnKeyare character-for-character the same — move one method up, delete both child copies, run tests. Repeat per method. If the two copies have tiny accidental differences (one logs, one does not), stop and decide the correct single behaviour first — pulling up is also a chance to heal drift. -
Template the similar-but-different methods. Suppose both labs have a
closeForDay()that locks up, but each does one special check first. Pull up the skeleton; leave the differing step abstract:
abstract class Lab {
closeForDay(): string {
const note = this.specialClosingCheck(); // child fills this step
this.returnKey();
return `Lab closed. ${note}`;
}
protected abstract specialClosingCheck(): string;
}-
Pull up constructor common parts. If both children set the same fields in their constructors, move that assignment into a parent constructor and call
super(...)from each child. -
Review for over-lifting. Walk through the parent. Does every member make sense for every child? If something only one lab uses sneaked up, push it back down now, before anyone starts depending on it.
Pull up fields before the methods that use them, and compile after every single move. If you lift a method first, it will reference child fields the parent cannot see, and you will face a wall of errors instead of one small fixable step. Slow is smooth; smooth is fast.
🧮 A bigger real-life example
A reporting module has two exporters that grew separately — PDF and CSV. Both build a file name with a timestamp, ensure the output folder exists, and log the export. Only the body-writing differs:
// AFTER extraction: shared machinery up, format-specific work down
abstract class ReportExporter {
constructor(protected outputFolder: string) {}
export(reportName: string, rows: string[][]): string {
const fileName = this.buildFileName(reportName);
this.ensureFolderExists();
const content = this.renderBody(rows); // the abstract step
this.writeFile(fileName, content);
console.log(`Exported ${fileName} (${rows.length} rows)`);
return fileName;
}
private buildFileName(reportName: string): string {
const stamp = new Date().toISOString().slice(0, 10);
return `${this.outputFolder}/${reportName}-${stamp}.${this.fileExtension()}`;
}
private ensureFolderExists(): void { /* mkdir -p logic */ }
private writeFile(name: string, content: string): void { /* fs write */ }
protected abstract renderBody(rows: string[][]): string;
protected abstract fileExtension(): string;
}
class CsvExporter extends ReportExporter {
protected renderBody(rows: string[][]): string {
return rows.map(r => r.join(",")).join("\n");
}
protected fileExtension(): string { return "csv"; }
}
class PdfExporter extends ReportExporter {
protected renderBody(rows: string[][]): string {
return `%PDF... ${rows.length} rows rendered ...`;
}
protected fileExtension(): string { return "pdf"; }
}Before the refactoring, the file-naming, folder-checking, and logging code existed twice and had already drifted (one exporter used toISOString, the other a hand-rolled date string — so PDF and CSV files of the same report sorted differently!). After it, the whole export pipeline is written once; each exporter contributes only its two honest differences. Adding an ExcelExporter tomorrow means writing exactly two small methods.
College corner — Template Method and the fragile base: the export() skeleton above is the Template Method pattern from the Gang of Four catalog, and Extract Superclass is how it usually appears in the wild — discovered, not designed. One warning your textbook may underplay: a superclass couples children to its implementation, not just its interface. If Lab.bookSlot() later starts calling another overridable method internally, a child's innocent override can change bookSlot's behaviour in ways the child never intended — the fragile base class problem. Defences: keep parent internals private, keep the overridable surface small and explicitly named (like specialClosingCheck), and in C# mark methods non-virtual by default (which the language already does — unlike Java, where every method is virtual unless final).
💼 The same refactoring in C#
The lab example in C#, showing the language details — abstract, protected, and base(...) constructor chaining:
public abstract class Lab
{
private readonly Dictionary<string, string> _bookings = new();
private string? _keyHolder;
protected string RoomNumber { get; }
protected Lab(string roomNumber) => RoomNumber = roomNumber;
public void BookSlot(string period, string className)
{
if (_bookings.ContainsKey(period))
throw new InvalidOperationException("Slot already booked");
_bookings[period] = className;
}
public void IssueKey(string teacher) => _keyHolder = teacher;
public void ReturnKey() => _keyHolder = null;
public abstract string DailyChecklist(); // shape shared, detail differs
}
public class ScienceLab : Lab
{
public ScienceLab(string roomNumber) : base(roomNumber) { }
public override string DailyChecklist() => "Check gas taps and acid cupboard";
}
public class ComputerLab : Lab
{
public ComputerLab(string roomNumber) : base(roomNumber) { }
public override string DailyChecklist() => "Check UPS and update antivirus";
}Remember the C# rule that shapes this whole refactoring: a class may have one base class but many interfaces (class ComputerLab : Lab, IBookable, IAuditable). Extracting Lab spends the only base-class slot ScienceLab and ComputerLab will ever have — worth it here, because real code moved up. Java behaves identically (extends one, implements many), and TypeScript too.
🐍 The same idea in Python
Python allows multiple inheritance, but the disciplined version of this refactoring looks the same — one abstract base carrying the shared machinery, marked with the abc module so nobody constructs a bare Lab:
from abc import ABC, abstractmethod
class Lab(ABC):
def __init__(self, room_number: str) -> None:
self.room_number = room_number
self._bookings: dict[str, str] = {}
self._key_holder: str | None = None
def book_slot(self, period: str, class_name: str) -> None:
if period in self._bookings:
raise ValueError("Slot already booked")
self._bookings[period] = class_name
def issue_key(self, teacher: str) -> None:
self._key_holder = teacher
def return_key(self) -> None:
self._key_holder = None
@abstractmethod
def daily_checklist(self) -> str: ...
class ScienceLab(Lab):
def daily_checklist(self) -> str:
return "Check gas taps and acid cupboard"
class ComputerLab(Lab):
def daily_checklist(self) -> str:
return "Check UPS and update antivirus"The @abstractmethod decorator gives Python a taste of what C# and Java enforce natively: instantiating Lab() directly raises a TypeError, and a child that forgets daily_checklist cannot be constructed either.
College corner — abstract class vs interface, precisely: an abstract class (C# abstract class, Java abstract class, TS abstract class) may contain fields, constructors, full method bodies, and abstract members — and consumes the single inheritance slot. An interface (C# interface, Java interface, TS interface) traditionally contains only signatures — zero state, zero slot cost, unlimited per class. Modern complications worth knowing for interviews: Java 8+ allows default method bodies in interfaces and C# 8+ allows default interface members, but neither allows instance fields — so shared state still forces an abstract class. Quick test: "Do I need a field up there?" Yes → abstract class. "Only behaviour promises?" → interface. "Bodies but no fields, many parents needed?" → default interface methods, used sparingly.
🛠️ IDE support
This refactoring has first-class automation:
- IntelliJ IDEA (Java/Kotlin): Refactor → Extract → Superclass... opens the Extract Superclass dialog. You type the new parent's name and package, then tick checkboxes for which members to move up; an extra checkbox per method lets you pull it up as
abstractinstead of moving the body. The IDE rewiresextendsclauses and offers to use the new superclass where possible. - JetBrains Rider / ReSharper (C#): Refactor This → Extract Superclass gives the same member-selection dialog, with options to make members abstract in the parent and to update usages from child type to parent type.
- IntelliJ-family for TypeScript (WebStorm): Refactor → Extract Superclass works on TypeScript classes too.
- Visual Studio (C#): Extract base class... appears in the Quick Actions (Ctrl+.) menu on a class name — choose members and whether each becomes abstract.
A practical tip with all these dialogs: extract with the fields plus the identical methods ticked, and leave the similar-but-different methods for a careful manual template-method pass afterwards. Tools are great at moving code; only you can decide which differences are accidental drift and which are real.
⚖️ Benefits and risks
First, the headline decision every developer must learn — and the one exams love to ask. A superclass shares code; an interface shares a contract:
| Question | Extract Superclass | Extract Interface |
|---|---|---|
| What is shared? | Actual code — fields, method bodies, constructors (implementation) | Only a promise — method signatures, no bodies (contract) |
| Removes duplication? | Yes — duplicated bodies move up and exist once | No — each class still writes its own bodies |
| How many per class (C#/Java/TS)? | One base class only | Many interfaces freely |
| Relationship asserted | "is-a" — children are the same kind of thing | "can-do" — types merely play the same role |
| Can it hold state (fields)? | Yes — including private machinery | No — not even with default methods |
| Best when | Sibling classes duplicate how they work | Unrelated classes share what they offer; you need swapping or test doubles |
| Story version | One Lab Rulebook both labs follow | One sign-in form any visitor can fill |
| Cost | Spends the single inheritance slot; couples children to parent's code | Almost free structurally; but shares zero code |
They also combine beautifully: extract a superclass for the shared machinery and an interface for the contract callers depend on — class ScienceLab extends Lab implements IBookableRoom. The rulebook and the form are teammates, not rivals.
And the general trade-offs of this refactoring:
| Aspect | Benefit | Risk / cost |
|---|---|---|
| Duplication | Shared rules live once; drift becomes impossible | Accidental similarity wrongly fused looks like a real concept |
| Type system | Records "these are the same kind"; enables polymorphism (Lab[]) | A false is-a is hard to undo once callers rely on it |
| Future features | New shared behaviour has an obvious home | The parent can swell into a god class if everything lands there |
| New variants | New child reuses all the machinery for free | Children are coupled to the parent's implementation details |
| Inheritance budget | — | The one extends slot is now spent |
🧪 Which smells does it cure?
| Smell | How Extract Superclass helps |
|---|---|
| Duplicate Code | The primary cure for duplication spread across sibling classes — copies merge into one parent body |
| Large Class | Each child sheds its copy of the shared machinery and keeps only its specialty |
| Alternative Classes with Different Interfaces | Pulling members up forces the siblings to agree on one set of names and signatures |
| Refused Bequest | A risk to avoid, not a cure — pull up only universal members, or you manufacture this smell |
🧠 The whole idea on one map
📦 Quick revision box
+--------------------------------------------------------------+
| EXTRACT SUPERCLASS |
+--------------------------------------------------------------+
| Problem : Two sibling classes carry the SAME fields and |
| methods, written twice, drifting apart. |
| Story : Science lab & computer lab each kept their own |
| booking diary, key register, cleaning rota -> |
| one common "Lab Rulebook" both follow. |
| Fix : 1. Empty abstract parent; children extend it |
| 2. Pull Up Field (data first!) |
| 3. Pull Up Method (identical bodies) |
| 4. Form Template Method (similar bodies) |
| 5. Push back anything only one child uses |
| Shares : IMPLEMENTATION (real code) - vs interface, |
| which shares only a CONTRACT (promise). |
| Budget : One base class per class (C#/Java/TS); |
| interfaces are unlimited. |
| Cures : Duplicate Code, Large Class |
| Inverse : Collapse Hierarchy |
+--------------------------------------------------------------+✍️ Practice exercise
A school's transport office has these two classes:
class SchoolBus {
constructor(public plateNumber: string, public driverName: string) {}
private trips: string[] = [];
logTrip(route: string): void { this.trips.push(`${new Date().toISOString()}: ${route}`); }
tripCount(): number { return this.trips.length; }
fitnessCertificateDue(): string { return "Check RTO fitness certificate yearly"; }
studentCapacity(): number { return 40; }
}
class StaffVan {
constructor(public plateNumber: string, public driverName: string) {}
private trips: string[] = [];
logTrip(route: string): void { this.trips.push(`${new Date().toISOString()}: ${route}`); }
tripCount(): number { return this.trips.length; }
fitnessCertificateDue(): string { return "Check RTO fitness certificate yearly"; }
acRepairContact(): string { return "Cool Breeze Services, Pune"; }
}Your tasks:
- Underline every duplicated member across the two classes (you should find one field, one constructor pattern, and three methods). Draw your own version of the pie in Figure 4 — what fraction of each class is a copy?
- Extract a
SchoolVehiclesuperclass the safe way: empty parent first, then fields, then identical methods, compiling after each move. Check yourself against the state diagram in Figure 8. - Decide: should
SchoolVehiclebe abstract? ShouldstudentCapacity()move up? (Careful — the van does not carry students; pulling it up would create which smell?) - Bonus: the school's booking software wants to treat any bookable resource — vehicles, labs, the auditorium — uniformly with
book(slot). Real code is NOT shared between a van and an auditorium. Which refactoring names that common role, and why is it not Extract Superclass? Place the case on the quadrant in Figure 5 before answering. - College-level bonus: suppose next year someone wants
SchoolBusto also inherit from a newGpsTrackedVehiclebase class that holds tracking state. Explain, in terms of the single-inheritance slot, why this is now a design problem — and name two ways out (hint: one uses composition, one uses an interface plus a shared helper).
If your finished parent contains only members that both vehicles genuinely use, and each child keeps exactly one specialty method, your rulebook is printed correctly — and Mrs. Rao would pass your inspection without a single red-ink remark.
Frequently asked questions
- What is the difference between Extract Superclass and Extract Interface?
- Extract Superclass shares actual CODE — real fields and method bodies that children inherit and reuse. Extract Interface shares only a PROMISE — method signatures with no bodies, which each class must implement itself. If two classes duplicate how they do something, extract a superclass; if they only share what they can do, extract an interface.
- Why is single inheritance called a 'budget' here?
- In C#, Java, and TypeScript a class can extend only ONE base class, but can implement MANY interfaces. Choosing a superclass spends that one slot forever. So before extracting a superclass, ask whether the relationship is truly 'is-a' and whether the shared code is worth the slot.
- How is Extract Superclass different from Extract Subclass?
- Opposite directions. Extract Superclass is bottom-up: two existing sibling classes give their common parts to a NEW parent above them. Extract Subclass is top-down: one overloaded class pushes its special-case parts into a NEW child below it. Generalise upward versus specialise downward.
- What if two methods look similar but are not identical?
- Use Form Template Method. Put the shared skeleton in the superclass as a concrete method that calls small abstract steps, and let each subclass override just the steps that differ. Identical methods move up whole; similar methods move up as a template; totally different methods stay down.
- Should the extracted superclass be abstract?
- Usually yes. The new parent often represents a concept like 'Lab' or 'Exporter' that nobody should construct directly — only its concrete children make sense as real objects. Marking it abstract documents that and lets you declare abstract methods the children must fill in.
Further reading
Related Lessons
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.
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.
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.
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.