I'll Be Backend
Principles · TypeScript

Law of Demeter: break the getter chain

Let each class ask its neighbor, so inner changes stay inside.

11 steps4 chapters~5 min at 1.25×#coupling#encapsulation#getters#code smells
The lesson plays right here. Pause anywhere, step back, or jump to a step below. Watch the TypeScript video on YouTube Code on GitHub

Watch the narrated video

Every step

Problem

Steps 1–3

Refactor

Steps 4–7

Payoff

Steps 8–9

Recap

Steps 10–11

The code: before and after

Before

class Membership {
  constructor(private readonly level: string) {}
  getLevel(): string {
    return this.level;
  }
}

class Customer {
  constructor(
    private readonly name: string,
    private readonly membership: Membership,
  ) {}
  getName(): string {
    return this.name;
  }
  getMembership(): Membership {
    return this.membership;
  }
}

class Rental {
  constructor(
    private readonly car: string,
    private readonly customer: Customer,
  ) {}
  getCar(): string {
    return this.car;
  }
  getCustomer(): Customer {
    return this.customer;
  }
}

class ReturnDesk {
  lateFee(rental: Rental, hours: number): string {
    const level =
      rental.getCustomer().getMembership().getLevel();
    const rate = level === "gold" ? 5 : 15;
    const fee = hours * rate;
    return `${rental.getCar()} late fee, ${fee} USD`;
  }
}

class Receipts {
  header(r: Rental): string {
    const level =
      r.getCustomer().getMembership().getLevel();
    return `${r.getCustomer().getName()} (${level})`;
  }
}

function desk(): string[] {
  const rental = new Rental(
    "Golf",
    new Customer("Ana", new Membership("gold")),
  );
  return [
    new ReturnDesk().lateFee(rental, 2),
    new Receipts().header(rental),
  ];
}

After

class Membership {
  constructor(private readonly level: string) {}
  getLevel(): string {
    return this.level;
  }
}

class LoyaltyAccount {
  constructor(private readonly membership: Membership) {}
  level(): string {
    return this.membership.getLevel();
  }
}

class Customer {
  constructor(
    private readonly name: string,
    private readonly account: LoyaltyAccount,
  ) {}
  getName(): string {
    return this.name;
  }
  membershipLevel(): string {
    return this.account.level();
  }
}

class Rental {
  constructor(
    private readonly car: string,
    private readonly customer: Customer,
  ) {}
  getCar(): string {
    return this.car;
  }
  membershipLevel(): string {
    return this.customer.membershipLevel();
  }
  customerName(): string {
    return this.customer.getName();
  }
}

class ReturnDesk {
  lateFee(rental: Rental, hours: number): string {
    const level = rental.membershipLevel();
    const rate = level === "gold" ? 5 : 15;
    const fee = hours * rate;
    return `${rental.getCar()} late fee, ${fee} USD`;
  }
}

class Receipts {
  header(r: Rental): string {
    const level = r.membershipLevel();
    return `${r.customerName()} (${level})`;
  }
}

function desk(): string[] {
  const rental = new Rental(
    "Golf",
    new Customer("Ana", new LoyaltyAccount(new Membership("gold"))),
  );
  return [
    new ReturnDesk().lateFee(rental, 2),
    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 the desk function at the bottom is the caller that runs both. It returns 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 the desk function builds one for Ana. Both chains grow by one getter, so each one now reaches four objects deep. The compiler is happy, and desk returns 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 returns 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 as a private field, and the compiler stops any other class from reading 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. The desk function builds the account for Ana, and it returns 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 the desk function, 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.