Law of Demeter
Definition

Violations (Train Wrecks)
// BAD: Method chain reaching deep into object graph
public class OrderService {
void processOrder(Order order) {
// Violation: Reaching through multiple objects
String cityName = order
.getCustomer() // Not my friend
.getAddress() // Not my friend
.getCity() // Not my friend
.getName(); // Too deep!
// Violation: Getting internal objects and manipulating them
Money balance = order
.getCustomer()
.getWallet()
.getBalance();
if (balance.isGreaterThan(order.getTotal())) {
order.getCustomer().getWallet().debit(order.getTotal());
}
}
}
// Problems:
// - OrderService knows too much about Customer structure
// - If Wallet changes to PaymentMethods, OrderService breaks
// - If Address structure changes, OrderService breaks
// - Tightly coupled to entire object graph
Proper Implementation
// GOOD: Only talk to immediate friends
public class OrderService {
void processOrder(Order order) {
// Tell, don't ask - Order handles its own payment
order.process();
}
}
public class Order {
private Customer customer;
private Money total;
void process() {
// Order talks to its direct field (customer)
customer.charge(total);
}
String getShippingCity() {
// Delegate to customer, don't expose Address
return customer.getShippingCity();
}
}
public class Customer {
private Wallet wallet;
private Address address;
void charge(Money amount) {
// Customer talks to its direct field (wallet)
wallet.debit(amount);
}
String getShippingCity() {
// Customer talks to its direct field (address)
return address.getCityName();
}
}
// Now OrderService only knows about Order
// Customer structure can change without affecting OrderService
// Much better encapsulation!
What's Allowed
public class Example {
private Helper helper; // Field - OK to call
void method(Parameter param, Factory factory) {
// 1. Call methods on itself
this.doSomething(); // OK
// 2. Call methods on parameters
param.process(); // OK
// 3. Call methods on objects it creates
Local local = new Local();
local.execute(); // OK
// 4. Call methods on its fields
helper.help(); // OK
// 5. Call methods on objects created by parameters
Product product = factory.create();
product.use(); // OK (factory is param)
// NOT OK: Chain through returned objects
param.getChild().getGrandchild().doIt(); // VIOLATION!
}
}
Fluent APIs Exception

Refactoring Train Wrecks
// BEFORE: Train wreck violation
class InvoiceGenerator {
void generate(Order order) {
// Reaching deep into Customer
String name = order.getCustomer().getName();
String street = order.getCustomer().getAddress().getStreet();
String city = order.getCustomer().getAddress().getCity();
String country = order.getCustomer().getAddress().getCountry();
// Create invoice with this data...
}
}
// AFTER: Proper encapsulation
// Option 1: Delegate methods
class Order {
String getCustomerName() { return customer.getName(); }
String getBillingAddress() { return customer.getFormattedAddress(); }
}
class InvoiceGenerator {
void generate(Order order) {
String name = order.getCustomerName();
String address = order.getBillingAddress();
}
}
// Option 2: Create data transfer object
record InvoiceData(
String customerName,
String billingAddress,
Money total
) {}
class Order {
InvoiceData getInvoiceData() {
return new InvoiceData(
customer.getName(),
customer.getFormattedAddress(),
total
);
}
}
class InvoiceGenerator {
void generate(Order order) {
InvoiceData data = order.getInvoiceData();
// Use data directly, no reaching needed
}
}
// Option 3: Tell, don't ask
class Order {
Invoice generateInvoice() {
return Invoice.builder()
.customer(customer.getName())
.address(customer.getFormattedAddress())
.total(total)
.build();
}
}
class InvoiceService {
void process(Order order) {
Invoice invoice = order.generateInvoice();
invoiceRepository.save(invoice);
}
}
When to Ignore LoD

Tips & Tricks
