Extract Interface: One Common Form, Many Different Workers
Learn the Extract Interface refactoring with a visitor-register story about an electrician and a plumber, contract-extraction in TypeScript and C#, test doubles, and the interface-vs-superclass decision table.
🚪 The electrician, the plumber, and one register at the gate
An office building in Chennai has a security desk at the gate, manned by Murugan, a guard who has seen every kind of visitor in his fifteen years on the chair. Today two workers arrive. First comes Ravi the electrician — toolbox full of wires, testers, and tape, here to fix the third-floor distribution board. Later comes Selvam the plumber — wrenches, washers, and pipe cutters, called for a leaking pantry tap.
Think about how different these two men are. Different trades, different training, different companies, different tools. Nobody would say a plumber is a kind of electrician, or that they come from some common "worker family." They share no boss, no rulebook, no skills. If you tried to write one training manual covering both their jobs, you would produce nonsense.
And yet, at the security desk, Murugan treats them exactly the same way. He opens one register and says the same words to both: "Name? Purpose? Sign in. And sign out when you leave." The register does not care about wires or pipes. It cares about exactly two abilities: can sign in, can sign out. Anyone with those two abilities — electrician, plumber, courier boy, school inspector — fits the register.
Notice what the register really is. It is not a family the workers belong to. It is a form — a short list of things each visitor must be able to do. Ravi fills it his way ("Ravi, electrical repair, 9:40"), Selvam his way ("Selvam, plumbing, 11:15"). The form fixes what must happen, never how.
Programming has this exact idea, and it is called an interface: a contract listing abilities, with no code inside. And the refactoring that discovers such a contract hiding in existing classes is Extract Interface — take the slice of methods that several clients or several classes share, and give that slice a name of its own.
📋 What is Extract Interface?
An interface is a type that declares method signatures only — names, parameters, return types — and not one line of implementation. A class that implements the interface promises the compiler: "I provide a real body for every method on this list."
Extract Interface is the refactoring where, instead of designing the contract up front, you discover it in code that already exists:
- Look at a class (or several classes) and find the cohesive slice of methods that callers actually use — the role being played.
- Declare an interface containing just those signatures.
- Mark the class as implementing it. The compiler checks the match; usually no method changes at all.
- Change clients to depend on the interface type instead of the concrete class.
Two classic situations call for it. First, unrelated classes share a role: Electrician and Plumber both have signIn() and signOut(), and the gate register wants to handle either without caring which. Second, clients use only a slice of a big class: a service calls only save() and load() on a store that also has ten maintenance methods — extracting ReportStore with just those two narrows the dependency.
The deep difference from Extract Superclass: a superclass shares actual code — fields and method bodies the children inherit. An interface shares only a promise. Nothing is reused; everything is guaranteed. That is also why the language rules differ: in C#, Java, and TypeScript a class may extend one base class but implement many interfaces — promises stack freely, implementations do not.
Here is the choosing logic Murugan's situation walks through:
Ravi and Selvam share zero method bodies — an electrician signs in with his licence number, a plumber with his contractor card. Only the role is common. That path leads straight to the interface.
One-line summary: Extract Interface names the role several types play — a contract of signatures with no code — so callers can depend on the role and any worker who fills the form can be used, swapped, or faked.
🔔 When do we need it?
Watch for these moments:
- Unrelated classes, same operations.
ElectricianandPlumbercannot share a parent — they are not the same kind of thing, and forcing a common superclass would hand them inherited members they would refuse, breeding the Refused Bequest smell. An interface gives them one contract with zero forced family ties. - You cannot test without the real thing. If
ReportServiceholds a concreteFileReportStore, every unit test touches the disk. ExtractIReportStore, and tests can pass in an in-memory fake. - Callers use only a slice. When clients of a Large Class each call just two or three of its thirty methods, extracting per-role interfaces documents and narrows each dependency.
- You need to swap implementations. File today, database tomorrow, cloud next year. Clients written against the interface never change.
- Type-checking ladders. Code like
if (worker instanceof Electrician) ... else if (worker instanceof Plumber) ...repeated at every call site is duplication of decision-making — a cousin of Duplicate Code. One interface plus polymorphic calls deletes the ladder.
How varied is the traffic at Murugan's gate in a normal month? Very — and the register handles all of it with one form:
No two slices of that pie belong to the same "family", and the register never needed them to. That is the quiet power of a contract: it scales across unrelated types, which is exactly where inheritance cannot follow.
And the cautions: an interface removes no duplicated bodies (that is superclass work); a one-implementer interface with no test or decoupling need is Speculative Generality; and an interface that mirrors the entire class is just a rename, not a role.
The decision sits on the same quadrant we use in the Extract Superclass post — but look where the gate case lands this time:
College corner — two SOLID letters at once: Extract Interface is the working end of two principles. The Interface Segregation Principle (ISP) says clients should never be forced to depend on methods they do not call — which is why we extract the slice, not the whole class. The Dependency Inversion Principle (DIP) says high-level policy (the register) should not depend on low-level detail (the electrician's toolbox); both should depend on an abstraction (the visitor form). When your architecture textbook draws an arrow flipping direction across a module boundary, Extract Interface is the refactoring that physically performs the flip.
🔍 Before and after at a glance
The security desk in TypeScript. Before — the register knows every trade personally:
// BEFORE: the register depends on each concrete worker type
class Electrician {
constructor(public name: string) {}
signIn(time: string): string { return `${this.name} (electrician) in at ${time}`; }
signOut(time: string): string { return `${this.name} out at ${time}`; }
testCircuit(): string { return "Circuit tested"; } // trade-specific
}
class Plumber {
constructor(public name: string) {}
signIn(time: string): string { return `${this.name} (plumber) in at ${time}`; }
signOut(time: string): string { return `${this.name} out at ${time}`; }
fixLeak(): string { return "Leak fixed"; } // trade-specific
}
class GateRegister {
logEntry(worker: Electrician | Plumber, time: string): void {
console.log(worker.signIn(time)); // a union that grows with every new trade!
}
}After Extract Interface — the register depends only on the form:
// AFTER: one contract; the register never learns trades
interface Visitor {
signIn(time: string): string;
signOut(time: string): string;
}
class Electrician implements Visitor {
constructor(public name: string) {}
signIn(time: string): string { return `${this.name} (electrician) in at ${time}`; }
signOut(time: string): string { return `${this.name} out at ${time}`; }
testCircuit(): string { return "Circuit tested"; }
}
class Plumber implements Visitor {
constructor(public name: string) {}
signIn(time: string): string { return `${this.name} (plumber) in at ${time}`; }
signOut(time: string): string { return `${this.name} out at ${time}`; }
fixLeak(): string { return "Leak fixed"; }
}
class GateRegister {
logEntry(visitor: Visitor, time: string): void {
console.log(visitor.signIn(time)); // works for any future trade, unchanged
}
}Note the arrow style in the diagram: the dashed arrow (<|..) means "implements the contract", while a solid arrow would mean "inherits the code". The electrician and plumber remain strangers to each other — only the form connects them. When a CourierBoy class arrives next month, it implements Visitor and GateRegister does not change by even one character.
That last sentence is the measurable win. Count the lines of GateRegister you must touch each time the building gets a new kind of visitor:
With the union type, every new trade meant editing the union, often an instanceof ladder, and every test that mentioned the old union — about six lines on average in the real codebase this example is based on. With the interface: zero. This is the Open-Closed Principle showing up in a guard's register: open for new visitors, closed for modification.
🪜 Step-by-step, the safe way
The refactoring moves through compile-green states, and the early steps are astonishingly low-risk:
-
Find the role. List the methods callers actually invoke. For the register it is
signInandsignOut— nottestCircuit, notfixLeak. The role is the intersection of what clients need, not the union of what the class offers. -
Declare the interface. Signatures only. Choose a role-name (
Visitor,ReportStore,Notifier), not an implementation name. C# convention prefixesI(IVisitor); TypeScript and Java usually do not. -
Make the class implement it. Add
implements Visitor. The compiler verifies every signature matches. The class body does not change — this is the safest step in all of refactoring.
// INTERMEDIATE STEP: contract declared, clients not yet migrated
interface Visitor {
signIn(time: string): string;
signOut(time: string): string;
}
class Electrician implements Visitor { /* unchanged body */ }
class GateRegister {
logEntry(worker: Electrician | Plumber, time: string): void { /* still the old union */ }
}-
Migrate clients one declaration at a time. Change each parameter, field, and return type from the concrete class to the interface. Compile and test after each site. Every migration is independently safe.
-
Add the second implementer. Have
Plumber(and any future class) implement the interface too, then delete the union types andinstanceofladders that the interface replaces. -
Resist fattening the contract. When someone later asks, "shouldn't
Visitoralso havetoolboxWeight()?", check: do register clients need it? If only one trade has it, it stays out of the form.
Extract the slice, not the class. If you tick every public method into the interface, you have created a fat contract that future implementers must fully satisfy — a fake in a test would need thirty dummy methods. Keep the interface as small as the role really is; you can always extract a second interface for a second role.
🧮 A bigger real-life example
The school's notice-board software stores notices in files. The service class is welded to the disk, so every test writes real files:
// BEFORE: welded to the file system
class FileNoticeStore {
save(notice: { id: number; text: string }): void { /* write to disk */ }
load(id: number): string { /* read from disk */ return "..."; }
compactFolder(): void { /* disk-only maintenance */ }
}
class NoticeService {
private store = new FileNoticeStore(); // cannot swap, cannot fake
publish(id: number, text: string): void {
this.store.save({ id, text: text.trim() });
}
}NoticeService uses only save and load — never compactFolder. Extract exactly that slice:
// AFTER: the service depends on the role, not the disk
interface NoticeStore {
save(notice: { id: number; text: string }): void;
load(id: number): string;
}
class FileNoticeStore implements NoticeStore {
save(notice: { id: number; text: string }): void { /* write to disk */ }
load(id: number): string { return "..."; }
compactFolder(): void { /* still exists — just not part of the contract */ }
}
class NoticeService {
constructor(private store: NoticeStore) {} // injected; any implementer fits
publish(id: number, text: string): void {
this.store.save({ id, text: text.trim() });
}
}
// In tests: a fake implementer — the second "worker" signing the same form
class InMemoryNoticeStore implements NoticeStore {
private notices = new Map<number, string>();
save(n: { id: number; text: string }): void { this.notices.set(n.id, n.text); }
load(id: number): string { return this.notices.get(id) ?? ""; }
}Now a unit test constructs new NoticeService(new InMemoryNoticeStore()) and runs in microseconds with no disk at all. Next year, a CloudNoticeStore implements the same contract, and NoticeService ships unchanged. This pairing — extract interface, then inject the dependency — is the everyday backbone of testable design.
And here is Murugan's working life, told as a journey — because the register story and the notice-store story are the same story:
💼 The same refactoring in C#
The gate register in C#, with the I-prefix convention and a class that signs two forms at once:
public interface IVisitor
{
string SignIn(TimeOnly time);
string SignOut(TimeOnly time);
}
public interface ISafetyTrained // a second, separate role
{
string ShowSafetyCard();
}
public class Electrician : IVisitor, ISafetyTrained // many interfaces — fine!
{
public string Name { get; }
public Electrician(string name) => Name = name;
public string SignIn(TimeOnly time) => $"{Name} (electrician) in at {time}";
public string SignOut(TimeOnly time) => $"{Name} out at {time}";
public string ShowSafetyCard() => "High-voltage safety card, valid 2027";
public string TestCircuit() => "Circuit tested";
}
public class Plumber : IVisitor
{
public string Name { get; }
public Plumber(string name) => Name = name;
public string SignIn(TimeOnly time) => $"{Name} (plumber) in at {time}";
public string SignOut(TimeOnly time) => $"{Name} out at {time}";
}
public class GateRegister
{
public void LogEntry(IVisitor visitor, TimeOnly time)
=> Console.WriteLine(visitor.SignIn(time));
}Electrician implements two interfaces while still being free to extend a base class someday — class Electrician : Contractor, IVisitor, ISafetyTrained is legal, with the single base class listed first. That is the structural cheapness of interfaces: each new role costs nothing from the inheritance budget.
College corner — default methods and what they cannot do: modern C# (8+) allows default interface members and Java (8+) allows default methods — method bodies living inside an interface. Does this blur the line with abstract classes? Less than it seems. Neither language allows instance fields in an interface, so an interface still cannot hold state like Lab's booking map — shared state always needs an abstract class. Default methods exist mainly for interface evolution: adding a method to a published interface without breaking the hundred classes already implementing it. Use them for that, not as a sneaky multiple-inheritance trick — examiners and code reviewers both notice.
🐍 The same idea in Python — Protocols
Python's twist is delightful: with typing.Protocol, classes do not even have to declare that they implement the contract. If the methods match, the type checker accepts them — structural typing, also called duck typing with a seatbelt:
from typing import Protocol
class Visitor(Protocol):
def sign_in(self, time: str) -> str: ...
def sign_out(self, time: str) -> str: ...
class Electrician: # note: no explicit "implements"!
def __init__(self, name: str) -> None:
self.name = name
def sign_in(self, time: str) -> str:
return f"{self.name} (electrician) in at {time}"
def sign_out(self, time: str) -> str:
return f"{self.name} out at {time}"
class GateRegister:
def log_entry(self, visitor: Visitor, time: str) -> None:
print(visitor.sign_in(time)) # mypy verifies the shape matchesTypeScript secretly works the same way — its type system is structural, so a class with matching signIn/signOut methods is assignable to Visitor even without the implements clause (the clause is still good manners: it makes the intent visible and turns a signature drift into an error at the class, not at the call site). C# and Java are nominal: the class must explicitly name the interface. Knowing which camp your language is in is a favourite interview question.
🛠️ IDE support
Extract Interface is one of the best-automated refactorings anywhere:
- Visual Studio (C#): put the cursor on the class name, press Ctrl+. and choose Extract interface... (also under Edit → Refactor). The dialog asks for the interface name, the destination file, and shows checkboxes for which public members to include; on OK it creates the interface and adds it to the class's base list automatically.
- ReSharper / JetBrains Rider (C#): Refactor This → Extract Interface opens a dialog to pick members; if the class already implements other interfaces, those can be folded into the new one too, and Rider can update usages of the class to the new interface where it is safe.
- IntelliJ IDEA (Java) / WebStorm (TypeScript): Refactor → Extract → Interface... lets you tick members and offers "Replace class references with interface where possible" — the tool performs our step 4 across the whole project in one shot.
One thing no tool can decide for you: which members form the role. The dialogs happily let you tick everything; a thoughtful human ticks the small cohesive slice that clients truly use.
⚖️ Benefits and risks
The decision table every student should memorise — what a superclass shares versus what an interface shares:
| Question | Extract Interface | Extract Superclass |
|---|---|---|
| What is shared? | Only a promise — signatures, no bodies (contract) | Actual code — fields, method bodies, constructors (implementation) |
| Removes duplicated bodies? | No — every implementer writes its own | Yes — one copy moves into the parent |
| How many per class (C#/Java/TS)? | Many interfaces freely | One base class only |
| Relationship asserted | "can-do" — types play the same role | "is-a" — children are the same kind |
| Works for unrelated classes? | Yes — electrician and plumber stay strangers | No — forces a family that may not exist |
| Can it hold state (fields)? | No — not even with default methods | Yes — including private machinery |
| Story version | One sign-in form any visitor can fill | One Lab Rulebook both labs follow |
| Best when | Swapping, mocking, decoupling, multiple roles | Siblings genuinely duplicate how they work |
| Cost | Zero structural cost; zero code reuse | Spends the single inheritance slot |
And the overall trade-offs of extracting an interface:
| Aspect | Benefit | Risk / cost |
|---|---|---|
| Coupling | Clients depend on a role, not a concrete class | One more type to name, file, and maintain |
| Testing | Fakes and mocks implement the contract trivially | A fat interface makes every fake painful |
| Substitution | File / memory / cloud implementations swap freely | Single-implementer "just in case" interfaces are clutter |
| Duplication | Deletes instanceof/union ladders at call sites | Does not remove duplicated method bodies |
| Inheritance budget | Costs nothing; stacks with a base class | — |
Remember the composition trick from the Extract Superclass post: the two refactorings are teammates, not rivals. Shared machinery goes into a base class; the slim contract clients see goes into an interface; a class can have both — class ScienceLab extends Lab implements IBookableRoom.
🧪 Which smells does it cure?
| Smell | How Extract Interface helps |
|---|---|
| Alternative Classes with Different Interfaces | Gives look-alike classes one agreed contract, so callers stop special-casing each |
| Duplicate Code | Removes repeated type-checking ladders at call sites (not duplicated bodies — that needs a superclass) |
| Large Class | Per-role interfaces carve a big class's surface into small documented slices clients can depend on |
| Refused Bequest | Prevents it — unrelated types share a role without inheriting members they would have to refuse |
🧠 The whole idea on one map
📦 Quick revision box
+--------------------------------------------------------------+
| EXTRACT INTERFACE |
+--------------------------------------------------------------+
| Problem : Unrelated classes share a ROLE (same operations), |
| or clients use only a slice of a big class, |
| or you cannot swap/fake a dependency. |
| Story : Electrician & plumber - totally different |
| workers, but the gate register needs only |
| "sign in, sign out" -> one common FORM |
| (contract), NOT a common family. |
| Fix : 1. Find the cohesive slice clients really use |
| 2. Declare interface (signatures only) |
| 3. Class implements it (body unchanged) |
| 4. Migrate clients to the interface type |
| 5. Second implementer signs the same form |
| Shares : CONTRACT (promise only) - vs superclass, |
| which shares IMPLEMENTATION (real code). |
| Budget : Many interfaces per class; only ONE base class. |
| Beware : Fat interfaces; one-implementer "just in case". |
+--------------------------------------------------------------+✍️ Practice exercise
A hospital app has these two unrelated classes:
class AmbulanceDriver {
constructor(public name: string) {}
startShift(time: string): string { return `${this.name} on duty at ${time}`; }
endShift(time: string): string { return `${this.name} off duty at ${time}`; }
refuelVehicle(): string { return "Ambulance refuelled"; }
}
class WardNurse {
constructor(public name: string) {}
startShift(time: string): string { return `Nurse ${this.name} on duty at ${time}`; }
endShift(time: string): string { return `Nurse ${this.name} off duty at ${time}`; }
checkMedicineStock(): string { return "Stock checked"; }
}
class DutyRoster {
markPresent(staff: AmbulanceDriver | WardNurse, time: string): void {
console.log(staff.startShift(time));
}
}Your tasks:
- Identify the role
DutyRosteractually depends on. Which two methods belong in the contract? Which two methods must stay out, and why? (Hint: re-read the warning callout about slices.) - Extract an
OnDutyStaffinterface and migrateDutyRosteraway from the union type, following the safe steps — check yourself against the state diagram in Figure 7. - Write a tiny
FakeStaffimplementing the interface so a unit test can verifyDutyRosterwithout real drivers or nurses. - Decision question: a teammate suggests "make
HospitalStaffan abstract base class instead, since both havestartShift/endShift." The two method bodies differ (the nurse version adds "Nurse "). Would a superclass remove any real duplication here? Place the case on the quadrant in Figure 4, then defend your choice of interface or superclass in two sentences. - College-level bonus: the hospital also wants
DutyRosterto mark security guards present — but the guard class comes from a third-party package you cannot edit, and it already has matchingstartShift/endShiftmethods. Explain why this is trivial in TypeScript or Python (structural typing / Protocol) and what extra step a nominal language like C# would need (hint: a small adapter class signing the form on the guard's behalf).
If DutyRoster compiles with no union types, no instanceof, and your fake passes the test, the form at your gate is working — and Murugan would wave any new worker through tomorrow without changing a single line.
Frequently asked questions
- What exactly does an interface share, if not code?
- Only a promise — a list of method signatures every implementer must provide. There are no fields and no method bodies to inherit. The electrician and the plumber each sign in their own way; the FORM only guarantees that both CAN sign in and sign out. Shared code needs Extract Superclass instead.
- Why can a class implement many interfaces but extend only one class?
- Because interfaces carry no state or implementation, there is nothing to clash when a class takes on several of them — they are just stacked promises. A base class carries fields and bodies, and inheriting those from two parents creates conflicts, so C#, Java, and TypeScript allow exactly one base class.
- When should I extract an interface instead of a superclass?
- When the classes share only a role, not code — like an electrician and plumber who are totally different workers but both 'sign in and sign out'. Also when the classes already extend something else, when you need swappable implementations, or when tests need a fake. If method bodies are duplicated, an interface will not help; extract a superclass.
- Is it bad to create an interface with only one implementing class?
- Often yes — that is the Speculative Generality smell, an interface existing 'just in case'. The honest exceptions are testing and decoupling: if a fake implementation in your tests is the second implementer, or if you need to invert a dependency across module boundaries, the interface is earning its place.
- Should the interface copy every public method of the class?
- No. Extract only the cohesive slice that clients actually use — the role. A fat interface that mirrors the whole class just renames it. Small, focused interfaces follow the Interface Segregation Principle: clients should not be forced to depend on methods they never call.
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.
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.
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.