Law of Demeter: Don't Talk to Strangers
How the Law of Demeter reduces coupling, makes code more testable, and why train wrecks like a.getB().getC().doThing() are a design smell.
The Law of Demeter (LoD), also known as the principle of least knowledge, states that a method should only talk to its immediate friends — not to strangers. In practice: don’t chain through objects to reach deep dependencies.
The Rule
A method M of object O should only call methods on:
OitselfM’s parameters- Objects created within
M O’s direct component objects
Train Wrecks
The most visible violation of LoD is the “train wreck” — a chain of method calls that reaches deep into an object graph:
// ❌ Train wreck — reaching through multiple layers
const city = order.getCustomer().getAddress().getCity();
// This method now depends on:
// - Order's interface
// - Customer's interface
// - Address's interface
// Change ANY of these and this code breaks
Why Train Wrecks Are Problematic
- Tight coupling: Your code knows the internal structure of objects it shouldn’t know about
- Fragile code: Any change in the chain breaks callers
- Hard to test: You need to set up the entire object graph for a simple test
- Hard to refactor: Moving or restructuring any intermediate object cascades changes everywhere
Fixing Train Wrecks
Approach 1: Tell, Don’t Ask
Instead of reaching in to get data, tell the object what you need:
// ❌ Ask: reach in and compute externally
function calculateShipping(order: Order): number {
const city = order.getCustomer().getAddress().getCity();
const weight = order.getItems().reduce((sum, i) => sum + i.getWeight(), 0);
return shippingRates.calculate(city, weight);
}
// ✅ Tell: let the object handle its own data
class Order {
calculateShipping(rates: ShippingRateService): number {
return rates.calculate(this.shippingCity, this.totalWeight);
}
get shippingCity(): string {
return this.customer.shippingCity;
}
get totalWeight(): number {
return this.items.reduce((sum, item) => sum + item.weight, 0);
}
}
class Customer {
get shippingCity(): string {
return this.address.city;
}
}
Each object only exposes what its direct consumers need, encapsulating its own structure.
Approach 2: Delegate Methods
// ❌ Caller navigates the object graph
function sendInvoice(order: Order) {
const email = order.getCustomer().getContactInfo().getEmail();
mailer.send(email, buildInvoice(order));
}
// ✅ Order delegates to customer
class Order {
get customerEmail(): string {
return this.customer.email;
}
}
class Customer {
get email(): string {
return this.contactInfo.email;
}
}
function sendInvoice(order: Order) {
mailer.send(order.customerEmail, buildInvoice(order));
}
Approach 3: Pass What You Need
// ❌ Function receives more than it needs
function formatShippingLabel(order: Order): string {
const addr = order.getCustomer().getAddress();
return `${addr.getStreet()}\n${addr.getCity()}, ${addr.getZip()}`;
}
// ✅ Function receives exactly what it needs
interface ShippingAddress {
street: string;
city: string;
zip: string;
}
function formatShippingLabel(address: ShippingAddress): string {
return `${address.street}\n${address.city}, ${address.zip}`;
}
// Caller extracts the address once
const label = formatShippingLabel(order.shippingAddress);
Data Structures vs Objects
The Law of Demeter applies to objects (which hide data behind behavior), not to data structures (which expose data by design):
// Data structures — chaining is fine
const name = response.data.user.firstName;
const config = appConfig.database.connection.host;
// These are just data — there's no behavior to encapsulate
// LoD doesn't apply here
The distinction matters. DTOs, API responses, and configuration objects are data structures. Business entities with behavior are objects.
Testing Benefits
LoD-compliant code is dramatically easier to test:
// ❌ Without LoD — need to build entire graph
test('calculates shipping', () => {
const address = new Address('Paris', '75001', 'FR');
const customer = new Customer('John', address);
const order = new Order(customer, items);
expect(calculateShipping(order)).toBe(15.99);
});
// ✅ With LoD — pass only what's needed
test('calculates shipping', () => {
const address: ShippingAddress = { city: 'Paris', zip: '75001', country: 'FR' };
expect(calculateShipping(address, totalWeight)).toBe(15.99);
});
Practical Guidelines
- Count the dots: More than one dot usually means an LoD violation (
a.b.c= smell) - Fluent APIs are exceptions:
query.where('age', '>', 18).orderBy('name').limit(10)is fine — same object returned - Don’t over-delegate: Creating dozens of pass-through methods can be worse than the original chain
- Use interfaces: Define narrow interfaces for what callers actually need
- Think in terms of responsibilities: Each object should answer questions about its own domain
“Shy code that doesn’t reveal itself to others and doesn’t interact with too many things has fewer couplings, and couplings are the root cause of many maintenance headaches.” — The Pragmatic Programmer