Skip to content
aipatterns.dev

Code smellObject-Orientation Abusers

Temporary Field

A field that is set and meaningful only during one operation or in one special case, and is empty or stale the rest of the time.

Signs

  • A field is undefined or null for most of the object's life, and readers must guess when it holds a real value.
  • Methods begin with guards like if (!this.currentBatch) throw ....
  • A field is assigned at the top of one method and cleared at the bottom, so it is really a local variable stored on the instance.
  • Field names carry hints like tmp, current, working or last.
  • Running two operations concurrently on the same instance corrupts results because they share the field.

Why it happens

A long algorithm needs many intermediate values. Passing them between helper methods means long parameter lists, so the author stores them as fields instead. The helpers stay short, but the class now has state that is only valid while that one algorithm is running. A similar pattern arises when an optional feature adds fields that only some instances use.

Example

A payroll calculator that stores intermediate values on the service instance.

payroll.ts
class PayrollService {
  private grossCents?: number;
  private taxableCents?: number;
  private employee?: Employee;

  computeNet(employee: Employee, hours: number): number {
    this.employee = employee;
    this.grossCents = Math.round(hours * employee.hourlyRateCents);
    this.taxableCents = this.grossCents - this.pretaxDeductions();
    const net = this.taxableCents - this.incomeTax();
    this.employee = this.grossCents = this.taxableCents = undefined;
    return net;
  }

  private pretaxDeductions(): number {
    return Math.round(this.grossCents! * this.employee!.retirementRate);
  }

  private incomeTax(): number {
    return Math.round(this.taxableCents! * 0.22);
  }
}

Moving the calculation into its own short-lived object makes the fields always valid, and the service stays stateless.

payroll.ts
class PayCalculation {
  readonly gross: number;
  readonly taxable: number;

  constructor(private readonly employee: Employee, hours: number) {
    this.gross = Math.round(hours * employee.hourlyRateCents);
    this.taxable = this.gross - this.pretaxDeductions();
  }

  net(): number {
    return this.taxable - this.incomeTax();
  }

  private pretaxDeductions(): number {
    return Math.round(this.gross * this.employee.retirementRate);
  }

  private incomeTax(): number {
    return Math.round(this.taxable * 0.22);
  }
}

class PayrollService {
  computeNet(employee: Employee, hours: number): number {
    return new PayCalculation(employee, hours).net();
  }
}

Treatment

  • If the fields support one algorithm, use Replace Method with Method Object: the algorithm becomes a class whose fields are set in its constructor and always valid.
  • If the fields and the code that uses them form a separate concept (an optional feature, a special mode), use Extract Class and hold an instance only when it applies.
  • If code checks the field for emptiness before every use, use Introduce Null Object to provide a harmless default.
  • If the intermediate values are cheap, pass them as parameters or return them from helpers instead of storing them anywhere.

Payoff

  • Every field on every object holds a meaningful value whenever the object is reachable.
  • Non-null assertions and defensive guards disappear.
  • Long-lived services become stateless, so they are safe to share across requests and threads.
  • The algorithm becomes a named, testable unit.

When to ignore

  • Lazily computed caches that are valid once filled and invalidated deliberately. A memoized field is not a temporary field if its lifecycle is clear.
  • Framework lifecycle hooks that require fields to be set in one callback and read in another, such as test fixtures assigned in beforeEach.

Agent checklist

  • You traced each field's writes and confirmed it is valid only during part of the object's life.
  • After the change, no field on the original class is optional solely because of the algorithm you moved.
  • The new method object or extracted class is constructed fully valid; no setter is required before use.
  • Shared services you touched are now free of per-call state, and concurrent calls cannot interfere.
  • Existing tests pass; add a test that runs two calculations back to back to catch stale state.

Treatments

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.