Refactoring techniqueDealing with Generalization
Extract Interface
Declare the subset of a class's methods that clients actually use as an interface, so clients depend on the contract instead of the class.
Problem
Several clients use only part of a class, or several classes offer the same operations and clients should not care which one they get. Clients still name the concrete class, so they cannot be given a fake in tests or a different implementation in production.
Solution
Declare an interface with exactly the methods the clients use, make the class implement it, and change the clients to depend on the interface.
Before
class S3Storage {
constructor(private client: S3Client, private bucket: string) {}
async put(key: string, body: Buffer): Promise<void> { /* ... */ }
async get(key: string): Promise<Buffer> { /* ... */ }
async presign(key: string, ttlSeconds: number): Promise<string> { /* ... */ }
async listVersions(key: string): Promise<string[]> { /* ... */ }
}
class InvoiceArchive {
constructor(private storage: S3Storage) {}
async store(invoice: Invoice, pdf: Buffer): Promise<string> {
const key = `invoices/${invoice.tenantId}/${invoice.number}.pdf`;
await this.storage.put(key, pdf);
return this.storage.presign(key, 3600);
}
}After
interface BlobStore {
put(key: string, body: Buffer): Promise<void>;
presign(key: string, ttlSeconds: number): Promise<string>;
}
class S3Storage implements BlobStore {
constructor(private client: S3Client, private bucket: string) {}
async put(key: string, body: Buffer): Promise<void> { /* ... */ }
async get(key: string): Promise<Buffer> { /* ... */ }
async presign(key: string, ttlSeconds: number): Promise<string> { /* ... */ }
async listVersions(key: string): Promise<string[]> { /* ... */ }
}
class InvoiceArchive {
constructor(private storage: BlobStore) {}
async store(invoice: Invoice, pdf: Buffer): Promise<string> {
const key = `invoices/${invoice.tenantId}/${invoice.number}.pdf`;
await this.storage.put(key, pdf);
return this.storage.presign(key, 3600);
}
}Why
InvoiceArchivecan be tested with an in-memoryBlobStoreand no AWS SDK.- Swapping S3 for another store touches only the composition root.
- The interface documents which capabilities the client relies on.
Steps
- List the methods the client actually calls on the class.
- Declare an interface containing only those methods, with identical signatures. Run the compiler.
- Mark the class as implementing the interface. Run the compiler and tests.
- Change the client's field, parameter or variable type from the class to the interface. Run the tests.
- Repeat for other clients. Create separate small interfaces if clients use different subsets.
Watch out
- Do not extract an interface with a single implementation and no test double or second implementation in sight. It adds a file and an indirection. In TypeScript, structural typing lets tests pass any object of the right shape without a declared interface.
- Do not copy the whole public surface into the interface. A wide interface forces fakes and new implementations to stub methods no one uses.
- Interfaces carry no code. If implementations share behavior, combine with Extract Superclass or composition.
- There is no named inverse. Inline the interface (replace it with the class) when it has one implementation and no test value.
Related
- Fixes Alternative Classes with Different Interfaces and loosens Inappropriate Intimacy.
- Compare with Extract Superclass, which shares implementation as well.
- An extracted interface is often the seam for a Strategy.
Agent checklist
- The interface contains only methods a client actually calls.
- There is a second implementation, a test double, or a module boundary that justifies it.
- Clients now reference the interface type, not the class.
- Construction of the concrete class happens in one place.
Treats these smells
- Alternative Classes with Different InterfacesTwo classes do the same job but expose different method names or signatures, so callers cannot swap one for the other.
- Inappropriate IntimacyTwo classes reach into each other's private fields and internals. Cut the back-and-forth by moving members or narrowing what each exposes.
See also
- Extract SuperclassCreate a shared superclass for two classes with overlapping fields and methods, and pull the common parts up into it.
- Extract ClassSplit a class that does two jobs by moving one job's fields and methods into a new class it holds a reference to.
- Replace Inheritance with DelegationTurn a subclass that uses only part of its superclass into a class that holds an instance of it and forwards just the calls it needs.
- StrategyPut each interchangeable algorithm behind one interface so the caller picks a behavior at runtime without branching on it.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.