I'll Be Backend
Principles · TypeScript

Tell, Don’t Ask: move the decision inward

Tell an object what to do, so its rules live in one place.

13 steps4 chapters~5 min at 1.25×#encapsulation#getters#setters#code smells
The lesson plays right here. Pause anywhere, step back, or jump to a step below. Watch the TypeScript video on YouTube Code on GitHub

Watch the narrated video

Every step

Problem

Steps 1–4

Refactor

Steps 5–10

Payoff

Step 11

Recap

Steps 12–13

The code: before and after

Before

class Rental {
  constructor(
    private readonly car: string,
    private readonly hoursBooked: number,
    private charges: number,
  ) {}

  getCar(): string { return this.car; }
  getHoursBooked(): number { return this.hoursBooked; }
  getCharges(): number { return this.charges; }
  setCharges(charges: number): void { this.charges = charges; }
}

class ReturnDesk {
  checkIn(rental: Rental, hoursUsed: number): string {
    const late = Math.max(0, hoursUsed - rental.getHoursBooked());
    rental.setCharges(rental.getCharges() + late * 15);
    return `${rental.getCar()} returned, ` +
      `owes ${rental.getCharges()} USD`;
  }
}

class NightDrop {
  dropKeys(r: Rental, hoursUsed: number): string {
    const late = Math.max(0, hoursUsed - r.getHoursBooked());
    r.setCharges(r.getCharges() + late * 15);
    return `${r.getCar()} keys in box, ` +
      `owes ${r.getCharges()} USD`;
  }
}

function runDesk(): string[] {
  const golf = new Rental("Golf", 24, 80);
  const polo = new Rental("Polo", 48, 60);
  return [
    new ReturnDesk().checkIn(golf, 26),
    new NightDrop().dropKeys(polo, 49),
  ];
}

After

class Rental {
  constructor(
    private readonly car: string,
    private readonly hoursBooked: number,
    private charges: number,
  ) {}

  getCar(): string { return this.car; }
  getCharges(): number { return this.charges; }

  chargeLateHours(hoursUsed: number): void {
    const late = Math.max(0, hoursUsed - this.hoursBooked - 1);
    this.charges += late * 15;
  }
}

class ReturnDesk {
  checkIn(rental: Rental, hoursUsed: number): string {
    rental.chargeLateHours(hoursUsed);
    return `${rental.getCar()} returned, ` +
      `owes ${rental.getCharges()} USD`;
  }
}

class NightDrop {
  dropKeys(r: Rental, hoursUsed: number): string {
    r.chargeLateHours(hoursUsed);
    return `${r.getCar()} keys in box, ` +
      `owes ${r.getCharges()} USD`;
  }
}

function runDesk(): string[] {
  const golf = new Rental("Golf", 24, 80);
  const polo = new Rental("Polo", 48, 60);
  return [
    new ReturnDesk().checkIn(golf, 26),
    new NightDrop().dropKeys(polo, 49),
  ];
}

Transcript, step by step

Problem

Step 1: The return desk Before

This is the same car rental desk from last time, and now it bills the cars that come back. A Rental holds the car, the hours booked, and what the customer owes. Return Desk checks a car in at the counter, and Night Drop handles keys left in the box after hours. Both charge fifteen dollars for every hour past the booking, and run Desk is the caller that uses one of each. It returns Golf returned, owes a hundred and ten dollars, and then Polo keys in box, owes seventy five. A getter is a method that hands out a field, and a setter is one that overwrites a field from outside. Rental hands out every field and lets anyone set the charges, so each desk reads what it needs and writes back the total.

Step 2: One free grace hour Change request

Now the rental office wants one free grace hour before late fees start. The quick edit subtracts one more hour in Return Desk, and the same hour again in Night Drop. Both desks still write the new total back through set charges. It compiles, and run Desk now returns ninety five dollars for the Golf and sixty for the Polo.

Step 3: Ask, decide, write back Code smell

Each desk asks the Rental for its data, makes the fee decision itself, and writes the result back through a setter. You can see it in Return Desk, which asks here and writes back here, and Night Drop does the same here and here. The fee rule is about the Rental's own hours, yet it lives in two classes that don't own them, so the grace hour had to go in twice. And set charges is public, so nothing forces a caller through the fee rule at all.

Step 4: Tell, Don't Ask Principle

A shared helper, say a late fees function, would remove the copy, but it would still need the public getter and setter. Only a method inside Rental can use those private fields directly, and that's what lets the setter go. The name for this is Tell, Don't Ask, which means you tell an object what to do instead of asking for its data, deciding outside, and writing the result back. Fowler's catalog has a name for a method that cares more about another class's data than its own, Feature Envy, and both desk methods fit it.

Refactor

Step 5: Refactor first Back it out

So the grace hour comes back out for now, and both desks return the old totals again. We'll move the fee rule into Rental one small step at a time, and then the grace hour becomes a one-line change.

Step 6: Name the fee rule Extract Method

First, in Return Desk, the two fee lines become a private method called charge late hours. Check in now calls it with the rental and the hours used, and the output stays the same. Look at what the new method touches, because the hours it reads and the charges it writes both belong to the Rental.

Step 7: Rental decides the fee Move Method

Next, Move Method puts charge late hours into Rental, where it reads hours booked and adds to charges directly. Return Desk now makes one call, and that call is the tell, because the desk says how long the car was out and the Rental decides the fee. Run Desk still returns a hundred and ten and seventy five.

Step 8: Night Drop tells too Replace Inline Code with Function Call

Night Drop held the same two lines, word for word, so they become the same single call. Now both desks tell the Rental how long the car was out, and neither one works out a fee.

Step 9: Nobody writes the bill Remove Setting Method

Set charges has no callers left, so we delete it. Charges is a private field, so the compiler stops any outside write, and the fee rule inside Rental is now the only way to add to the bill.

Step 10: Drop the unused getter Remove Dead Code

Get hours booked has no callers either, so it goes too. Get car and get charges stay, because the desks read them only to build the message. A message reports the bill and decides nothing, so it can stay with each desk.

Payoff

Step 11: The grace hour, once Payoff

Now the grace hour goes back in, and it's one edit inside charge late hours. Return Desk and Night Drop did not change, and each one is still a single call here and here. Run Desk returns ninety five for the Golf and sixty for the Polo, the same totals as the quick edit. The rule lives in the one class that owns the hours, and with the setter gone, it's the only way to add to the bill.

Recap

Step 12: Where we started, where we ended Before / after

On the left, each desk asked for the hours, decided the fee, and wrote it back through the setter. Night Drop held a second copy of the same rule. On the right, Rental owns charge late hours, and nobody outside can set the charges. Each desk tells the Rental how long the car was out, and reads back only what it prints.

Step 13: When to reach for it Takeaways

Use it when callers read an object's fields to make a decision, and then write the result back into it. Skip it when a caller only reads data to show it, like a receipt line, since the format belongs to the caller. Skip it too for a plain data record crossing a boundary, such as a request body, where behavior doesn't belong. You'll spot it by a getter and a setter on the same object, inside one method of another class.