Skip to content

Replace Subclass with Fields

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.

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.

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 constants
abstract 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 fields
class 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; }
}
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
  }
Two data-only subclasses collapse into one class with fields
  1. Apply Replace Constructor with Factory Function to the superclass, so callers create instances through factory methods rather than new Subclass() directly.
  2. For each method that the subclasses override to return a constant, add a field on the superclass holding that value.
  3. Make each factory method pass the appropriate constant when constructing the superclass instance.
  4. Change the superclass method to return the new field instead of being abstract.
  5. Run your tests. Each call site should now build a superclass instance with the right field values.
  6. Delete the empty subclasses once nothing references them.

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.

Which subclasses are candidates for Replace Subclass with Fields?
What replaces each constant-returning overridden method after the refactoring?
What is the typical first step that lets callers stop using new Subclass() directly?
How do Go and Rust relate to this refactoring?