Skip to content

Preserve Whole Object

A caller already holds an object, then digs out two or three of its fields only to pass them, one by one, into a function. Hand the whole object across instead. The argument list shrinks, the function can reach for any field it needs, and if the object grows a new relevant field later, the signature does not have to change.

At the call site you see a string of accessor calls feeding straight into another call: plot(point.x, point.y), fits(range.low, range.high). The values being unpacked all come from the same object and travel together. The function’s parameter list mirrors the object’s internals, so the two are quietly coupled while pretending to be independent.

A reading is checked against a temperature range. The caller pulls low and high out of the range to pass them in. Instead, pass the range itself and let the check ask it directly.

// Before
function fitsRange(reading: number, low: number, high: number): boolean {
return reading >= low && reading <= high;
}
const ok = fitsRange(today, range.low, range.high);
// After
interface Range { low: number; high: number; }
function fitsRange(reading: number, range: Range): boolean {
return reading >= range.low && reading <= range.high;
}
const ok = fitsRange(today, range);
flowchart LR
  subgraph Before["Before"]
    A["caller pulls low + high<br/>from range"] --> B["fits(low, high)"]
  end
  subgraph After["After"]
    C["caller passes range"] --> D["fits(range)"]
  end
  Before -.->|"Preserve Whole Object"| After
Several unpacked fields collapse into one whole-object argument
  1. Add a new parameter for the whole object, leaving the existing field parameters in place for now.
  2. Inside the function, replace each use of an unpacked field with a read from the whole object.
  3. Update each caller to pass the object and stop passing the individual fields. Run your tests after each one.
  4. Once every caller is converted, delete the now-unused field parameters.
  5. Look for logic in the caller that only manipulated those fields — it may belong inside the function, or as a method on the object itself.

Reach for this when the unpacked fields all belong to one object, when several functions pull the same cluster of fields, or when the function would benefit from access to more of the object than it currently receives. It often reveals Feature Envy — code that fiddles with another object’s data — which you can then move onto the object as a method.

The trade-off is a tighter dependency: the function now knows about the object’s type, not just two plain numbers. If the callee should stay ignorant of that type — for reuse in unrelated contexts, or to avoid a circular dependency — keep the loose parameters. And if a caller does not already have the whole object handy, assembling one just to make the call is a step backward.

What does Preserve Whole Object replace in an argument list?
Which call site smell signals this refactoring?
What deeper smell does Preserve Whole Object often expose?
When should you keep passing loose fields instead of the whole object?