Tell, Don’t Ask: move the decision inward
Tell an object what to do, so its rules live in one place.
Watch the narrated video
Every step
Problem
Steps 1–4Refactor
Steps 5–10Payoff
Step 11Recap
Steps 12–13The code: before and after
Before
class Rental:
def __init__(self, car: str, hours_booked: int, charges: int) -> None:
self.car = car
self.hours_booked = hours_booked
self.charges = charges
class ReturnDesk:
def check_in(self, rental: Rental, hours_used: int) -> str:
late = max(0, hours_used - rental.hours_booked)
rental.charges += late * 15
return f"{rental.car} returned, owes {rental.charges} USD"
class NightDrop:
def drop_keys(self, r: Rental, hours_used: int) -> str:
late = max(0, hours_used - r.hours_booked)
r.charges += late * 15
return f"{r.car} keys in box, owes {r.charges} USD"
def main() -> None:
golf = Rental("Golf", 24, 80)
polo = Rental("Polo", 48, 60)
print(ReturnDesk().check_in(golf, 26))
print(NightDrop().drop_keys(polo, 49))
if __name__ == "__main__":
main()After
class Rental:
def __init__(self, car: str, hours_booked: int, charges: int) -> None:
self.car = car
self._hours_booked = hours_booked
self._charges = charges
@property
def charges(self) -> int:
return self._charges
def charge_late_hours(self, hours_used: int) -> None:
late = max(0, hours_used - self._hours_booked - 1)
self._charges += late * 15
class ReturnDesk:
def check_in(self, rental: Rental, hours_used: int) -> str:
rental.charge_late_hours(hours_used)
return f"{rental.car} returned, owes {rental.charges} USD"
class NightDrop:
def drop_keys(self, r: Rental, hours_used: int) -> str:
r.charge_late_hours(hours_used)
return f"{r.car} keys in box, owes {r.charges} USD"
def main() -> None:
golf = Rental("Golf", 24, 80)
polo = Rental("Polo", 48, 60)
print(ReturnDesk().check_in(golf, 26))
print(NightDrop().drop_keys(polo, 49))
if __name__ == "__main__":
main()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 main is the caller that runs one of each. The run prints Golf returned, owes a hundred and ten dollars, and then Polo keys in box, owes seventy five. A getter hands out a field, and a setter overwrites a field from outside, so in Python reading an attribute is a getter and assigning one is a setter. Rental leaves every field as a plain public attribute, 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 by assigning to charges. Mypy is happy, and the run now prints 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 charges is a public attribute, so nothing forces a caller through the fee rule at all.
Step 4: Tell, Don't Ask Principle
A shared helper function, say charge late fee, would remove the copy, but it would still read the hours and assign the charges from outside. Only a method inside Rental can use that data directly, and that's what lets the public attributes 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 let's take the grace hour back out and move the fee rule first. Once the Rental owns that rule, the grace hour should be a single edit in one place.
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 its own hours booked and adds to its own charges. 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. The run still prints 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 Encapsulate Field
No desk assigns to charges anymore, so Encapsulate Field moves the value into underscore charges and adds a read-only property. That property is now the getter, and since it has no setter, assigning to charges from outside fails. The leading underscore tells readers the field is private to Rental, though Python won't stop someone who reaches for it. Now the only way to add to the bill is the fee rule inside Rental.
Step 10: Hide the hours Encapsulate Field
Nobody outside reads hours booked anymore, so it becomes underscore hours booked, with no property at all. Car and charges stay readable, because the desks read them only to print the message. A printed 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. The run prints 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 charges read-only, 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 attribute. Night Drop held a second copy of the same rule. On the right, Rental owns charge late hours, and nobody outside can assign 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. In Python, a dict has set default, which tells the dict to supply the list in place of checking, creating and storing it yourself.