DEV Community

Cover image for Tell Don’t Ask
Matthew Lucas
Matthew Lucas

Posted on • Originally published at notmattlucas.com

Tell Don’t Ask

Quality software, and most importantly software that maintains that quality, is written in a way that protects itself from future corruption. It makes it easy to build upon without introducing new bugs or breaking existing features.

There are many strategies to help us achieve this quality — unit testing for one — but there is an aspect of object-oriented programming that is designed specifically to tackle this problem. The key is to keep data and the logic that uses that data close together.

The aim is to produce “shy” code. Code that doesn’t expose too much to anyone who interacts with it, and only interacts through well-defined commands and queries. This is the very definition of encapsulation. It gives us the protection we need to build in robustness and it is applicable at all levels — object, module and service.

Unfortunately, we all slip up from time to time and accidentally reveal more than we should’ve. There is, however, a principle that can help us avoid this pitfall.

Tell Don’t Ask

When it comes to combining logic with the data that drives it there are two ways you can approach this, and they closely align with procedural and object-oriented schools of thought.

Asking

Procedural code generally asks for some data and then acts on this data. The behavior and data are kept separate.

Take the following example of a bank account. The account has a fixed overdraft limit and a set of transactions (tuples of payee x amount).

class BankAccount:

    def __init__(self, overdraft):
        self.overdraft = overdraft
        self.transactions = []
Enter fullscreen mode Exit fullscreen mode

Say we want to calculate and print the balance of the bank account — a function print_balance. In the procedural style, the function has to pull everything it needs from the bank account and handle all calculations itself.

def print_balance(account):
    amount = sum([amt for _, amt in account.transactions])
    print("Current Balance: %s" % amount)
    if amount < -account.overdraft:
        print("(Overdrawn by %s)" % (amount + account.overdraft))
Enter fullscreen mode Exit fullscreen mode

Whilst this gives a lot of power to the caller it also hands over much of the responsibility. Due to the loss of encapsulation, we’re now exposed to duplication, coupling and inevitably bugs.

This is an anti-pattern known as the Anemic Domain Model.

Telling / Querying

Rather than asking the data structure for all of its data, if we tell (or query) the object for that information, the responsibilities become clearer and the data model becomes richer.

Instead of asking for the raw data, we ask for facts for which the object is responsible — the account balance and available credit.

Rather than adding any transactions directly, we inform the account about them using an add method where it can make any protective assertions based on its state.

class BankAccount:

    def __init__(self, overdraft):
        self._overdraft = overdraft
        self._transactions = []

    def add(self, payee, amount):
        self._transactions.append((payee, amount))

    def balance(self):
        return sum([amt for _, amt in self._transactions])

    def is_overdrawn(self):
        return self.balance() < -self._overdraft

    def available_credit(self):
        return self.balance() + self._overdraft
Enter fullscreen mode Exit fullscreen mode
def print_balance(account):
    print("Current Balance: %s" % account.balance())
    if account.is_overdrawn():
        print("(Overdrawn by %s)" % account.available_credit())
Enter fullscreen mode Exit fullscreen mode

Beware the paperboy

In “The Art of Enbugging” there is a great analogy for this principle.

A paperboy comes to the door to collect his payment for the week. He snatches the wallet from your back pocket, takes a wad of cash, and puts it back. This doesn’t sound quite right does it, but it’s how a lot of software is written. Some central logic bullies several “data” objects to do its bidding.

More realistically, the paperboy would tell you to pay and after some thought (validation) you should hand over the cash. This is how object-oriented code is supposed to work, with you looking after your affairs, the paperboy his, and communication taking place through messages passed back and forth.

Final words

‘Tell don’t ask’ is a good rule of thumb to help you structure your code well, and it doesn’t just apply to low-level code. The core message — behavior/data co-location — is equally as important for broader architecture too.

It is, however, just a rule of thumb and as with many good design principles should be considered within the context of everything else. You will find that in certain circumstances the wider architecture dictates a more important structure (for example layering), and in that case, data co-location is just one factor of many to bear in mind.

Further reading

Discussion (0)