In object-oriented programming (OOP), adhering to principles that promote loose coupling and encapsulation is essential for building maintainable, scalable systems. One such principle is the Law of Demeter, also known as the Principle of Least Knowledge. This principle helps prevent objects from knowing too much about the internal workings of other objects, which is a common issue in OOP.
In this blog post, we’ll explore the Law of Demeter and demonstrate how you can apply it in a Ruby on Rails application. By the end, you’ll understand how this principle can simplify your code and make your application easier to maintain.
What is the Law of Demeter in Object-Oriented Programming?
In object-oriented programming, the Law of Demeter (LoD) can be summarized with the following rule: “An object should only talk to its immediate friends and not strangers.”
To put this more clearly, an object should only invoke methods on:
- Itself.
- Its direct dependencies (fields or instance variables).
- Objects passed in as arguments.
- Objects it creates.
This rule reduces the dependencies between objects, making the code less fragile and easier to maintain. When an object reaches through another object to access a third object’s method (e.g., order.customer.address.city
), it creates tight coupling. This kind of code is harder to test, extend, and debug.
The Problem: Breaking the Law of Demeter
Let’s look at a common example in a Rails application where the Law of Demeter is violated. Suppose we have three models: Order
, Customer
, and Address
. An Order
belongs to a Customer
, and a Customer
has an Address
. In the code below, the Order
is calculating a discounted shipping price based on the customer’s address.
1
2
3
4
5
6
class Order
def discounted_shipping_price(discount_code)
coupon = Coupon.new(discount_code)
coupon.discount(customer.address.shipping_price)
end
end
This implementation violates the Law of Demeter. The Order
object is reaching through Customer
to access Address
and then fetching the shipping_price
. This leads to a situation where Order
knows too much about the internal structure of Customer
and Address
, resulting in tight coupling between these objects.
Refactoring: Applying the Law of Demeter
To fix this, we can move the responsibility of calculating the discounted shipping price to the Customer
model. This way, the Order
only interacts with its direct collaborator, Customer
, and the Customer
manages its relationship with the Address
.
Here’s the refactored code:
1
2
3
4
5
6
7
8
9
10
11
12
class Order
def discounted_shipping_price(discount_code)
customer.discounted_shipping_price(discount_code)
end
end
class Customer
def discounted_shipping_price(discount_code)
coupon = Coupon.new(discount_code)
coupon.discount(address.shipping_price)
end
end
Now, the Order
class no longer directly accesses the Address
. Instead, it delegates the calculation to the Customer
class. This reduces coupling and makes the system more maintainable.
Using delegate
for Even Cleaner Code
We can take this refactoring a step further by using Rails’ built-in delegate
method. The delegate
method allows us to automatically forward method calls from one object to another, making our code cleaner and easier to read.
Here’s how we can refactor the previous example using delegate
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Order
belongs_to :customer
# Delegate discounted_shipping_price to customer
delegate :discounted_shipping_price, to: :customer
end
class Customer
has_one :address
def discounted_shipping_price(discount_code)
coupon = Coupon.new(discount_code)
coupon.discount(address.shipping_price)
end
end
How Does This Work?
- In
Order
: The linedelegate :discounted_shipping_price, to: :customer
allows theOrder
to delegate the call toCustomer
without having to manually define a method. - In
Customer
: Thediscounted_shipping_price
method handles the interaction withAddress
and applies the discount logic.
This refactor makes the code cleaner, while still respecting the Law of Demeter.
Why Follow the Law of Demeter?
1. Loose Coupling
By following the Law of Demeter, objects don’t need to know the internal details of other objects. This reduces the likelihood that a change in one part of your system will break another part. For example, if we change how Address
is structured, Order
won’t need to change.
2. Encapsulation
Each class is responsible for its own logic. In our example, the Customer
class is responsible for managing the address and shipping price, while Order
focuses on the order details. This clear separation of concerns makes the code easier to understand and maintain.
3. Better Maintainability
Less knowledge about other objects’ structures means fewer dependencies. This makes the code more flexible and easier to refactor in the future. If the business logic around shipping changes, you only need to update the Customer
model without impacting Order
.
The Law of Demeter is a key principle in object-oriented programming that helps reduce coupling and promote better code organization. In Rails applications, adhering to this principle results in more maintainable and scalable systems. By refactoring your code to delegate responsibilities and limit an object’s knowledge of others, you create a cleaner and more robust application architecture.
In our example with Order
, Customer
, and Address
, we’ve seen how easy it is to break the Law of Demeter and how using Rails’ delegate
method can help fix it. As your Rails application grows, applying the Law of Demeter will ensure your codebase remains clean, maintainable, and easy to extend.
Key Takeaways:
- The Law of Demeter encourages objects to only communicate with their immediate collaborators, reducing dependencies.
- Violating this law leads to tightly coupled code, which is harder to maintain.
- Using Rails’
delegate
method makes it easy to apply this principle, resulting in cleaner, more modular code.