Tell Don't Ask
Core Principle
Tell Don't Ask (TDA) means: instead of *asking* an object for its data and then acting on it externally, *tell* the object what to do and let it use its own data internally.
This flows directly from OOP's core idea — bundle data with the behavior that operates on it. Tightly coupled data and behavior belong in the same component.
Identifying "Ask" Style (The Problem)
Watch for this pattern: external code queries an object's state and then makes decisions based on that state.
if (order.status === "pending" && order.total > 1000) {
order.approvalQueue.push(order.id);
order.status = "awaiting_approval";
}Red flags:
- Chains of getters used to make a decision
if (obj.getX() > obj.getLimit())patterns outside the class- External code setting state after reading state (
getthenset) - Logic that "belongs" to an object scattered across callers
Applying the Refactor
Move the decision-making logic into the object that owns the data.
order.submitForApproval();class Order {
submitForApproval(): void {
if (this.status === "pending" && this.total > 1000) {
this.approvalQueue.push(this.id);
this.status = "awaiting_approval";
}
}
}The caller tells the object what to do; the object decides how.
Step-by-Step Refactoring Guide
- Find the ask chain — locate external code that reads object data to make a decision.
- Identify which object owns the data — the class whose fields are being read.
- Name the intent — what is the caller *trying to accomplish*? Name a method for that intent (e.g.,
submitForApproval,checkAlarm,applyDiscount). - Move the logic in — cut the conditional/action block and paste it into the new method on the owning class.
- Replace the call site — swap the ask chain with a single tell call.
- Remove now-unnecessary getters if nothing else uses them (be cautious — see nuance below).
Nuance: When Getters Are Fine
Martin Fowler himself notes he doesn't strictly follow TDA. Query methods are legitimate when:
- An object transforms data for its caller (e.g., formatting, aggregation)
- The object is a value object or DTO whose purpose is to carry data
- You're crossing architectural layers (e.g., persistence, serialization)
- Removing a getter would create convolutions worse than the query
The goal is co-locating behavior with data, not eliminating all accessors. Don't become a "getter eradicator" — use judgment.
Quick Reference Examples
| Ask (avoid) | Tell (prefer) |
|---|---|
if (a.value > a.limit) a.alarm.warn(...) | a.setValue(newVal) — alarm logic inside setValue |
if (cart.items.length === 0) showEmpty() | if (cart.isEmpty()) showEmpty() — at minimum, encapsulate the check |
order.status = order.computeNextStatus() | order.advance() |
When Reviewing Code
For each class under review:
- List all getters — are any used only to make external decisions about the object?
- Find conditional blocks that read from a single object's fields — can they move in?
- Look for setter chains (
setA,setB,setCin sequence) — could that be oneconfigure(...)orinitialize(...)call?
Flag violations clearly, suggest the encapsulated alternative, but note if the refactor would be overkill (e.g., trivial DTOs, framework-required accessors).