skip to content

What does strict adherence to the Law of Demeter cost, and how do you decide when the wrapper/delegation overhead isn't worth paying?

level: seniorimportance: should knowfreq 34%

answer

  1. Hide Delegate ↔ Remove Middle Man (both real refactorings)
  2. Premium in wrappers vs payout on structural change
  3. Wider API = traded structural for interface coupling
  4. Data structures exempt; hybrids worst
  5. Move behaviour, don't just add a getter

basics

~20 s

Every hidden hop needs a pass-through method, so classes fill with delegating one-liners (the "middle man" smell), APIs get wider, and changes ripple through several files. It's worth paying when the hidden structure is genuinely likely to change.

solid answer

~60 s

Hiding a chain means adding a forwarding method at each level: `Order.customerCity()` → `Customer.city()` → `Address.city()`. Costs: - **Wrapper proliferation** — classes accumulate delegating one-liners with no behaviour of their own, Fowler's **Middle Man** smell (its refactoring is *Remove Middle Man* — the inverse of *Hide Delegate*). - **Wider public APIs** — every intermediate object must expose everything anyone downstream might want, which is its own coupling. - **More files touched per change** — adding one field can mean edits at three levels. - **Obscured data flow** — a stack of forwarders is harder to read than the honest chain. Decide by asking whether the hidden structure is *volatile* and whether the relationship is *behavioural*. Hide the chain when the intermediate types are yours and likely to change, when the chain repeats across the codebase, or when a real operation is hiding in it. Don't bother for stable data structures, DTOs at boundaries, or one-off navigation in a test. And prefer *moving the behaviour* over adding a getter — a delegating getter satisfies the letter of LoD while keeping the design flaw.

go deeper

for a junior

Say that hiding chains means writing pass-through methods, which adds code; that's the cost.

for a middle

Name the Middle Man smell and the Hide Delegate / Remove Middle Man pair, and note that data structures and DTOs are legitimately exempt.

for a senior

Present it explicitly as a trade-off: structural coupling traded for interface coupling plus change amplification, and give concrete criteria (volatility, repetition, boundary crossing) for when to pay.

for a principal

Frame it as coupling insurance with a real premium, articulate a codebase-wide policy (strict for behaviour-rich objects, loose for data structures, violations confined to adapters at boundaries), and note the failure mode of satisfying LoD's letter while leaving Feature Envy in place.

## The cost side of the ledger Law of Demeter is often taught as pure upside. It isn't. Every violation you remove is paid for in delegation. ### What the fix actually costs Starting point: ``` name = order.customer().address().city().name() ``` Mechanical fix (*Hide Delegate*, Fowler): ``` class Order { fun cityName() = customer.cityName() } class Customer{ fun cityName() = address.cityName() } class Address { fun cityName() = city.name() } ``` Four types, three new methods, to move one string. **1. Middle Man smell.** Fowler names the failure mode: a class whose methods are mostly forwarding. Its refactoring, **Remove Middle Man**, deliberately *reintroduces* the chain — the explicit acknowledgement that LoD can be over-applied. **2. Wider APIs = new coupling.** To hide the chain, `Order` must now expose `cityName()`. If different callers need city, postcode, and country, `Order` grows three more methods it has no conceptual interest in. You traded *structural* coupling for *interface* coupling and a bloated public surface. **3. Change amplification.** "Also show the region" now means edits in Address, Customer, and Order — three files instead of one line. **4. Readability loss.** The train wreck at least told the truth about where the data lives. A three-deep forwarding stack hides it; readers must open three files to answer "where does this come from?" **5. Performance / laziness edge cases.** Forwarders can force eager loading in ORM-backed graphs, or hide N+1 query behaviour behind an innocuous-looking method. ### When to pay Pay the cost when: - **The intermediate types are yours and volatile.** LoD's value is insurance against structural change; no expected change, no payout. - **The chain repeats.** One occurrence is a line of code; twenty occurrences is a design decision now duplicated twenty times. - **A real operation is hiding in the chain.** `order.customer().account().debit(x)` isn't navigation — it's "charge the order". Name it. - **The chain crosses a module or bounded-context boundary.** Transitive coupling across a boundary is the expensive kind (see also anti-corruption layers). - **Tests need mocks returning mocks.** That's the cost already being paid, in the test suite. Don't pay when: - **It's a data structure, not an object.** DTOs, parsed JSON/config, records, ASTs, wire models. They exist to be navigated; there is no encapsulation to protect. - **The types are stable and external.** Chaining on a mature standard-library or well-versioned third-party type is low-risk. - **It's a fluent/value chain** — no stranger is acquired. - **It's test setup or a one-off script.** Optimise for readability. ### The trap: satisfying the letter, missing the point Adding `order.customerCityName()` *silences* the LoD complaint but often preserves the underlying flaw — the caller is still pulling data out to compute something itself (**Feature Envy**). The higher-value move is **Tell, Don't Ask**: give `Order` the responsibility (`order.shipTo(carrier)`) so the data never leaves. A useful discriminator: *if the delegating method's only caller is one place that immediately does something with the result, move that something inward instead.* ### A pragmatic policy 1. Treat objects-with-behaviour strictly; treat data structures loosely — never mix the two in one type ("hybrids" are the worst case). 2. When you see a train wreck, first ask what the caller *wants to accomplish*; try moving the behaviour. 3. If the behaviour genuinely belongs at the caller, then delegate — but name the delegating method after the caller's intent, not the traversal (`shippingCity()`, not `getCustomerAddressCity()`). 4. Accept some violations at boundaries, and keep them in one adapter rather than sprinkled everywhere. ### The framing to give in an interview LoD is **coupling insurance**: you pay a fixed premium in delegation to avoid a variable cost when structure changes. Like all insurance, buying it for events that will never happen is waste — and the premium is real code that real people maintain.

  • Fowler describes both 'Hide Delegate' and 'Remove Middle Man'. What does having both refactorings imply about the Law of Demeter?
    That it is a tunable trade-off, not an absolute. Hide Delegate moves toward LoD compliance; Remove Middle Man moves away when the forwarding methods have become pure noise. You are meant to slide the dial per class based on how volatile the hidden structure is.
  • Why is adding a delegating getter often an unsatisfying fix for a train wreck?
    Because it removes the chain but not the design problem: the caller is still pulling data out to act on it (Feature Envy). The better move is Tell-Don't-Ask — push the operation into the object that owns the data so nothing needs to be extracted.

It is insurance. The delegating wrappers are the monthly premium; the payout arrives only if the object graph actually changes shape. Insuring a thing that will never change is pure cost — and you also pay it in code every future reader must scroll past.

context