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.
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.
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 = 
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))
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.
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
def print_balance(account): print("Current Balance: %s" % account.balance()) if account.is_overdrawn(): print("(Overdrawn by %s)" % account.available_credit())
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.
‘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.