Extract Class
Intent
Section titled “Intent”A class has quietly taken on a second responsibility. Pull the fields and methods that serve that second job out into a brand-new class, and give the original class a reference to it. Each class now has one clear reason to change.
The smell
Section titled “The smell”This is the cure for the Large Class — and, more precisely, for a class with two responsibilities tangled together. The signs: a subset of fields that always change together, methods that only operate on that subset, or a name that has to use “and” to describe what the class does. When you can draw a clean line through the class’s data, the half on each side wants to be its own type.
Before → After
Section titled “Before → After”A Person that also stores and formats a telephone number. Before, the phone fields and the phone formatting live on Person. After, they form a TelephoneNumber class, and Person simply holds one.
// Beforeclass Person { constructor( public name: string, public areaCode: string, public number: string, ) {}
telephoneNumber(): string { return `(${this.areaCode}) ${this.number}`; }}
// Afterclass TelephoneNumber { constructor(public areaCode: string, public number: string) {}
toString(): string { return `(${this.areaCode}) ${this.number}`; }}
class Person { constructor(public name: string, public telephone: TelephoneNumber) {}
telephoneNumber(): string { return this.telephone.toString(); }}# Beforeclass Person: def __init__(self, name, area_code, number): self.name = name self.area_code = area_code self.number = number
def telephone_number(self): return f"({self.area_code}) {self.number}"
# Afterclass TelephoneNumber: def __init__(self, area_code, number): self.area_code = area_code self.number = number
def __str__(self): return f"({self.area_code}) {self.number}"
class Person: def __init__(self, name, telephone): self.name = name self.telephone = telephone
def telephone_number(self): return str(self.telephone)// Beforetype Person struct { Name string AreaCode string Number string}
func (p Person) TelephoneNumber() string { return fmt.Sprintf("(%s) %s", p.AreaCode, p.Number)}
// Aftertype TelephoneNumber struct { AreaCode string Number string}
func (t TelephoneNumber) String() string { return fmt.Sprintf("(%s) %s", t.AreaCode, t.Number)}
type Person struct { Name string Telephone TelephoneNumber}
func (p Person) TelephoneNumber() string { return p.Telephone.String()}// Beforestruct Person { name: String, area_code: String, number: String,}
impl Person { fn telephone_number(&self) -> String { format!("({}) {}", self.area_code, self.number) }}
// Afterstruct TelephoneNumber { area_code: String, number: String,}
impl TelephoneNumber { fn to_string(&self) -> String { format!("({}) {}", self.area_code, self.number) }}
struct Person { name: String, telephone: TelephoneNumber,}
impl Person { fn telephone_number(&self) -> String { self.telephone.to_string() }}classDiagram
class PersonBefore {
name
areaCode
number
telephoneNumber()
}
class PersonAfter {
name
telephone
telephoneNumber()
}
class TelephoneNumber {
areaCode
number
toString()
}
PersonAfter --> TelephoneNumber : has a
PersonBefore ..> PersonAfter : Extract Class Mechanics
Section titled “Mechanics”- Decide how to split the responsibilities, and create a new empty class for the one you are extracting. If the old name no longer fits the remaining class, rename it.
- Add a link from the old class to the new one — usually a field holding an instance of the new class.
- Move the relevant fields across one at a time, using Move Field, keeping the suite green after each.
- Move the methods that belong with those fields across, using Move Function, starting from the lowest-level ones.
- Replace the old class’s direct field access with calls through the new instance.
- Run your tests after every move. Each step is small enough that a red bar points at one change.
- Review the public surface of both classes and narrow it — the extracted class may be able to hide details the original had to expose.
When to use / trade-offs
Section titled “When to use / trade-offs”Extract a class when a subset of fields and methods forms an obvious cluster, when a class is hard to summarize without “and”, or when one responsibility changes for entirely different reasons than the rest.
The cost is one more class to name, construct, and navigate, plus a layer of delegation. If a class is not genuinely doing two jobs, extracting only adds ceremony. The inverse is Inline Class: if the new class never grows into its own responsibility, fold it back in.