How does the Law of Demeter relate to "Tell, Don't Ask" and to the Feature Envy smell, and how do you refactor a train wreck without simply adding a delegating getter?
answer
- LoD = detector, Tell-Don't-Ask = direction, Move Method = mechanics
- Read the line AFTER the chain
- Intent name, not traversal name
- Pass collaborators as parameters (params are friends)
- Query/read models are exempt
basics
~20 sA long getter chain usually means the caller is pulling data out to do work that belongs elsewhere (Feature Envy). Instead of adding another getter, move the operation into the object that owns the data — tell it what to do.
solid answer
~50 sThe three ideas describe one failure from different angles. **Feature Envy** is a method more interested in another object's data than its own. **Tell, Don't Ask** says instruct objects to perform operations rather than querying their state and deciding externally. A **Law of Demeter violation** — the train wreck — is the *syntactic footprint* both leave behind: to act on data you don't own, you first have to navigate to it. So the refactoring is not "add `order.customerCity()`"; that hides the chain and keeps the misplaced logic. Instead: look at what the caller does *after* the chain, and use **Move Method** to relocate that logic to the object nearest the data. `if (order.customer().address().country() == "US") applyDomesticRate()` becomes `order.applyShippingRate(rates)` or `order.shippingRate(rates)`. Test for success: after the refactoring the object graph is no longer traversed by the caller *and* the decision lives with the data. If you only achieved the first, you got compliance, not design.
code
pseudocode · 9 lines// ASK (train wreck + feature envy): decision far from the data
if (order.customer().address().country() == "US") cost = base
else cost = base * 1.8
// Hide Delegate only: LoD satisfied, rule still misplaced
if (order.customerCountry() == "US") ...
// TELL: decision moves to the owner of the data; rateTable passed in (params are friends)
cost = order.shippingCost(rateTable)go deeper
Say a long getter chain usually means the code is in the wrong class, and the fix is to move that code to the object that owns the data.
Name Feature Envy and Tell-Don't-Ask, and show the difference between Hide Delegate (symptom) and Move Method (cause).
Give the worked before/weak-fix/strong-fix progression, explain why passing collaborators as parameters keeps the result LoD-clean, and state the two-part success test.
Discuss where Tell-Don't-Ask stops applying (read models, mappers, reporting), the god-class counterweight, and how to keep unavoidable traversal confined to explicit boundary components.
## Three names, one problem ### Definitions - **Feature Envy** (Fowler): a method that uses another object's data more than its own — the classic sign that the method is in the wrong class. - **Tell, Don't Ask** (Sharp/Pragmatic Programmers): don't ask an object for its state, make a decision, and then act on it from outside; *tell* the object what you need done and let it decide. Procedural code asks; object-oriented code tells. - **Law of Demeter**: only send messages to immediate collaborators; don't navigate through returned objects. The relationship: **Tell-Don't-Ask and Feature Envy explain *why* the train wreck exists; the Law of Demeter is what you *see*.** You had to walk the graph because you wanted data that isn't yours, because the behaviour that consumes it is sitting in the wrong place. A useful order of operations: LoD is a **detector**; Tell-Don't-Ask is the **direction**; Move Method / Extract Method are the **mechanics**. ### Worked refactoring **Before — asking:** ``` // in ShippingService if (order.customer().address().country() == "US") cost = base * 1.0 else cost = base * 1.8 ``` Problems: three graph edges hard-coded, the domestic/international rule lives in a service, and testing requires constructing Customer, Address, Country. **Weak fix — Hide Delegate:** ``` if (order.customerCountry() == "US") ... ``` One dot. LoD satisfied. The rule still lives outside the domain, `Order` now exposes a country it doesn't care about, and the next requirement ("EU is its own tier") still changes the service. **Strong fix — Tell:** ``` cost = order.shippingCost(rateTable) // Order asks its Customer; Customer asks its Address; each answers about itself ``` Now: no traversal in the caller, the rule lives with the data, and `rateTable` (the genuinely external input) is *passed in* as a parameter — which LoD explicitly permits. ### Mechanics checklist 1. **Look past the chain to the next line.** The chain is never the point; the code that consumes its result is. 2. **Name the operation the caller actually wants** (`shippingCost`, `canCheckOut`, `notifyOwner`) — an intent name, not a traversal name. 3. **Move Method** the consuming logic to the object holding the most-used data, repeating one hop at a time. 4. **Pass collaborators in as parameters** (rate tables, printers, formatters). Parameters are friends, so this stays LoD-clean and avoids the object having to *know* those things. 5. **Consider a double dispatch / visitor** when the outer object shouldn't grow the method: pass a handler in and let the inner object call back. 6. **Introduce a value object** when the chain keeps bottoming out on primitives (`Country`, `Money`) — it gives the behaviour a home. ### When Tell doesn't apply - **Query/read models and reporting.** Presenting data *is* the job; there's no behaviour to move. Build a dedicated read DTO instead of navigating the domain graph. - **Serialization/mapping boundaries.** Something must read the whole graph. Confine it to a mapper. - **Value objects.** `money.currency().symbol()` is not envy; it's composition of values. - **Getters as legitimate API.** Objects may expose data; the smell is only when *behaviour* follows the read. ### The exam answer "The train wreck is a symptom. The disease is that a decision is being made far from the data it depends on. Hide Delegate treats the symptom; Move Method plus Tell-Don't-Ask treats the disease. After the fix, ask two questions — did the traversal disappear, *and* did the decision move? If only the first, I refactored the syntax, not the design."
- If the behaviour genuinely belongs in the calling service (say a pricing engine), how do you still avoid the train wreck?Pass the service what it needs as parameters rather than letting it navigate: `pricing.quote(country, weight)`, with the domain object extracting its own values (`order.quoteFrom(pricing)`). Parameters are permitted collaborators, so the traversal stays inside the object that owns the graph.
- Doesn't Tell-Don't-Ask push too much behaviour into domain objects and create god classes?It can, if applied without limit. The counterweights are Single Responsibility and extracting policies into their own collaborators that get passed in. The goal is that decisions live near their data, not that one class owns every decision.
Asking is a manager who requests every employee's calendar, computes availability at their own desk, and books the meeting. Telling is the manager saying "find us an hour on Thursday" — same outcome, but nobody has to know how anyone else keeps their schedule.