In software development, the way we structure our code can significantly impact its clarity, maintainability, and scalability. One guiding principle that helps achieve these qualities is the “Tell, Don’t Ask” principle. This principle is not just a set of rules but a mindset that, when embraced, transforms the interaction between objects in our code, leading to a more robust and cohesive design.

Understanding this principle is best achieved through an example. Consider the code snippet below:

public class ATMService {

    public void withdraw(Account account, Double amount) {
        
        if (account.getBalance() > amount) {
            account.setBalance(account.getBalance() - amount);
        } else {
            throw new InsufficientAccountBalanceException();
        }
    }

}

public class Account {
    private Double balance;

    public Double getBalance() {
        return this.balance;
    }
    
    public void setBalance(Double balance) {
        this.balance = balance;
    }
}

Analyze this for a moment and consider whether the ATMService class is taking on more responsibilities than it should.

The preceding code is quite typical, where business logic is implemented in the service layer, and our models serve merely as data containers (model state), leading to the prevalence of anemic models.

In the code above, the ATMService class has access to the entire state of the Account object, even making decisions about operations that should be executed by the object itself:

public class ATMService {

    public void withdraw(Account account, Double amount) {
        
        if (account.getBalance() > amount) {
            account.setBalance(account.getBalance() - amount);
        } else {
            throw new InsufficientAccountBalanceException();
        }
    }

}

...

Decisions and changes to an object’s internal state should ideally be encapsulated within the model itself. Instead of relying on anemic models, it’s advisable to employ rich models that encapsulate business logic, thereby limiting state changes to their public methods.

The essence of the “Tell, Don’t Ask” principle is to interact with objects not by inquiring about their state and making decisions on their behalf, but rather by telling them what to do. This approach fosters better encapsulation and interaction among objects.

Let’s refactor the previous code to apply this principle:

public class ATMService {

    public void withdraw(Account account, Double amount) {
        account.withdraw(amount);
    }

}

public class Account {
    private Double balance;

    public void withdraw(Double amount) {
        if (this.balance > amount) {
            this.balance -= amount;
        } else {
            throw new InsufficientAccountBalanceException();
        }
    }

    // Getters
    // Setters
}

By moving the balance check inside the Account model, decision-making is confined to the model, reducing the service’s responsibility to merely invoking the desired action on the model.

Identifying Violations

There are a few common code structures/patterns that can help us spot if our code isn’t following this principle.

Detective Conditionals

A common pattern indicating a violation of this principle is checking an object’s state before performing an action. For example:

// Incorrect
if (employee.getStatus().equals("ACTIVE")) {
    employee.doWork();
} else {
    console.log("This employee can't work right now.");
}

// Correct
class Employee {
    public void doWork() {
        if (this.status.equals("ACTIVE")) {
            System.out.println("Work!");
        } else {
            System.out.println("This employee can't work right now.");
        }
    }
}

// Usage
employee.doWork();

Take a look at other example:

// Incorrect
if (cart.getItems().isEmpty()) {
    System.out.println("Your cart is empty, like your promises.");
} else {
    cart.checkout();
}

// Correct
class Cart {
    public void checkout() {
        if (this.items.isEmpty()) {
            System.out.println("Your cart is empty, like your promises.");
        } else {
            System.out.println("Checking out...");
        }
    }
}

// Usage
cart.checkout();

Leakage of Business Logic

Another common pattern is accessing a model’s state to perform calculations, a sign that business logic is leaking outside the model. For instance, calculating the total value in a Shopping Cart:

class ShoppingCart {
    private List<Double> prices = new ArrayList<>();

    public void addPrice(double price) {
        prices.add(price);
    }

    public List<Double> getPrices() {
        return prices;
    }
}

ShoppingCart cart = new ShoppingCart();
cart.addPrice(19.99);
cart.addPrice(5.99);

double total = 0;

for (double price : cart.getPrices()) { // Incorrect
    total += price;
}

System.out.println("Total: $" + total);

While this seems alright at first glance, we fall into the trap of anemic models and violate our “Tell, Don’t Ask” principle. The issues that emerge are:

  1. We’re implementing the business logic to calculate the total value of the shopping cart outside the ShoppingCart model, leading to Business Logic Leakage. This is insecure, and every time we want to know the cart’s total value, we’ll have to duplicate the calculation code violating “Don’t Repeat Yourself” principle (DRY).
  2. The Service using our model is accessing the prices and making decisions on how to calculate, rather than letting the model handle it itself.

To address this issue, let’s apply the following changes:

class ShoppingCart {
    private List<Double> prices = new ArrayList<>();

    public void addPrice(double price) {
        prices.add(price);
    }

    public double calculateTotal() {
        double total = 0;
        for (double price : this.prices) {
            total += price;
        }
        return total;
    }
}

ShoppingCart cart = new ShoppingCart();
cart.addPrice(19.99);
cart.addPrice(5.99);
System.out.println("Total: $" + cart.calculateTotal());

With this new version, the logic is contained within the ShoppingCart model, and the decision on how to calculate the total (business logic) is encapsulated. If another service needs to perform the calculation, it doesn’t need to duplicate the code; it simply calls the calculateTotal method.

In Summary

Adopting the “Tell, Don’t Ask” principle has several benefits:

  • Encapsulation: It keeps an object’s state hidden, reinforcing the object-oriented principle of encapsulation.
  • Simplicity: It simplifies the calling code, as the consumer doesn’t need to make decisions based on the object’s state.
  • Cohesion: It encourages designing objects that are responsible for their actions, leading to higher cohesion within the object’s design.
  • Flexibility: It makes your code more adaptable to changes, as the behavior related to state changes is localized within the object itself.

Adhering to this principle and advocating for rich domain models enables us to centralize business logic within our models. This practice prevents the dispersion of business logic across different services, making our systems more maintainable, understandable, and scalable over time.

What’s Next?

In this post, we’ve explored the “Tell, Don’t Ask” principle. We also touched on other concepts that go hand in hand with this principle, which you should review to further improve your code:

  • Rich Domain Models
  • Anemic Domain Models
  • Invariants
  • Don’t Repeat Yourself Principle

Exploring these concepts will deepen your understanding and application of best practices in your code, leading to a more robust and maintainable software architecture.

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Agregue un comentario

Su dirección de correo no se hará público. Los campos requeridos están marcados *