Code smellCouplers
Message Chains
Client code walks a chain like a.getB().getC().getD() to reach what it needs, coupling itself to every link in the path.
- Also known as
- Train Wreck, Law of Demeter violation
Signs
- Expressions such as
order.customer.account.billingAddress.countryappear in client code. - The same navigation path is repeated in several files.
- Null checks guard each step:
a?.b?.c?.d. - A change to how
Accountstores addresses breaks code that only wanted a country code. - Tests must build a deep object graph to exercise one line.
Why it happens
- The object model mirrors the database schema, and callers navigate it the way they would join tables.
- Getters exist for every field, so following them is the path of least resistance.
- Nobody asked the first object for what the client actually needed, so no method for it exists.
Example
A tax calculator needs the buyer's country and digs for it.
class Address {
constructor(readonly country: string) {}
}
class Account {
constructor(readonly billingAddress: Address) {}
}
class Customer {
constructor(readonly account: Account) {}
}
class Order {
constructor(readonly customer: Customer, readonly subtotal: number) {}
}
function vatFor(order: Order): number {
const country = order.customer.account.billingAddress.country;
return country === "DE" ? order.subtotal * 0.19 : 0;
}
function shippingZone(order: Order): string {
return order.customer.account.billingAddress.country === "US" ? "domestic" : "intl";
}Both functions know the full path from Order to Address.
class Customer {
constructor(private readonly account: Account) {}
billingCountry(): string {
return this.account.billingAddress.country;
}
}
class Order {
constructor(private readonly customer: Customer, readonly subtotal: number) {}
billingCountry(): string {
return this.customer.billingCountry();
}
}
function vatFor(order: Order): number {
return order.billingCountry() === "DE" ? order.subtotal * 0.19 : 0;
}
function shippingZone(order: Order): string {
return order.billingCountry() === "US" ? "domestic" : "intl";
}Clients now depend on Order alone. Only Customer knows about Account.
Treatment
- If clients repeat a chain to reach one value, use Hide Delegate on the first link so it answers directly.
- If the client does work with the value at the end of the chain, use Extract Method on that work, then Move Method it to an object near the start of the chain.
- If you find yourself hiding delegates on every class, stop. That produces Middle Man. Hide only the paths clients actually use.
Payoff
- Clients depend on one neighbor instead of the whole object graph.
- Restructuring intermediate objects stops breaking distant code.
- Tests need shallower fixtures.
When to ignore
- Fluent and builder APIs return
thisor a value of the same kind:query.where(...).orderBy(...).limit(10). That is one object, not a chain of strangers. - Pipelines over plain values, such as
items.filter(...).map(...), are transformations, not navigation. - Immutable records and read models you only read, such as parsed JSON or configuration, where the shape is the contract. Wrapping every path adds methods without reducing coupling.
- Hiding every link would turn each class into a forwarding layer. Prefer the occasional chain.
Agent checklist
- Confirm the chain crosses several distinct types, not a fluent API on one type.
- Count how many call sites use the same path before adding a delegating method.
- Name the new method for what the client wants (
billingCountry), not for the path. - Reduce visibility of the intermediate getters once no client uses them.
- Avoid creating a Middle Man with many pure forwarders.
Treatments
- Hide DelegateAdd a method on the server object that forwards to its delegate, so clients stop navigating through it to reach the delegate.
- Extract MethodMove a coherent fragment of a long function into its own well-named function and call it from the original place.
- Move MethodMove a method to the class whose data it uses most, and leave a delegating call or update the callers.
See also
- Middle ManA class mostly forwards calls to another object and adds nothing. Let clients talk to the real object or fold the wrapper away.
- Feature EnvyA method reads and computes on another object's data more than its own. Move the logic to where the data lives.
- Inappropriate IntimacyTwo classes reach into each other's private fields and internals. Cut the back-and-forth by moving members or narrowing what each exposes.
- Hide DelegateAdd a method on the server object that forwards to its delegate, so clients stop navigating through it to reach the delegate.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.