Refactoring techniqueSimplifying Method Calls
Remove Setting Method
Delete the setter for a field that should only be set at creation, and assign it in the constructor so it cannot change later.
Problem
A field is meant to be fixed once the object exists: an ID, a tenant, the currency of an invoice. A public setter lets any code change it later. Changing it can corrupt related data, and readers cannot tell whether the value is stable.
Solution
Set the field through the constructor (or a factory), make it read-only, and delete the setter.
Before
class Invoice {
private currency = "USD";
private lines: InvoiceLine[] = [];
setCurrency(currency: string): void {
this.currency = currency;
}
getCurrency(): string {
return this.currency;
}
addLine(line: InvoiceLine): void {
this.lines.push(line);
}
}
const invoice = new Invoice();
invoice.setCurrency("EUR");
invoice.addLine({ description: "Consulting", amountCents: 120_000 });After
class Invoice {
private lines: InvoiceLine[] = [];
constructor(readonly currency: string) {}
addLine(line: InvoiceLine): void {
this.lines.push(line);
}
}
const invoice = new Invoice("EUR");
invoice.addLine({ description: "Consulting", amountCents: 120_000 });Why
- An invoice's currency can no longer change after lines priced in the old currency were added.
- Readers and the compiler both know the field is stable, so no defensive re-checks are needed.
- Immutable fields are safe to share and to use as map keys.
Steps
- Find every caller of the setter. Confirm each call happens right after construction, before the object is used.
- Add a constructor parameter for the field. Have existing callers pass the value they used to set. Run the tests.
- Remove the setter calls that follow construction. Run the tests.
- Delete the setter and mark the field
readonly(orfinal,val). The compiler will flag any remaining writes. Run the tests. - If the field is set internally in a later step, such as an ID assigned on save, keep that write private and named for its purpose rather than a general setter.
Watch out
- Do not remove a setter for state that legitimately changes, like an order's status. Replace it with intention-revealing methods (
ship(),cancel()) instead. - Frameworks such as ORMs, serializers and dependency injectors may need setters or a no-argument constructor. Check whether they can use constructor binding or private field access before removing.
- If construction now takes many arguments, use a factory or builder instead of a long constructor; see Replace Constructor with Factory Method.
- The inverse direction is adding a setter through Encapsulate Field; do it only when the value must truly change.
Related
- Treats Data Class, where fields are freely writable.
- Often paired with Replace Constructor with Factory Method and Hide Method.
Agent checklist
- All setter calls occurred during initialization only.
- Frameworks that hydrate this class still work without the setter.
- The field is marked
readonlyso the compiler blocks future writes. - State that does change goes through named domain methods, not a setter.
Treats these smells
See also
- Replace Constructor with Factory MethodWrap object creation in a named static or module function when a plain constructor cannot express intent, pick a subtype, or reuse instances.
- Encapsulate FieldMake a public field private and give clients accessor methods, so the class controls how its data is read and changed.
- Hide MethodReduce a method's visibility to private or protected when nothing outside the class needs it, shrinking the public surface.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.