I'll Be Backend
Principles · C#

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 C# 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(string level)
{
    public string Level { get; } = level;
}

class Customer(string name, Membership membership)
{
    public string Name { get; } = name;
    public Membership Membership { get; } = membership;
}

class Rental(string car, Customer customer)
{
    public string Car { get; } = car;
    public Customer Customer { get; } = customer;
}

class ReturnDesk
{
    public string LateFee(Rental rental, int hours)
    {
        string level = rental.Customer.Membership.Level;
        int rate = level == "gold" ? 5 : 15;
        return $"{rental.Car} late fee, {hours * rate} USD";
    }
}

class Receipts
{
    public string Header(Rental r)
    {
        string level = r.Customer.Membership.Level;
        return $"{r.Customer.Name} ({level})";
    }
}

class Desk
{
    static void Main()
    {
        var rental = new Rental("Golf",
            new Customer("Ana", new Membership("gold")));
        Console.WriteLine(new ReturnDesk().LateFee(rental, 2));
        Console.WriteLine(new Receipts().Header(rental));
    }
}

After

class Membership(string level)
{
    public string Level { get; } = level;
}

class LoyaltyAccount(Membership membership)
{
    public string Level => membership.Level;
}

class Customer(string name, LoyaltyAccount account)
{
    public string Name { get; } = name;
    public string MembershipLevel => account.Level;
}

class Rental(string car, Customer customer)
{
    public string Car { get; } = car;
    public string MembershipLevel => customer.MembershipLevel;
    public string CustomerName => customer.Name;
}

class ReturnDesk
{
    public string LateFee(Rental rental, int hours)
    {
        string level = rental.MembershipLevel;
        int rate = level == "gold" ? 5 : 15;
        return $"{rental.Car} late fee, {hours * rate} USD";
    }
}

class Receipts
{
    public string Header(Rental r)
    {
        string level = r.MembershipLevel;
        return $"{r.CustomerName} ({level})";
    }
}

class Desk
{
    static void Main()
    {
        var account = new LoyaltyAccount(new Membership("gold"));
        var rental = new Rental("Golf",
            new Customer("Ana", account));
        Console.WriteLine(new ReturnDesk().LateFee(rental, 2));
        Console.WriteLine(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, and in C sharp a get-only property like Level is one. 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 property 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 read only properties of the rental they were given.

Step 7: Delete the unused getters Remove Dead Code

Rental's Customer property and Customer's Membership property have no readers left. So we delete both, and no one can start the chain again. The delegates now read the constructor parameters, so Rental still holds its customer, but 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 LINQ query, 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 properties 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.