Tell Don't Ask
Tell objects what to do, don't ask for their data
Tell, Don't Ask
Tell objects what to do instead of asking them for data and making decisions for them. This principle emphasizes that objects should encapsulate both data and the behavior that operates on that data.
"Procedural code gets information then makes decisions. Object-oriented code tells objects to do things." - Alec Sharp
The Problem
When you ask an object for its internal state and then make decisions based on that state, you're putting the logic in the wrong place. This leads to:
- Violated encapsulation: Internal state exposed to the outside
- Duplicated logic: Same decision logic repeated in multiple places
- Tight coupling: Caller depends on internal structure
- Difficult maintenance: Changes require updating multiple locations
❌ BAD: Ask Then Decide
// Asking for data and making decisions externally
interface BankAccount {
balance: number;
overdraftLimit: number;
accountType: "savings" | "checking";
isLocked: boolean;
}
// ❌ BAD: External function making decisions about account
const withdraw = (account: BankAccount, amount: number): boolean => {
// Ask for data
if (account.isLocked) {
console.log("Account is locked");
return false;
}
// Ask for more data, make decisions
if (account.accountType === "savings") {
// Savings accounts have different rules
if (account.balance < amount) {
console.log("Insufficient funds in savings");
return false;
}
} else {
// Checking accounts can use overdraft
if (account.balance + account.overdraftLimit < amount) {
console.log("Exceeds overdraft limit");
return false;
}
}
// Mutate state directly
account.balance -= amount;
return true;
};
// ❌ BAD: More asking and deciding
const getTransactionFee = (account: BankAccount, amount: number): number => {
// Same pattern: ask for type, make decision
if (account.accountType === "savings") {
return amount > 1000 ? 0 : 5;
} else {
return account.balance > 10000 ? 0 : 3;
}
};
// ❌ BAD: Logic about account scattered everywhere
const canTransferTo = (
from: BankAccount,
to: BankAccount,
amount: number
): boolean => {
// Asking both accounts for their internals
if (from.isLocked || to.isLocked) return false;
if (from.balance < amount && from.accountType === "savings") return false;
if (from.balance + from.overdraftLimit < amount) return false;
return true;
};Problems:
- Business logic about accounts is spread across multiple functions
- Every function needs to understand account internals
- Adding a new account type requires changes everywhere
- Account invariants might be violated by direct mutation
The Solution
Let the object make its own decisions. Tell it what you want, and let it figure out how to do it.
✅ GOOD: Tell Objects What To Do
// Tell objects what to do, let them decide how
interface WithdrawalResult {
success: boolean;
newBalance?: number;
error?: string;
}
interface TransferResult {
success: boolean;
error?: string;
}
// Account encapsulates its own rules
const createBankAccount = (
initialBalance: number,
overdraftLimit: number,
accountType: "savings" | "checking"
) => {
let balance = initialBalance;
let isLocked = false;
return {
// Tell the account to withdraw - it decides if it can
withdraw: (amount: number): WithdrawalResult => {
if (isLocked) {
return { success: false, error: "Account is locked" };
}
const availableFunds =
accountType === "checking" ? balance + overdraftLimit : balance;
if (amount > availableFunds) {
return { success: false, error: "Insufficient funds" };
}
balance -= amount;
return { success: true, newBalance: balance };
},
// Tell the account to deposit - it handles the logic
deposit: (amount: number): WithdrawalResult => {
if (isLocked) {
return { success: false, error: "Account is locked" };
}
if (amount <= 0) {
return { success: false, error: "Invalid amount" };
}
balance += amount;
return { success: true, newBalance: balance };
},
// Tell the account to calculate its fee - it knows its rules
calculateTransactionFee: (amount: number): number => {
if (accountType === "savings") {
return amount > 1000 ? 0 : 5;
}
return balance > 10000 ? 0 : 3;
},
// Tell the account to transfer - it coordinates with another account
transferTo: (
targetAccount: ReturnType<typeof createBankAccount>,
amount: number
): TransferResult => {
const withdrawResult = this.withdraw(amount);
if (!withdrawResult.success) {
return { success: false, error: withdrawResult.error };
}
const depositResult = targetAccount.deposit(amount);
if (!depositResult.success) {
// Rollback
balance += amount;
return { success: false, error: depositResult.error };
}
return { success: true };
},
// Tell the account to lock itself
lock: (): void => {
isLocked = true;
},
// Tell the account to unlock itself
unlock: (): void => {
isLocked = false;
},
// Only expose what's truly needed
getBalance: (): number => balance,
isAccountLocked: (): boolean => isLocked,
};
};
// Usage - we TELL the account what to do
const savings = createBankAccount(1000, 0, "savings");
const checking = createBankAccount(500, 200, "checking");
// Tell savings to withdraw
const result = savings.withdraw(100);
if (result.success) {
console.log(`New balance: ${result.newBalance}`);
}
// Tell savings to transfer to checking
savings.transferTo(checking, 200);
// Tell account to calculate its own fee
const fee = savings.calculateTransactionFee(500);Benefits:
- All account logic is in one place
- Easy to add new account types
- Caller doesn't need to know internal rules
- Account invariants are protected
Tell Don't Ask Visualization
Real-World Example: Shopping Cart
❌ BAD: Asking Cart for Data
// Ask-style shopping cart
interface CartItem {
productId: string;
name: string;
price: number;
quantity: number;
category: string;
}
interface ShoppingCart {
items: CartItem[];
couponCode?: string;
membershipLevel: "regular" | "premium" | "vip";
}
// ❌ BAD: External function asking cart for everything
const calculateTotal = (cart: ShoppingCart): number => {
let total = 0;
// Ask for items, iterate externally
for (const item of cart.items) {
total += item.price * item.quantity;
}
// Ask for membership, apply discount
if (cart.membershipLevel === "premium") {
total *= 0.95; // 5% discount
} else if (cart.membershipLevel === "vip") {
total *= 0.9; // 10% discount
}
// Ask for coupon, apply if present
if (cart.couponCode === "SAVE10") {
total -= 10;
}
return total;
};
// ❌ BAD: More external logic
const hasElectronics = (cart: ShoppingCart): boolean => {
return cart.items.some((item) => item.category === "electronics");
};
const getShippingCost = (cart: ShoppingCart): number => {
// Ask for total
const total = calculateTotal(cart);
// Ask for items to check categories
if (hasElectronics(cart)) {
return 15; // Electronics need special shipping
}
return total > 50 ? 0 : 10; // Free shipping over $50
};✅ GOOD: Telling Cart What to Do
// Tell-style shopping cart
interface Product {
id: string;
name: string;
price: number;
category: string;
}
interface CartItemInfo {
product: Product;
quantity: number;
}
const createShoppingCart = (
membershipLevel: "regular" | "premium" | "vip" = "regular"
) => {
const items = new Map<string, CartItemInfo>();
let couponCode: string | null = null;
// Private helper - cart's internal logic
const getMembershipDiscount = (): number => {
switch (membershipLevel) {
case "vip":
return 0.1;
case "premium":
return 0.05;
default:
return 0;
}
};
const getCouponDiscount = (): number => {
if (couponCode === "SAVE10") return 10;
if (couponCode === "SAVE20") return 20;
return 0;
};
return {
// Tell cart to add item - it handles the logic
addItem: (product: Product, quantity: number = 1): void => {
const existing = items.get(product.id);
if (existing) {
existing.quantity += quantity;
} else {
items.set(product.id, { product, quantity });
}
},
// Tell cart to remove item
removeItem: (productId: string): boolean => {
return items.delete(productId);
},
// Tell cart to update quantity
updateQuantity: (productId: string, quantity: number): boolean => {
const item = items.get(productId);
if (!item) return false;
if (quantity <= 0) {
items.delete(productId);
} else {
item.quantity = quantity;
}
return true;
},
// Tell cart to apply coupon - it validates
applyCoupon: (code: string): boolean => {
const validCoupons = ["SAVE10", "SAVE20"];
if (validCoupons.includes(code)) {
couponCode = code;
return true;
}
return false;
},
// Tell cart to calculate its total - it knows the rules
getTotal: (): number => {
let subtotal = 0;
for (const { product, quantity } of items.values()) {
subtotal += product.price * quantity;
}
// Apply membership discount
subtotal *= 1 - getMembershipDiscount();
// Apply coupon
subtotal -= getCouponDiscount();
return Math.max(0, subtotal);
},
// Tell cart to calculate shipping - it knows its contents
getShippingCost: (): number => {
const total = this.getTotal();
const hasElectronics = Array.from(items.values()).some(
({ product }) => product.category === "electronics"
);
if (hasElectronics) return 15;
return total > 50 ? 0 : 10;
},
// Tell cart to provide a summary
getSummary: () => ({
itemCount: Array.from(items.values()).reduce(
(sum, { quantity }) => sum + quantity,
0
),
subtotal: this.getTotal(),
shipping: this.getShippingCost(),
total: this.getTotal() + this.getShippingCost(),
}),
// Tell cart to check if it's empty
isEmpty: (): boolean => items.size === 0,
// Tell cart to clear itself
clear: (): void => {
items.clear();
couponCode = null;
},
};
};
// Usage - Tell the cart what to do
const cart = createShoppingCart("premium");
cart.addItem({ id: "1", name: "Book", price: 29.99, category: "books" }, 2);
cart.addItem({ id: "2", name: "Laptop", price: 999, category: "electronics" });
cart.applyCoupon("SAVE10");
const summary = cart.getSummary();
console.log(`Total: $${summary.total}`);Identifying "Ask" Patterns
Summary
| Ask Pattern | Tell Pattern |
|---|---|
| Get data, make decision | Send command with parameters |
| Logic in caller | Logic in object |
| Object is passive data | Object is active participant |
| Exposed internals | Encapsulated behavior |
| Scattered logic | Centralized logic |
Remember: If you find yourself writing code that gets data from an object, makes a decision, and then tells the object what to do based on that decision - you're doing it wrong. Push that decision into the object itself.
Related Principles
- Law of Demeter - Only talk to immediate friends
- SRP - Each object handles its own responsibility
- OCP - Tell enables extension through new commands