Low-level designDeep dive9 min
Law of Demeter, or "tell, don't ask"
Talk to your immediate collaborators, not to the objects behind them. Instead of reaching through one object to change another, tell the first object what you want and let it pass the request on.
A bike share charges riders
Dock charges the unlock fee and RideMeter the fare. Each takes the money from the rider's wallet.
1 · The situation
A dock and a meter that both reach into the wallet
A bike share charges riders twice: a $1.00 fee when a dock unlocks a bike, and 15 cents a minute when the ride ends. A Rider has an Account, and the account has a Wallet with the balance. Dock and RideMeter each get the wallet with rider.account().wallet(), check the balance, and set the new one.
Python writes the wallet's methods as balance_cents() and set_balance_cents(); the other names are the same in all four languages. Formatting money happens in main, so the domain classes never call back into it.
class Wallet {
private int balanceCents;
Wallet(int balanceCents) {
this.balanceCents = balanceCents;
}
int balanceCents() {
return balanceCents;
}
void setBalanceCents(int cents) {
balanceCents = cents;
}
}
class Account {
private final Wallet wallet;
Account(Wallet wallet) {
this.wallet = wallet;
}
Wallet wallet() {
return wallet;
}
}
class Rider {
private final String name;
private final Account account;
Rider(String name, Account account) {
this.name = name;
this.account = account;
}
String name() {
return name;
}
Account account() {
return account;
}
}
class Dock {
boolean unlock(Rider rider) {
Wallet wallet = rider.account().wallet();
if (wallet.balanceCents() < 100) return false;
wallet.setBalanceCents(wallet.balanceCents() - 100);
return true;
}
}
class RideMeter {
boolean finish(Rider rider, int minutes) {
Wallet wallet = rider.account().wallet();
int fare = 15 * minutes;
if (wallet.balanceCents() < fare) return false;
wallet.setBalanceCents(wallet.balanceCents() - fare);
return true;
}
}
public class Main {
static String money(int cents) {
String sign = cents < 0 ? "-" : "";
int c = Math.abs(cents);
return String.format("%s$%d.%02d", sign, c / 100, c % 100);
}
public static void main(String[] args) {
Rider ana = new Rider("Ana", new Account(new Wallet(150)));
boolean unlocked = new Dock().unlock(ana);
System.out.println("Unlock for Ana: " + (unlocked ? "yes" : "no"));
System.out.println("Ana's balance: " + money(ana.account().wallet().balanceCents()));
boolean paid = new RideMeter().finish(ana, 10);
System.out.println("Ride of 10 min for Ana: " + (paid ? "paid" : "needs a top-up"));
System.out.println("Ana's balance: " + money(ana.account().wallet().balanceCents()));
}
}The class diagram of this version, drawn from its code: the dock and the meter depend on the wallet as well as on the rider.
Ana has $1.50. The unlock leaves her 50 cents, and a ten-minute ride costs $1.50, so it isn't paid:
Unlock for Ana: yes Ana's balance: $0.50 Ride of 10 min for Ana: needs a top-up Ana's balance: $0.50
2 · The change
A credit limit lands in every caller
The operator decides riders may go up to $2.00 into credit, so nobody is stranded at a dock for want of a few cents. That's a rule about wallets. In this design the rule for when a wallet can pay lives in its callers, so both of them change:
class Dock {
boolean unlock(Rider rider) {
Wallet wallet = rider.account().wallet();
if (wallet.balanceCents() - 100 < -200) return false;
wallet.setBalanceCents(wallet.balanceCents() - 100);
return true;
}
}
class RideMeter {
boolean finish(Rider rider, int minutes) {
Wallet wallet = rider.account().wallet();
int fare = 15 * minutes;
if (wallet.balanceCents() - fare < -200) return false;
wallet.setBalanceCents(wallet.balanceCents() - fare);
return true;
}
}Unlock for Ana: yes Ana's balance: $0.50 Ride of 10 min for Ana: paid Ana's balance: -$1.00
The ride is paid now, and Ana's balance goes below zero within the limit.
It works, and it shows the cost. The dock and the meter know how a rider is put together: a rider holds an account, which holds a wallet, which has a balance that callers may read and overwrite.
Caller | What it knows | What a change to it costs |
|---|---|---|
| That a rider has an account with a wallet, and how to debit it | Edited for the credit limit |
| The same, written a second time | Edited for the credit limit |
The next caller, such as a reservation fee | Whatever it copies from the first two | One more place for the next wallet rule |
If the account later holds two wallets, a prepaid one and a card, every caller that wrote account().wallet() breaks. The practice checks mark both methods with a hint: each reaches through rider.account().wallet().
3 · The fix
Tell the rider to pay
Start again from the first version. The callers don't need the wallet; they need the rider to pay. Say that, and let each object pass the request to the next:
Wallet.pay(cents) checks the balance, takes the money and answers yes or no. The setter goes; nobody outside sets a balance any more.
class Wallet {
private int balanceCents;
Wallet(int balanceCents) {
this.balanceCents = balanceCents;
}
int balanceCents() {
return balanceCents;
}
boolean pay(int cents) {
if (balanceCents < cents) return false;
balanceCents -= cents;
return true;
}
}After three steps the program prints exactly what the first version printed. After the fourth, it prints what the credit version printed, with one class edited instead of two:
Unlock for Ana: yes Ana's balance: $0.50 Ride of 10 min for Ana: paid Ana's balance: -$1.00
Classes the credit-limit change edits. Before: Dock and RideMeter. After: Wallet alone, however many callers there are.
4 · The principle
Only talk to your friends
The Law of Demeter, also called the principle of least knowledge, says a method should call methods only on:
- its own object;
- its parameters;
- objects it creates;
- the objects its own fields hold.
An object returned by one of those, such as the account a rider hands back, is a stranger: the method shouldn't call it. Tell, don't ask is the same advice from the other side: tell an object what you want done, rather than asking for its data and doing the work yourself.
After the fix: the dock and the meter depend on Rider alone; the wallet is reached only through the objects that hold it.
Call inside Dock.unlock(rider) | Allowed? | Why |
|---|---|---|
| Yes |
|
| Yes, but | A parameter's method, though what comes back is a stranger |
| No | Calls a method on the stranger |
| No | Changes an object two steps away |
The short form is "use only one dot", and it's only a rough guide. The question is how many objects' insides the caller depends on, not how many dots it types. rider.account().wallet() depends on two; a chain of calls that each return the same kind of object depends on one.
5 · When breaking it is fine
Wrappers, and chains through plain data
Following the law has a cost:
- Wrapper methods.
Account.pay()does nothing but pass the call on. Every operation callers need becomes a method on each object along the way. - Wider classes.
Ridernow haspay()andbalanceCents()that belong to the wallet. Done for every wallet operation, a rider grows into a copy of its parts.
When most of a class's methods only forward calls, the class is a middle man; then it's better to hand callers the part directly, or move the behavior.
Reaching through is fine when:
- The chain walks plain data. Records, value objects and configuration have no behavior to protect:
order.address().city()reads two values. - Each call returns the same kind of object. A builder, a stream or a query API is one collaborator, however many dots it takes.
- A query is what you need. Asking for the balance to show on a screen is a fair question; the rule is about not doing the wallet's work for it.
Chain | Reach-through? | Why |
|---|---|---|
| Yes | Changes an object two steps away, with its rules in the caller |
| No | Reads values; there's no behavior to bypass |
| No | Each call returns a new stream: one collaborator |
| No | Each call returns the same builder |
6 · Getting it right
Move the work, not just the dots
- Move the behavior to the data. The fix put the payment rule in
Wallet. Hiding the same chain in a helper method,Dock.walletOf(rider), keeps every dependency and adds a method. - Ask what the caller wants done. "Pay this" is a request; "give me your wallet" is a reach.
- Tell values apart from objects. Chains through records and value objects are fine; chains through objects with rules aren't.
- Watch the forwarding. If an object only forwards, consider giving callers the part they need as a parameter instead.
The checks report a method that follows two or more getters through other project objects as a hint, not a warning. They skip chains that end in an enum or record, and calls that return the same type again, which covers builders. Each language has its own fluent chains that the rule doesn't mean:
Language | Fluent chains that are fine |
|---|---|
Java | Streams, |
C++ | Ranges views joined with |
JavaScript | Array methods such as |
Python | String methods, query builders |
7 · Look-alikes
Neighbors that name the same smell
Several names point at the same trouble from different angles.
Idea | What it looks at |
|---|---|
Law of Demeter | Which objects a method may call |
Tell, don't ask | Whether a caller gives orders or takes data out to work on |
Message chains | The smell: a caller navigating |
Middle man | The opposite smell: a class that only forwards |
Feature envy | A method that uses another class's data more than its own |
Encapsulation | Whether an object protects its state; a public setter invites reaching in |
The first version shows several at once: the callers chain to the wallet (message chains), do the wallet's arithmetic (feature envy), and can because the wallet has a public setter (weak encapsulation). One move, putting pay() on the wallet, fixes all three.
8 · In practice
The same hint in the practice editor
The practice problems run the same check, so a design whose classes reach through each other gets the same hint there. In the parking lot, for example, the natural requests are "park this car" to the lot and "take this car" to a level, rather than chains from the lot down to a spot's fields.
Treat it as a prompt to look, not a verdict: the chain may walk plain data, and then it's fine. Design the parking lot or the vending machine and see which requests can be told instead.
Check yourself
4 questions
Takeaways
Remember this
- A method should call only its own object, its parameters, objects it creates and the objects its fields hold.
- Tell an object what you want done instead of taking its parts out and working on them.
- Reaching through objects copies their structure and rules into every caller.
- Following the law costs wrapper methods; a class that only forwards is a middle man.
- Chains through plain data and fluent APIs that return the same kind of object are fine.