Have you ever called a method on an object… then chained another… and another… until the line looked like a trail of dots?
Or made a small internal change to one class, and suddenly had to update code across five other layers?
If yes, you’ve probably run into a violation of one of the most overlooked design principles in software engineering: The Law of Demeter (LoD).
Let’s understand it with a real-world example and see why this principle matters more than you might think.
The Problem
Imagine you're building a simple e-commerce system.
You have:
- A Customer who owns a ShoppingCart
- The cart contains a list of CartItems
- Each CartItem refers to a Product
- And every Product has a Price
Now let’s say you want to display the price of the first product in a customer’s shopping cart.
A common (but flawed) approach would be to write something like this:
Money price = customer.getShoppingCart().getItems()[0].getProduct().getPrice();Look at that chain. OrderService is coupled to six classes deep. It knows about the customer's cart, the cart's internal list, the structure of a cart item, the product inside it, and the price object. A change to any of these classes could break the OrderService, even though those classes have nothing to do with order processing.
This is called a "train wreck" or "dot-chaining": one object reaching through several others to get what it wants. You start with a Customer, go into their ShoppingCart, peek into its internal list of CartItems, grab the first one, extract the Product, and finally get the Price.
Here is the full function that uses this pattern:
void displayFirstItemPrice(const Customer& customer) {
Money price = customer.getShoppingCart()
.getItems()[0]
.getProduct()
.getPrice();
cout << "Price of the first item: " << price.getAmount() << endl;
}This approach works. It compiles. It runs. But it smells bad, and it will cause real pain as your system evolves.
What's Wrong With This?
1. High Coupling
The OrderService method is now tightly coupled to the entire internal structure of the customer and their cart.
- If ShoppingCart changes how it stores items (e.g., using a Map instead of a List)
- If CartItem renames its getProduct() method
- Or if Product evolves to store pricing in a new way…
Boom. Your OrderService code breaks. Even though it had nothing to do with those decisions.
2. Encapsulation Violation
You are reaching deep into object internals, violating encapsulation at multiple levels.
- Customer exposes its ShoppingCart
- ShoppingCart exposes its internal list
- You assume the structure of that list
- And even dig through CartItem and Product just to get a price
Every layer of reach is a layer of exposed internals. The whole point of encapsulation is to hide these details, and this code tears down every wall.
3. Maintenance Nightmare
Imagine this change: You switch from using a Money wrapper to a BigDecimal for price representation in Product.
Now, every part of your codebase that dot-chased its way to product.getPrice() must be updated.
4. Testability Issues
Testing displayFirstItemPrice() becomes a mocking marathon.
To test it in isolation, you'd need to mock:
- A Customer
- That returns a ShoppingCart
- That returns a List
- That returns a CartItem
- That returns a Product
- That returns a Price
One function. Six mocks. That is exhausting, fragile, and a clear sign something is wrong with the design.
Historical Background
The Law of Demeter was developed at: Northeastern University during the Demeter Project.
The primary researcher was: Karl Lieberherr.
The idea emerged from observing large software systems.
Teams repeatedly encountered:
High Coupling
Fragile Code
Difficult RefactoringA common root cause:
Objects knew too much about other objects.
The solution:
Limit what an object is allowed to know.
Hence: Principle of Least Knowledge
The Law of Demeter (LoD)
The Law of Demeter, also knows as the Principle of Least Knowledge, was formulated in 1987 at Northeastern University during work on the Demeter project. Despite its age, it remains one of the most practical guidelines for writing maintainable object-oriented code.
An object should only talk to its immediate friends.
Or more formally:
A method should only invoke methods belonging to:
1 Itself
this->calculateTax();2 Its Direct Members
repository.save();3 Objects Created Inside The Method
Logger logger;
logger.log();4 Parameters Passed To The Method
void process(Customer& customer)
{
customer.activate();
}Not arbitrary objects deep inside other objects.
That's it. In plain terms: don't reach through one object to get to another.
If you call a.getB().getC().doSomething(), you are violating LoD because you are reaching through B to talk to C. You should only talking to A, and let A figure out how to get the job done.
The Intuition
Suppose you work in a company.
You need a document from Finance.
Bad approach:
Employee
-> Manager
-> Director
-> VP
-> CEO
-> Finance TeamYou navigate the entire organizational hierarchy.
Manager Structure
Director Structure
VP Structure
CEO StructureAny organizational change breaks the process.
Better:
Employee
-> ManagerThe manger gets the document.
The employee only knows:
Its Direct ContactThe employee doesn't need to know:
- Directors
- VPs
- Finance structure
This is exactly the Law of Demeter.
The Core Idea
The principle can be summarized as:
An object should know as little as possible about the internal structure of other objects.
Notice:
It does NOT say:
Don't use object.It does NOT say:
Don't call methods.It says:
Don't depend on internalsThe Famous Rule
A method should only talk to:
Self
Direct FriendsNot:
Friends of FriendsThink:
Don't talk to strangersUnderstanding Object Graphs
Consider:
class City
{
};
class Address
{
public:
City city;
};
class Customer
{
public:
Address address;
};
class Order
{
public:
Customer customer;
};Usage:
order.customer.address.city.getName();Object graph:
Order
|
Customer
|
Address
|
CityThe caller traverse:
4 Levels DeepThis creates strong coupling.
Train Wreck Code
One of the easiest Demeter violations to recognize.
Example:
order
.getCustomer()
.getAddress()
.getCity()
.getName();Looks like:
--------->a train.
Hence the nickname:
Train Wreck
Another example:
user
.getProfile()
.getSettings()
.getTheme()
.getColor();The caller depends on:
Profile
Settings
ThemeHuge coupling.
Why Train Wrecks Are Dangerous
Suppose:
Addressbecomes:
LocationNow:
order
.getCustomer()
.getAddress()breaks everywhere.
A single internal change causes:
Massive Ripple EffectsThis is fragile design.
Better Design:
Instead of:
order
.getCustomer()
.getAddress()
.getCityName();Provide behavior:
class Customer
{
public:
std::string getCityName() const;
};Usage:
order
.getCustomer()
.getCityName();Now:
Address Structure HiddenEncapsulation preserved.
Tell, Don't Ask
The Law of Demeter naturally leads to another major design principle:
Tell, Don't Ask
Bad:
if (order.getCustomer()
.getMembership()
.getLevel() == GOLD)
{
discount = 20;
}The caller extracts data.
Then make decisions.
Better:
if (order.isEligibleForGoldDiscount())
{
discount = 20;
}The object owns the decision.
The caller simply asks for behavior.
This creates better object-oriented design.
Fluent APIs: An Important Exception
Many students become confused here.
Consider:
query
.select("*")
.where("id=10")
.orderBy("name")
.execute();Looks like a train wreck.
But it usually isn't.
Why?
Because:
select()
where()
orderBy()return:
*thisthe same object.
You are not traversing:
Object A
-> Object B
-> Object C
-> Object DYou are repeatedly operating on:
Object ATherefore fluent APIs are usually acceptable.
Refactoring with LoD in Mind
Let's rewrite the e-commerce example in a cleaner, more respectful way. The strategy is to push the responsibility down to the classes that own the data. Each class will expose a meaningful method instead of exposing its internals.
Step 1: Add a method to ShoppingCart
The ShoppingCar knows about its items. So it should be the one to answer questions about them.
class ShoppingCart {
private:
vector<CartItem> items;
public:
// ... constructor, other methods ...
Money getFirstItemPrice() const {
if (items.empty()) return Money::ZERO;
return items[0].getProduct().getPrice();
}
};Notice that ShoppingCart still reaches into CartItem and Product. That is fine here because ShoppingCart owns the items. The chain stays within the cart's own responsibility boundary. The important thing is that external callers no longer need to know about these internals.
Step 2: Add a method to Customer
The Customer owns the ShoppingCart, so it should be the one to delegate cart-related queries.
class Customer {
private:
ShoppingCart shoppingCart;
public:
// ... constructor, other methods ...
Money getFirstCartItemPrice() const {
return shoppingCart.getFirstItemPrice();
}
};Step 3: Update the OrderService
Now the OrderService only talks to its direct friend: the Customer.
void displayFirstItemPrice(const Customer& customer) {
Money price = customer.getFirstCartItemPrice();
cout << "Price of the first item: " << price.getAmount() << endl;
}Much better. Now the dependency chain looks completely different:
OrderService only talks to Customer. Customer only talks to ShoppingCart. Each layer hides the next one. Now OrderService does not care about:
- How the cart stores items
- What a CartItem contains
- How the Product holds its price
It just asks the Customer for what it needs, and the Customer delegates to the objects it owns. This is the Law of Demeter at work.
Benefits of Law of Demeter
Low Coupling
Each class depends only on its immediate collaborators. Code changes in one place don't ripple across your codebase. When ShoppingCart changes its internal storage from a List to a Map, only ShoppingCart needs updating.
Better Encapsulation
Each class handles its own logic. No external code peeks into internals. Objects expose meaningful behaviors ("give me the first item price") instead of raw structure ("give me your list so I can dig through it").
Easier Refactoring
You can evolve internal implementations without affecting consumers. If Product changes how it stores pricing, only CartItem and ShoppingCart need to adapt. The OrderService remains untouched.
Improved Testability
Fewer mocks are needed. To test the refactored displayFirstItemPrice, you only need to mock Customer and have it return a Money value. No more six-layer mock chains.
Cleaner APIs
Public methods become expressive, intentional, and meaningful. Instead of forcing callers to navigate your object graph, you provide clear entry points that describe what the caller actually wants.
Leave a comment
Your email address will not be published. Required fields are marked *
