Tell, Don't Ask
Definition

Ask vs Tell Examples
// EXAMPLE 1: Account balance
// BAD: Asking for data, deciding externally
class PaymentService {
void processPayment(Account account, Money amount) {
// Asking for internal state
if (account.getBalance().isGreaterThanOrEqualTo(amount)) {
// Making decision for the account
account.setBalance(account.getBalance().subtract(amount));
// What about overdraft? Minimum balance? Frozen account?
}
}
}
// GOOD: Telling the object what to do
class PaymentService {
void processPayment(Account account, Money amount) {
account.withdraw(amount); // Account knows its own rules
}
}
class Account {
void withdraw(Money amount) {
if (!canWithdraw(amount)) {
throw new InsufficientFundsException();
}
balance = balance.subtract(amount);
recordTransaction(Transaction.withdrawal(amount));
}
private boolean canWithdraw(Money amount) {
return balance.subtract(amount).isGreaterThanOrEqualTo(minimumBalance)
&& !isFrozen()
&& !isOverdraftLimitExceeded(amount);
}
}
More Examples
// EXAMPLE 2: Order status
// BAD: Feature envy - OrderProcessor knows too much about Order
class OrderProcessor {
void processOrder(Order order) {
if (order.getStatus() == Status.PENDING) {
if (order.getItems().size() > 0) {
if (order.getPaymentStatus() == PaymentStatus.PAID) {
order.setStatus(Status.PROCESSING);
order.setProcessedAt(Instant.now());
}
}
}
}
}
// GOOD: Tell Order to process itself
class OrderProcessor {
void processOrder(Order order) {
order.startProcessing();
}
}
class Order {
void startProcessing() {
if (!canStartProcessing()) {
throw new InvalidOrderStateException(status);
}
this.status = Status.PROCESSING;
this.processedAt = Instant.now();
}
private boolean canStartProcessing() {
return status == Status.PENDING
&& !items.isEmpty()
&& paymentStatus == PaymentStatus.PAID;
}
}
// EXAMPLE 3: User notifications
// BAD: Asking user for preferences, deciding externally
void sendNotification(User user, Message message) {
if (user.getEmailEnabled()) {
if (user.getEmail() != null) {
emailService.send(user.getEmail(), message);
}
}
if (user.getSmsEnabled()) {
if (user.getPhone() != null) {
smsService.send(user.getPhone(), message);
}
}
}
// GOOD: Tell user to handle notification
void sendNotification(User user, Message message) {
user.notify(message, notificationService);
}
class User {
void notify(Message message, NotificationService service) {
preferences.getEnabledChannels().forEach(channel ->
service.send(channel, getContactFor(channel), message)
);
}
}
Command Pattern
// Tell, Don't Ask often leads to Command pattern
// Instead of asking for state and calculating
class DiscountCalculator {
Money calculate(Order order) {
Money total = order.getSubtotal();
if (order.getCustomer().isPremium()) {
total = total.multiply(0.9); // 10% off
}
if (order.getItemCount() > 10) {
total = total.multiply(0.95); // 5% off
}
return order.getSubtotal().subtract(total);
}
}
// Tell order to apply its discount rules
class Order {
private List<DiscountRule> discountRules;
Money calculateDiscount() {
Money discount = Money.ZERO;
for (DiscountRule rule : discountRules) {
discount = discount.add(rule.apply(this));
}
return discount;
}
}
interface DiscountRule {
Money apply(Order order);
}
class PremiumCustomerDiscount implements DiscountRule {
public Money apply(Order order) {
if (order.getCustomer().isPremium()) {
return order.getSubtotal().multiply(0.10);
}
return Money.ZERO;
}
}
class BulkOrderDiscount implements DiscountRule {
public Money apply(Order order) {
if (order.getItemCount() > 10) {
return order.getSubtotal().multiply(0.05);
}
return Money.ZERO;
}
}
When Asking is OK

Refactoring Ask to Tell
// Step-by-step refactoring
// BEFORE: External decision making
class ShippingService {
void shipOrder(Order order) {
// Asking order for its state
if (order.getStatus() != OrderStatus.PAID) {
throw new CannotShipException();
}
if (order.getShippingAddress() == null) {
throw new NoAddressException();
}
if (order.getWeight() > 50) {
useFreightShipping(order);
} else {
useStandardShipping(order);
}
order.setStatus(OrderStatus.SHIPPED);
order.setShippedAt(Instant.now());
}
}
// AFTER: Object makes its own decisions
class ShippingService {
void shipOrder(Order order) {
ShippingMethod method = order.determineShippingMethod();
order.ship(method, this::executeShipment);
}
private void executeShipment(Order order, ShippingMethod method) {
// Actual shipping logic
}
}
class Order {
void ship(ShippingMethod method, BiConsumer<Order, ShippingMethod> shipper) {
validateCanShip(); // Throws if not valid
shipper.accept(this, method);
this.status = OrderStatus.SHIPPED;
this.shippedAt = Instant.now();
}
ShippingMethod determineShippingMethod() {
return weight > 50 ? ShippingMethod.FREIGHT : ShippingMethod.STANDARD;
}
private void validateCanShip() {
if (status != OrderStatus.PAID) {
throw new CannotShipException("Order not paid");
}
if (shippingAddress == null) {
throw new CannotShipException("No shipping address");
}
}
}
Tips & Tricks
