Replace Subclass with Fields
Intent
Section titled “Intent”Replace Subclass with Fields dissolves a small hierarchy whose subclasses carry no behaviour — only different constant values returned from their overridden methods. Each such method becomes a field on a single concrete class, and the values that distinguished the subclasses move into that class’s constructor. The hierarchy disappears; the data it encoded survives as plain fields.
The smell
Section titled “The smell”You spot a subclass whose every override is a one-liner returning a literal: isMale() { return true; }, code() { return "M"; }. The subclass adds no logic, holds no per-instance state, and never varies its answer. A whole type — with its own file, its own constructor, its own slot in the hierarchy — exists only to hard-code two constants. That is a Lazy Class dressed up as a subtype, and it makes the differences between variants harder to see than a row of field assignments would.
Before → After
Section titled “Before → After”A Person split into Male and Female subclasses, each overriding methods to return fixed constants. After, one Person holds those constants as fields set by a factory.
// Before — subclasses that only return constantsabstract class Person { abstract isMale(): boolean; abstract code(): string;}
class Male extends Person { isMale(): boolean { return true; } code(): string { return "M"; }}
class Female extends Person { isMale(): boolean { return false; } code(): string { return "F"; }}
// After — one class, the constants become fieldsclass Person { private constructor( private readonly male: boolean, private readonly genderCode: string, ) {}
static createMale(): Person { return new Person(true, "M"); } static createFemale(): Person { return new Person(false, "F"); }
isMale(): boolean { return this.male; } code(): string { return this.genderCode; }}# Before — subclasses that only return constantsclass Person: def is_male(self): raise NotImplementedError
def code(self): raise NotImplementedError
class Male(Person): def is_male(self): return True
def code(self): return "M"
class Female(Person): def is_male(self): return False
def code(self): return "F"
# After — one class, the constants become fieldsclass Person: def __init__(self, male, code): self._male = male self._code = code
@classmethod def create_male(cls): return cls(True, "M")
@classmethod def create_female(cls): return cls(False, "F")
def is_male(self): return self._male
def code(self): return self._code// Go has no inheritance, so the "before" would already be modelled with an// interface plus two empty structs whose methods return constants — pure// boilerplate. The idiomatic shape is the "after": one struct with fields,// built by constructor functions.
type Person struct { male bool code string}
func NewMale() Person { return Person{male: true, code: "M"} }func NewFemale() Person { return Person{male: false, code: "F"} }
func (p Person) IsMale() bool { return p.male }func (p Person) Code() string { return p.code }// Rust has no inheritance. The "before" — a trait with two unit structs// whose impls return constants — adds nothing but ceremony. The idiomatic// form holds the data in fields on one struct, with constructor functions.
struct Person { male: bool, code: char,}
impl Person { fn new_male() -> Self { Person { male: true, code: 'M' } }
fn new_female() -> Self { Person { male: false, code: 'F' } }
fn is_male(&self) -> bool { self.male }
fn code(&self) -> char { self.code }}classDiagram
class Person {
<<abstract>>
+isMale() bool
+code() string
}
class Male
class Female
Person <|-- Male
Person <|-- Female
class PersonAfter["Person"] {
-male: bool
-code: string
+isMale() bool
+code() string
} Mechanics
Section titled “Mechanics”- Apply Replace Constructor with Factory Function to the superclass, so callers create instances through factory methods rather than
new Subclass()directly. - For each method that the subclasses override to return a constant, add a field on the superclass holding that value.
- Make each factory method pass the appropriate constant when constructing the superclass instance.
- Change the superclass method to return the new field instead of being abstract.
- Run your tests. Each call site should now build a superclass instance with the right field values.
- Delete the empty subclasses once nothing references them.
When to use / trade-offs
Section titled “When to use / trade-offs”Reach for this whenever a subclass’s only job is to hard-code return values — no extra fields, no real behaviour, no per-call logic. Folding those values into fields removes a type, flattens the hierarchy, and lines the variants up so their differences read as data instead of class definitions.
Do not apply it if the subclasses carry genuine behaviour that varies, or if you expect them to grow it: then the hierarchy (or a strategy) is still earning its keep. The inverse is Replace Fields with Subclass — promote a discriminating field back into subtypes when behaviour, not just data, starts to diverge along it.