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
undefinedornullfor 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,workingorlast. - 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.
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.
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
- Extract ClassSplit a class that does two jobs by moving one job's fields and methods into a new class it holds a reference to.
- Replace Method with Method ObjectTurn a long method with tangled local variables into its own class, so the locals become fields and the body can be split freely.
- Introduce Null ObjectReplace repeated null checks for a missing collaborator with an object that implements the same interface and does the safe default.
See also
- Large ClassA class with too many fields, methods or responsibilities, so every change risks breaking something unrelated inside it.
- Long MethodA function that does several jobs in a row and has grown too long to read, name or test as one unit.
- Data ClassA class holds fields and accessors but no behavior, while other classes reach in to do the work that belongs to it.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.