Law of Demeter: break the getter chain
Let each class ask its neighbor, so inner changes stay inside.
Watch the narrated video
Every step
Problem
Steps 1–3Refactor
Steps 4–7Payoff
Steps 8–9Recap
Steps 10–11The code: before and after
Before
class Membership {
private final String level;
Membership(String level) {
this.level = level;
}
String getLevel() { return level; }
}
class Customer {
private final String name;
private final Membership membership;
Customer(String name, Membership membership) {
this.name = name;
this.membership = membership;
}
String getName() { return name; }
Membership getMembership() { return membership; }
}
class Rental {
private final String car;
private final Customer customer;
Rental(String car, Customer customer) {
this.car = car;
this.customer = customer;
}
String getCar() { return car; }
Customer getCustomer() { return customer; }
}
class ReturnDesk {
String lateFee(Rental rental, int hours) {
String level =
rental.getCustomer().getMembership().getLevel();
int rate = level.equals("gold") ? 5 : 15;
return rental.getCar() + " late fee, " + hours * rate + " USD";
}
}
class Receipts {
String header(Rental r) {
String level =
r.getCustomer().getMembership().getLevel();
return r.getCustomer().getName() + " (" + level + ")";
}
}
class Desk {
public static void main(String[] args) {
Rental rental = new Rental("Golf",
new Customer("Ana", new Membership("gold")));
System.out.println(new ReturnDesk().lateFee(rental, 2));
System.out.println(new Receipts().header(rental));
}
}After
class Membership {
private final String level;
Membership(String level) {
this.level = level;
}
String getLevel() { return level; }
}
class LoyaltyAccount {
private final Membership membership;
LoyaltyAccount(Membership membership) {
this.membership = membership;
}
String level() { return membership.getLevel(); }
}
class Customer {
private final String name;
private final LoyaltyAccount account;
Customer(String name, LoyaltyAccount account) {
this.name = name;
this.account = account;
}
String getName() { return name; }
String membershipLevel() { return account.level(); }
}
class Rental {
private final String car;
private final Customer customer;
Rental(String car, Customer customer) {
this.car = car;
this.customer = customer;
}
String getCar() { return car; }
String membershipLevel() { return customer.membershipLevel(); }
String customerName() { return customer.getName(); }
}
class ReturnDesk {
String lateFee(Rental rental, int hours) {
String level = rental.membershipLevel();
int rate = level.equals("gold") ? 5 : 15;
return rental.getCar() + " late fee, " + hours * rate + " USD";
}
}
class Receipts {
String header(Rental r) {
String level = r.membershipLevel();
return r.customerName() + " (" + level + ")";
}
}
class Desk {
public static void main(String[] args) {
Rental rental = new Rental("Golf", new Customer("Ana",
new LoyaltyAccount(new Membership("gold"))));
System.out.println(new ReturnDesk().lateFee(rental, 2));
System.out.println(new Receipts().header(rental));
}
}Transcript, step by step
Problem
Step 1: The return counter Before
This is the same car rental desk from last time, and now we're at its return counter. A Rental holds the car and the customer, and each Customer holds a Membership with a level. Return Desk charges gold members five dollars a late hour and everyone else fifteen. Receipts prints the customer's name and level, and Desk is the caller that runs both. The run prints a Golf late fee of ten dollars, and then Ana, gold. A getter is a method that only hands out a field, like get level here. Every getter is already written, so reaching through them is the shortest line you can type.
Step 2: Memberships move into an account Change request
Marketing moves memberships into a new loyalty account. The quick edit adds a Loyalty Account class that holds the membership and hands it out with a getter. Customer now holds the account instead of the membership, and Desk builds one for Ana. Both chains grow by one getter, so each one now reaches four objects deep. It compiles, and the run prints the same two lines as before.
Step 3: A getter chain Code smell
A getter chain is one line that calls a getter on whatever the last getter returned. Fowler's name for this smell is Message Chains, and here Return Desk and Receipts walk through Rental and Customer to reach a Membership they were never given. You can see it in Return Desk here, in Receipts here, and in the second reach for the name here. A change inside Customer, a class neither of them owns, broke both of them. So every class that walks the chain has to be found and edited, and here a new account class meant edits in two classes that only wanted one word, the level. The Law of Demeter says a method talks to its own fields, its parameters and the objects it creates, and not to the objects those hand back. In short, talk only to your immediate friends, and the only friend each desk was given is the rental.
Refactor
Step 4: Refactor first Back it out
So let's undo the edit, refactor first, and then add the account. The chains are back to three getters, and every step from here prints the same two lines.
Step 5: Customer answers the level Hide Delegate
The first move is Hide Delegate, where a class forwards a call to an object it holds, so callers don't reach past it. Customer gets a membership level method that returns its own membership's level. Both chains now stop at the customer and ask it for the membership level. Neither desk knows a Membership exists anymore, but both still talk to the Customer.
Step 6: Rental answers too Hide Delegate
We hide the delegate once more, this time in Rental. Rental gets membership level and customer name, and each one forwards to its customer. Return Desk and Receipts now call only methods on the rental they were given.
Step 7: Delete the unused getters Remove Dead Code
Rental's get customer and Customer's get membership have no callers left. So we delete both, and no one can start the chain again. Rental still holds its customer, but now it's the only class that talks to it.
Payoff
Step 8: Add the account again Payoff
Now we add the loyalty account again. Loyalty Account holds the membership and answers its level. Customer holds the account, and its membership level now asks the account. Desk builds the account for Ana, and the run prints the same two lines. Return Desk, Receipts and Rental did not change.
Step 9: One friend each Payoff
Return Desk now makes one call on the rental here, Receipts makes one here, and the name is one call here. The account change touched Customer, the one class that holds the account, and Desk, which builds it. By the Law of Demeter, each desk is given a Rental, so it talks only to the Rental. The desks no longer know a Customer or a Membership exists, so no change inside Customer or its account can reach them. To be fair, the first move alone already kept this change out of the desks. The second and third moves cut the last link they knew, the Customer, so the next change inside it, like a split of the name, stays out too.
Recap
Step 10: Where we started, where we ended Before / after
On the left, each desk walked from the rental through the customer to the membership. Receipts walked through the customer a second time to get the name. On the right, Rental answers membership level and customer name itself. So each desk makes one call on the rental and knows nothing behind it.
Step 11: When to reach for it Takeaways
Use it when a class goes through one object to get at another one's data, and does it in more than one place. Skip it for a builder or a stream, where each call returns a type you already depend on, so you reach into no inner structure. Skip it too for plain data records, whose fields are their public interface, because delegates would only copy them. You'll spot it by a line with several getters in a row, each called on what the last one returned.