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.

Watch it evolve1 / 10
The problem

A bike share charges riders

Dock charges the unlock fee and RideMeter the fare. Each takes the money from the rider's wallet.

80
Start
Stage 1
Stage 2
Stage 3
Final

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.

The first versionJava
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()));
    }
}
Wallet− balanceCents: int~ balanceCents(): int~ setBalanceCents(int)Account− wallet: Wallet~ wallet(): WalletRider− name: String− account: Account~ name(): String~ account(): AccountDock~ unlock(Rider): booleanRideMeter~ finish(Rider, int): boolean

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:

The credit limit, edited into both callersJava
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

Dock

That a rider has an account with a wallet, and how to debit it

Edited for the credit limit

RideMeter

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.

Java
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;
    }
}
Wallet− balanceCents: int~ balanceCents(): int~ pay(int): booleanAccount− wallet: Wallet~ balanceCents(): int~ pay(int): booleanRider− name: String− account: Account~ name(): String~ balanceCents(): int~ pay(int): booleanDock~ unlock(Rider): booleanRideMeter~ finish(Rider, int): boolean

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
Callers reach in
2
Callers tell
1

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.

Wallet− CREDIT_LIMIT_CENTS: int− balanceCents: int~ balanceCents(): int~ pay(int): booleanAccount− wallet: Wallet~ balanceCents(): int~ pay(int): booleanRider− name: String− account: Account~ name(): String~ balanceCents(): int~ pay(int): booleanDock~ unlock(Rider): booleanRideMeter~ finish(Rider, int): boolean

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

rider.pay(100)

Yes

rider is a parameter

rider.account()

Yes, but

A parameter's method, though what comes back is a stranger

rider.account().wallet()

No

Calls a method on the stranger

wallet.setBalanceCents(...)

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. Rider now has pay() and balanceCents() 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

rider.account().wallet().setBalanceCents(0)

Yes

Changes an object two steps away, with its rules in the caller

order.address().city()

No

Reads values; there's no behavior to bypass

list.stream().filter(...).map(...)

No

Each call returns a new stream: one collaborator

builder.size(10).name("x")

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, StringBuilder, builders

C++

Ranges views joined with |, builder-style setters that return *this

JavaScript

Array methods such as filter and map, promise chains

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 a.b().c().d()

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

1. After the fix, the operator adds a weekly spending cap to wallets. Which classes change?
2. Which call breaks the Law of Demeter inside Dock.unlock(rider)?
3. A teammate replaces rider.account().wallet() in both callers with a helper, walletOf(rider). Is the problem solved?
4. Which chain is fine as it is?

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.