Use when accessing nested object properties. Use when chaining method calls. Use when reaching through objects to get data.
Only talk to your immediate friends, not strangers.
A method should only call methods on: itself, its parameters, objects it creates, or its direct components. Never reach through an object to access another object's internals.
obj.a.b.cobj.getA().getB().getC()NEVER chain through objects. Ask, don't reach.
No exceptions:
If you see multiple dots, you're violating LoD:
// ❌ VIOLATION: Reaching through objects
function getEmployeeCity(company: Company, employeeId: string): string {
return company.employees
.find(e => e.id === employeeId)
?.address.city; // Reaching into employee, then into address
}
// More violations:
user.getProfile().getAddress().getZipCode();
order.getCustomer().getPaymentMethod().getLast4();
Let objects expose what's needed:
// ✅ CORRECT: Ask the object directly
class Employee {
constructor(
private name: string,
private address: Address
) {}
getCity(): string {
return this.address.city; // Employee asks its own address
}
}
class Company {
getEmployeeCities(): Map<string, string> {
return new Map(
this.employees.map(e => [e.id, e.getCity()])
);
}
getEmployeeCity(employeeId: string): string | undefined {
return this.employees.find(e => e.id === employeeId)?.getCity();
}
}
// Usage: Ask company, don't reach through it
const city = company.getEmployeeCity(employeeId);
| Problem | Impact |
|---|---|
| Tight coupling | Caller knows internal structure |
| Fragile code | Structure changes break all callers |
| Hidden dependencies | Not obvious what's needed |
| Hard to test | Must mock entire chain |
| Null danger | Each . is a potential null |
A method m of class C should only call methods on:
this - C's own methodsmm createsclass OrderProcessor {
constructor(private logger: Logger) {} // Component
process(order: Order): Receipt { // Parameter
this.validate(order); // this
const receipt = new Receipt(order); // Created
this.logger.log('Processed'); // Component
return receipt;
}
// ❌ NOT ALLOWED: order.customer.address.city
// ✅ ALLOWED: order.getShippingCity()
}
Pressure: "One line with dots is simpler than adding methods"
Response: Simple to write ≠ simple to maintain. Chains create fragile code.
Action: Add methods that expose needed data.
Pressure: "It's only two dots, not a big deal"
Response: Two dots = two objects you're coupled to. Both can change and break you.
Action: Even short chains should be eliminated.
Pressure: "The structure has the data, why wrap it?"
Response: Structure changes. Wrapping isolates you from changes.
Action: Ask the owner for the data.
Pressure: "I'm just reading, not modifying"
Response: Reading through chains still couples you to structure.
Action: Ask for what you need.
If you notice ANY of these, refactor:
a.b.c.dgetA().getB().getC()a?.b?.c?.dAll of these mean: Add a method to ask directly.
// ❌ BEFORE: Chain
const zip = user.getProfile().getAddress().getZipCode();
// ✅ AFTER: Ask
// In User class:
getZipCode(): string {
return this.profile.getZipCode();
}
// In Profile class:
getZipCode(): string {
return this.address.zipCode;
}
// Usage:
const zip = user.getZipCode();
| Chain (Bad) | Ask (Good) |
|---|---|
company.employees[0].address.city |
company.getEmployeeCity(id) |
order.customer.paymentMethod.last4 |
order.getPaymentLast4() |
user.profile.settings.theme |
user.getTheme() |
car.engine.fuel.level |
car.getFuelLevel() |
| Excuse | Reality |
|---|---|
| "It's simpler" | Chains are simpler to write, harder to maintain. |
| "Just one chain" | One chain = multiple couplings. |
| "Data is right there" | Expose it properly through methods. |
| "It's read-only" | Reading chains still couples you. |
| "Fewer lines" | Lines don't matter. Maintainability does. |
| "It's obvious what it does" | Obvious coupling is still coupling. |
Ask objects for what you need. Don't reach through them.
When you need data from nested objects: add a method on the owner that returns it. Never chain through multiple objects. Each dot is a dependency you're taking on.