open atlas
↑ Back to track
Code patterns & craft CP · 11 · 03

Introduce parameter object

Replace a recurring data clump or long parameter list with a parameter object that names a real concept, so related behavior and validation gravitate to it and call sites get clearer — without it becoming a junk drawer.

CP Senior ◷ 19 min
Level
FoundationsJuniorMiddleSenior

You keep seeing the same three arguments travel together: startDate, endDate, and timezone ride side by side through bookRoom, then priceStay, then renderCalendar, then the report exporter. Nobody decided they belong together; they just always show up as a trio because, conceptually, they are one thing — a span of time. The code never says so, so every function re-derives “is this range valid?”, “how many nights?”, “does it overlap?” on its own, slightly differently each time.

That recurring trio is a data clump, and the long argument lists it feeds are a smell with a specific cure: give the clump a name. Introduce parameter object replaces the clump (or an over-long parameter list) with a single typed object — and the surprising payoff is not the shorter signature. It’s that the new object becomes a home for all the behavior that was scattered across callers.

Goal

After this lesson you can recognise a data clump and a too-long parameter list as the trigger for Introduce Parameter Object; perform the refactor in safe steps under a green test suite; explain why the real win is behavior and validation migrating onto the new type rather than the shorter signature; and name the failure mode — a parameter object that is a junk drawer of unrelated fields bundled only to shorten a call, instead of a cohesive concept.

1

The trigger is a data clump or a long parameter list — the same fields traveling together. A clump is two or more values that keep appearing as a set: (startDate, endDate), (x, y), (amount, currency), (host, port, protocol). The tell is mechanical: if you delete one of them, the rest stop making sense. A long parameter list is the same disease at a single call site — five, six, eight positional arguments where the reader can no longer tell which is which.

// before: the (start, end) clump rides through every function, plus boolean noise
function priceStay(
  start: Date,
  end: Date,
  ratePerNight: number,
  taxRate: number,
  includeCleaning: boolean,
  isWeekendPremium: boolean,
): number { /* ... */ }

book(start, end, 120, 0.2, true, false); // which boolean is which?

The positional booleans are unreadable and the (start, end) pair has appeared in four functions already. Both are signals, not noise — they tell you a concept is missing.

2

Do it in safe steps, staying green the whole way. Introduce Parameter Object is a mechanical refactor with a defined sequence, and you keep the tests passing after each step rather than rewriting in one leap:

  1. Create the new type with the clumped fields. Don’t change any signatures yet.
  2. Add the object as a new parameter to the target function, keeping the old ones; have the body read from the new object. Tests still green.
  3. Update one caller to pass the object. Green. Then the next caller. Green.
  4. Once every caller passes the object, delete the now-unused individual parameters.
  5. Move behavior that operated on the clumped values onto the new type (next step).
type DateRange = { readonly start: Date; readonly end: Date };

function priceStay(range: DateRange, rate: RateCard): number { /* ... */ }

Each step is reversible and verified, so a mistake is caught one move from where you made it — that is the whole point of refactoring under tests rather than via a big-bang rewrite.

3

The real payoff: behavior and validation gravitate to the new object. Once DateRange exists, every “is this range valid?” / “how many nights?” / “do these overlap?” check that was duplicated across callers has an obvious home — on the type. Martin Fowler calls this the deeper reason for the refactor: the new object becomes a magnet that attracts the behavior scattered around it.

class DateRange {
  constructor(readonly start: Date, readonly end: Date) {
    if (end < start) throw new RangeError("end before start");
  }
  get nights(): number {
    return Math.round((+this.end - +this.start) / 86_400_000);
  }
  overlaps(other: DateRange): boolean {
    return this.start < other.end && other.start < this.end;
  }
}

The end < start validation now happens once, at construction — so no downstream function can ever receive an inverted range. nights is computed in one place instead of being re-derived (with off-by-one bugs) in pricing, calendar, and reporting. This is the senior point: a parameter object doesn’t just shorten signatures, it names a missing concept and gives the related logic a place to live, which kills a whole class of duplication.

4

Whole-object: pass the object you already have instead of unpacking it. A close cousin: when a caller pulls several fields out of an existing object only to pass them as separate arguments, pass the object itself. This shrinks the parameter list, decouples the callee from the caller’s field layout, and lets the callee ask the object for more later without a signature change.

// before: caller unpacks, callee re-couples to the field names
const within = isWithinBudget(quote.low, quote.high, order.total);

