Skip to content

Tell, Don't Ask


Definition

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

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

Tell, Don't Ask Tips and Tricks