// after: pass the whole object; the rule moves where it belongs
const within = quote.contains(order.total);

When NOT to: if the callee genuinely needs only one field, passing the whole object over-couples it to a type it barely uses — prefer the single value (this is the Law of Demeter / interface-segregation tension). Whole-object is right when the callee uses several fields or when “the rule lives on the object” reads more naturally than “the caller computes the rule.”

Worked example

A five-argument call becomes one typed object — and the logic follows. Start with a long parameter list whose first two args are a clump and whose calls are unreadable:

// before
function reserve(
  start: Date,
  end: Date,
  guestId: string,
  roomId: string,
  notifyByEmail: boolean,
): Reservation {
  if (end < start) throw new Error("bad dates");      // re-checked everywhere
  const nights = (+end - +start) / 86_400_000;         // re-derived everywhere
  // ...
}

reserve(start, end, "g_91", "r_204", true); // positional soup

Step 1–4 (mechanical, under green tests): introduce DateRange, thread it through, retire start/end. Step 5 (the payoff): move the validity check and the night-count onto DateRange, deleting the copies in reserve, priceStay, and calendar:

// after
class DateRange {
  constructor(readonly start: Date, readonly end: Date) {
    if (end < start) throw new RangeError("end before start");
  }
  get nights(): number { return Math.round((+this.end - +this.start) / 86_400_000); }
}

function reserve(stay: DateRange, guestId: string, roomId: string, notifyByEmail: boolean): Reservation {
  const nights = stay.nights;   // no validation here — it can't be invalid
  // ...
}

reserve(stay, "g_91", "r_204", true); // stay reads as one thing

What changed beyond the signature: an inverted range is now impossible to pass (the constructor is the only door), nights exists once, and priceStay/calendar lost their duplicated copies of both. The parameter object didn’t just tidy the call — it relocated the rules to where they belong. Note we did not stuff guestId, roomId, and notifyByEmail into the same object: those are not a cohesive concept with the dates, so bundling them would make a junk drawer (see below).

Why this works

Why is “the object attracts behavior” the real point and not the shorter signature? Because the shorter signature is cosmetic and reversible — you could get it with a // prettier tuple and gain nothing. The durable win is that a named type gives duplicated logic an unambiguous home, and once it has a home, the duplication collapses and invariants can be enforced at construction. A DateRange that validates end >= start once means every function downstream is relieved of that check and cannot be handed a broken range. That is a structural guarantee, not a tidiness preference. Senior reviewers approve this refactor for the invariant and the de-duplication, and treat the cleaner call site as a pleasant side effect.

Common mistake

The failure mode: a junk-drawer parameter object. Under pressure to shorten a reserve(a, b, c, d, e, f, g) signature, someone bundles all seven arguments into a ReserveParams bag — dates, guest id, room id, email flag, promo code, source channel — purely to make the call one argument. That is not Introduce Parameter Object; it’s hiding a long parameter list inside a struct. The fields don’t cohere, no behavior can sensibly live on the bag (what would ReserveParams.validate() even mean?), and you’ve made the type harder to read while teaching nobody anything. The test for a real parameter object: can you name it after a concept the domain already has (a date range, a money amount, an address), and does behavior want to live on it? If the only thing the fields share is “they’re arguments to the same function,” it’s a junk drawer — keep them separate, or find the smaller cohesive clumps hiding inside (the dates, then maybe a Guest), and extract those.

Check yourself
Quiz

You introduce a DateRange to replace a recurring (start, end) clump and the call sites get shorter. Beyond the shorter signatures, what is the primary senior justification for this refactor?

Recap

A data clump (the same fields always traveling together) and a long parameter list both signal a missing concept. Introduce Parameter Object cures them: create a typed object for the clumped fields and thread it through the callers in safe, green-the-whole-way steps, then delete the loose parameters. The shorter signature is the visible change, but the durable win is that related behavior and validation gravitate onto the new type — validity is enforced once at construction, derived values like nights exist in one place, and downstream functions can no longer receive a broken value. Whole-object is the sibling move: pass an object you already hold instead of unpacking its fields, when the callee uses several of them. The failure mode is the junk drawer — bundling unrelated arguments just to shorten a call. The discipline: a parameter object must name a real, cohesive concept the domain has, one that behavior actually wants to live on; if it doesn’t, you’ve only hidden a long parameter list.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.