Overview
The Law of Demeter (LoD), also known as the Principle of Least Knowledge, defines constraints on how objects communicate within object-oriented systems. Introduced by Karl Lieberherr and Ian Holland at Northeastern University in 1987, the principle emerged from work on the Demeter Project, which focused on adaptive programming.
The law establishes that an object should interact only with objects in its immediate scope: its own methods, objects it creates, objects passed as parameters, and objects held in instance variables. This restriction prevents objects from navigating through chains of associations, which creates tight coupling between distant parts of a system.
Consider a violation example where a customer object retrieves a wallet from a person object, then extracts payment from that wallet:
# Violation: traversing multiple object relationships
customer.person.wallet.extract_payment(amount)
The customer object reaches through the person object to access the wallet, creating dependencies on both the person's structure (having a wallet) and the wallet's interface (having an extract_payment method). Changes to either intermediate object break the customer's code.
The Law of Demeter addresses this by requiring objects to request services from direct collaborators rather than navigating their internal structure. This principle appears in discussions of object-oriented design alongside concepts like encapsulation and information hiding, but focuses specifically on method call chains and object collaboration patterns.
Key Principles
The Law of Demeter states that a method M of object O should invoke methods only on:
- O itself - the object can call its own methods
- Parameters of M - methods can call methods on objects passed as arguments
- Objects created within M - methods can call methods on objects they instantiate
- Direct component objects of O - methods can call methods on objects stored in instance variables
This constraint translates to the rule: "Only talk to your immediate friends, don't talk to strangers." An object qualifies as a stranger if accessing it requires traversing another object's internal structure.
The principle prohibits method chains that traverse multiple object boundaries. Each dot in a chain like object.component.subcomponent.method represents a dependency on intermediate object structures. The calling code must understand not only the final method's interface but also the structure of every intermediate object.
Consider the structural difference:
# Violates Law of Demeter - three levels of traversal
user.address.street.name
# Adheres to Law of Demeter - single method call
user.street_name
The violation creates dependencies on the existence of an address object, its structure containing a street object, and the street object having a name attribute. The conforming version encapsulates these relationships inside the user object, which handles the traversal internally.
The principle applies strictly to method calls on return values. While object.method1.method2 violates the law by calling a method on the return value of another method, fluent interfaces that return self remain acceptable. The distinction matters because fluent interfaces maintain the same object reference throughout the chain:
# Fluent interface - returns self, no violation
query.where(status: 'active').order(:name).limit(10)
# Law of Demeter violation - different objects in chain
order.customer.address.postal_code
Method chaining on the same object preserves the principle's intent by avoiding dependencies on intermediate object structures. Each method in the chain operates on the same object rather than reaching into collaborator internals.
The law does not prohibit accessing attributes or methods of objects created within the current method. Local variables and newly instantiated objects remain accessible:
def process_order(items)
# Allowed - local variable created in method
calculator = PriceCalculator.new
calculator.add_items(items)
calculator.calculate_total
end
This exemption recognizes that objects created within a method's scope do not represent external dependencies requiring encapsulation.
Ruby Implementation
Ruby's metaprogramming capabilities and delegation features provide several patterns for implementing Law of Demeter compliance. The delegate method from Forwardable creates forwarding methods that hide internal object structures:
require 'forwardable'
class Order
extend Forwardable
def_delegators :@customer, :name, :email
def_delegators :@shipping_address, :street, :city, :postal_code
def initialize(customer, shipping_address)
@customer = customer
@shipping_address = shipping_address
end
end
# Usage
order = Order.new(customer, address)
order.name # Delegates to customer.name
order.postal_code # Delegates to shipping_address.postal_code
The Forwardable module generates methods that forward calls to collaborator objects, eliminating the need for clients to traverse object relationships. The delegating methods become part of the order's interface, hiding the internal delegation.
Rails provides delegate as a class-level macro that achieves the same result with different syntax:
class Order < ApplicationRecord
belongs_to :customer
belongs_to :shipping_address
delegate :name, :email, to: :customer
delegate :street, :city, :postal_code, to: :shipping_address, prefix: true
end
# Usage
order.name # Delegates to customer
order.shipping_address_postal_code # Prefixed delegation
The prefix option adds the association name to delegated method names, preventing naming conflicts when multiple associations provide similar attributes.
Manual delegation methods offer more control over the delegation behavior:
class Invoice
def initialize(order)
@order = order
end
def customer_name
@order.customer_name
end
def billing_address
@order.billing_address_formatted
end
def total_with_tax
@order.total * (1 + tax_rate)
end
private
def tax_rate
@order.region_tax_rate
end
end
Manual delegation allows transformation of delegated values, combination of multiple delegated calls, or addition of logic around the delegation. This approach provides flexibility when simple forwarding proves insufficient.
Ruby's method_missing enables dynamic delegation based on method patterns:
class OrderPresenter
def initialize(order)
@order = order
end
def method_missing(method_name, *args, &block)
if method_name.to_s.start_with?('customer_')
attribute = method_name.to_s.sub('customer_', '')
@order.customer.public_send(attribute, *args, &block)
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_name.to_s.start_with?('customer_') || super
end
end
# Usage
presenter = OrderPresenter.new(order)
presenter.customer_name # Dynamically delegates to order.customer.name
presenter.customer_email # Dynamically delegates to order.customer.email
The method_missing approach generates delegation methods on demand, reducing boilerplate when numerous delegations follow predictable patterns. The implementation must override respond_to_missing? to maintain proper method introspection behavior.
Value objects and wrapper classes encapsulate complex object graphs:
class ShippingInfo
def initialize(order)
@order = order
end
def recipient_name
@order.customer.full_name
end
def delivery_address
address = @order.shipping_address
"#{address.street}\n#{address.city}, #{address.state} #{address.postal_code}"
end
def estimated_delivery
@order.created_at + shipping_method.delivery_days.days
end
private
def shipping_method
@order.shipping_method
end
end
# Usage
shipping = ShippingInfo.new(order)
shipping.recipient_name # Encapsulates customer access
shipping.delivery_address # Encapsulates address formatting
Wrapper objects create focused interfaces around complex collaborator networks, hiding traversal logic behind domain-specific methods.
Practical Examples
An e-commerce order processing system demonstrates Law of Demeter violations and corrections. The initial implementation reaches through multiple object relationships:
# Violation example
class OrderProcessor
def process(order)
# Multiple levels of object traversal
customer_name = order.customer.profile.full_name
customer_email = order.customer.profile.email
shipping_cost = order.shipping_address.region.shipping_calculator.calculate(order.weight)
if order.customer.wallet.balance >= order.total + shipping_cost
order.customer.wallet.deduct(order.total + shipping_cost)
send_confirmation(customer_email, customer_name, order)
else
raise InsufficientFundsError
end
end
end
This code depends on the internal structure of order, customer, profile, wallet, shipping_address, region, and shipping_calculator objects. Changes to any intermediate object's structure break the processor.
Refactoring to follow the Law of Demeter pushes responsibilities into appropriate objects:
# Compliant implementation
class Order
def customer_name
customer.full_name
end
def customer_email
customer.email
end
def total_cost
subtotal + shipping_cost
end
def shipping_cost
shipping_address.calculate_shipping(weight)
end
end
class Customer
def full_name
profile.full_name
end
def email
profile.email
end
def can_afford?(amount)
wallet.balance >= amount
end
def charge(amount)
wallet.deduct(amount)
end
end
class ShippingAddress
def calculate_shipping(weight)
region.calculate_shipping(weight)
end
end
class OrderProcessor
def process(order)
if order.customer.can_afford?(order.total_cost)
order.customer.charge(order.total_cost)
send_confirmation(order.customer_email, order.customer_name, order)
else
raise InsufficientFundsError
end
end
end
The refactored version distributes knowledge across objects, with each object responsible for coordinating with its direct collaborators. The processor interacts only with the order and customer objects, which handle their internal complexity.
A reporting system shows how the law applies to data aggregation scenarios:
# Initial violation
class SalesReport
def generate(date_range)
orders = Order.where(created_at: date_range)
orders.map do |order|
{
order_id: order.id,
customer: order.customer.profile.display_name,
region: order.shipping_address.region.name,
category: order.items.first.product.category.name,
total: order.line_items.sum { |item| item.product.price * item.quantity }
}
end
end
end
The report reaches through multiple object layers to extract data, creating widespread dependencies. Refactoring introduces presenter objects and query methods:
class Order
def customer_display_name
customer.display_name
end
def shipping_region_name
shipping_address.region_name
end
def primary_category_name
items.first&.category_name
end
def calculated_total
line_items.sum(&:total)
end
end
class LineItem
def total
product.price * quantity
end
def category_name
product.category_name
end
end
class SalesReport
def generate(date_range)
orders = Order.where(created_at: date_range)
orders.map do |order|
{
order_id: order.id,
customer: order.customer_display_name,
region: order.shipping_region_name,
category: order.primary_category_name,
total: order.calculated_total
}
end
end
end
Each object exposes domain methods that encapsulate traversal logic. The report operates on order objects without knowledge of their internal structure or collaborator relationships.
A user notification system illustrates the principle with service objects:
# Before refactoring
class NotificationService
def notify_order_shipped(order)
user = order.customer.user
preferences = user.notification_preferences
if preferences.email_enabled
EmailService.send(user.email, order.shipping_confirmation_message)
end
if preferences.sms_enabled && user.phone.verified?
SmsService.send(user.phone.number, order.shipping_summary)
end
end
end
# After refactoring
class Order
def notify_shipped
customer.notify_shipping(shipping_notification_details)
end
def shipping_notification_details
{
message: shipping_confirmation_message,
summary: shipping_summary,
tracking_url: tracking_url
}
end
end
class Customer
def notify_shipping(details)
user.send_shipping_notification(details)
end
end
class User
def send_shipping_notification(details)
notification_preferences.send_shipping_notification(details, self)
end
end
class NotificationPreferences
def send_shipping_notification(details, user)
send_email(user.email, details[:message]) if email_enabled
send_sms(user.phone_number, details[:summary]) if sms_enabled && user.phone_verified?
end
private
def send_email(address, message)
EmailService.send(address, message)
end
def send_sms(number, message)
SmsService.send(number, message)
end
end
The refactored design chains responsibility through collaborators, with each object making decisions based on its own state and delegating to direct collaborators. The notification service becomes a simple facade that initiates the process.
Design Considerations
The Law of Demeter trades coupling for increased interface surface area. Following the law reduces dependencies on object structures but increases the number of methods exposed by objects. An object that previously relied on traversing collaborator relationships must now expose methods for every piece of information clients require.
This trade-off becomes apparent in systems with deep object graphs. A chain like user.account.subscription.plan.features requires four intermediate delegation methods to eliminate the violation. Each delegation method adds to the object's public interface without adding new functionality.
The principle's value increases with the volatility of collaborator structures. Systems where object relationships change frequently benefit from the decoupling the law provides. Objects insulated from collaborator structure changes remain stable when those structures evolve. In contrast, systems with stable, rarely changing structures may not justify the additional delegation methods.
Delegation methods can hide important structural information from developers. The method order.customer_email obscures the fact that the email belongs to a customer object, potentially leading to confusion about object responsibilities. Explicit traversal like order.customer.email makes the relationship clear, though at the cost of coupling.
The law conflicts with the goal of minimal public interfaces. Tell Don't Ask encourages pushing behavior into objects to minimize getters, while the Law of Demeter encourages adding delegation methods to reduce coupling. Resolving this conflict requires evaluating which concern matters more in specific contexts.
Query methods that return complex objects present particular challenges. A method returning a collection of objects with their own collaborators creates opportunities for law violations. The caller receives an object that enables further traversal:
# Returns collection enabling violations
def active_orders
Order.where(status: 'active')
end
# Client can violate law
active_orders.each do |order|
puts order.customer.email # Violation
end
Addressing this requires either returning specialized objects that expose safe interfaces or accepting that query method results enable some degree of traversal. Data transfer objects or presenters can wrap returned objects to control client access.
The law applies differently to internal versus external interfaces. Private methods within a class can violate the law when the alternative introduces excessive indirection. The law targets external coupling between objects, not internal implementation details. A private method that directly accesses collaborator internals affects only the enclosing class, not system-wide coupling.
Rails associations introduce particular considerations. The framework generates methods like order.customer that return associated objects, enabling law violations. Developers must consciously add delegation methods to prevent clients from traversing associations:
class Order < ApplicationRecord
belongs_to :customer
# Without delegation
# Clients do: order.customer.email (violation)
# With delegation
delegate :email, :name, to: :customer, prefix: true
# Clients do: order.customer_email (compliant)
end
The decision to add delegations depends on how widely the order object is used and whether the customer association represents implementation detail or essential domain structure.
Common Pitfalls
Developers frequently create delegation method explosions where objects expose dozens of forwarding methods to satisfy the law. An object with five collaborators, each exposing ten methods, requires fifty delegation methods to fully comply. This transforms the object into a massive facade with an unwieldy interface:
# Delegation explosion
class Order
delegate :name, :email, :phone, :address, :city, :state,
:postal_code, :country, :preferred_language, :timezone,
to: :customer
delegate :street, :city, :state, :postal_code, :country,
:latitude, :longitude, :is_residential, :delivery_instructions,
to: :shipping_address
# ... dozens more delegations
end
The solution involves identifying which information clients genuinely need and exposing only those pieces. Not every attribute of every collaborator warrants a delegation method. Objects should expose methods that support their responsibilities, not blindly forward everything collaborators offer.
Method chaining on fluent interfaces creates confusion about law violations. Developers see chains of method calls and assume violations without recognizing that fluent interfaces return the same object:
# Not a violation - returns self throughout
query.where(status: 'active')
.order(:created_at)
.limit(10)
# Violation - different objects
order.shipping_address.region.tax_rate
The distinction lies in whether each call returns a different object or the same object. Fluent interfaces maintain a single object reference, avoiding the coupling problems the law addresses.
Treating the law as absolute leads to awkward designs in scenarios where it doesn't apply well. Data structures and value objects often legitimately expose their internal structure for access. Applying the law strictly to these cases creates pointless indirection:
# Overly strict application to simple structure
class Point
def x_coordinate
@coordinates.x # Unnecessary indirection
end
def y_coordinate
@coordinates.y
end
end
# Reasonable approach
class Point
attr_reader :x, :y
end
Data structures designed primarily for data access rather than behavior don't benefit from the same encapsulation concerns as behaviorally rich objects.
Developers sometimes violate the law in test code without recognizing the impact. Test assertions that reach through object structures create brittle tests coupled to implementation details:
# Brittle test with law violation
expect(order.customer.address.postal_code).to eq('12345')
# Better test using exposed interface
expect(order.shipping_postal_code).to eq('12345')
Tests that traverse object structures break when those structures change, even if the object's logical behavior remains constant. Testing through an object's public interface creates more maintainable tests.
The law's interaction with null objects and optional associations causes problems. Delegation methods must handle cases where collaborators don't exist:
# Unsafe delegation
def customer_name
customer.name # Fails if customer is nil
end
# Safe delegation
def customer_name
customer&.name || 'Unknown'
end
# Alternative with null object
def customer_name
customer.name # Customer returns NullCustomer when nil
end
Every delegation method must account for the possibility of missing collaborators, adding error handling or default values throughout the delegation chain.
Developers confuse the Law of Demeter with avoiding method chaining entirely. The law prohibits specific types of chaining that traverse object boundaries, not all chaining:
# Acceptable - string methods returning strings
name.strip.downcase.gsub(/\s+/, '_')
# Violation - traversing distinct objects
order.customer.account.subscription_level
The former chains operations on the same conceptual object (a string), while the latter navigates through different domain objects. The distinction matters for understanding when the law applies.
Reference
| Concept | Description | Example |
|---|---|---|
| Direct Collaborator | Object in immediate scope of method | Self, parameters, instance variables, local objects |
| Stranger Object | Object requiring traversal to access | customer.address.street |
| Delegation Method | Method forwarding call to collaborator | def name; customer.name; end |
| Fluent Interface | Method chain returning self | query.where().order().limit() |
| Law Violation | Method call on method return value | object.method1.method2 |
Allowed Method Calls
| Source | Description | Valid Example |
|---|---|---|
| Self | Instance methods on current object | self.calculate_total |
| Parameter | Methods on passed arguments | def process(order); order.ship; end |
| Instance Variable | Methods on stored collaborators | @customer.notify |
| Local Object | Methods on locally created objects | calculator = new(); calculator.compute |
| Same Object Return | Chaining on self returns | self.step1.step2.step3 |
Delegation Techniques
| Approach | Use Case | Trade-off |
|---|---|---|
| Forwardable | Many simple delegations | Explicit method list required |
| Rails delegate | Rails applications | Framework dependency |
| Manual methods | Complex delegation logic | More code to maintain |
| method_missing | Dynamic delegation patterns | Runtime behavior, harder debugging |
| Wrapper objects | Encapsulating complex graphs | Additional objects |
Common Violation Patterns
| Pattern | Problem | Solution |
|---|---|---|
| Chain traversal | Multiple dots accessing nested objects | Add delegation methods |
| Collection iteration | Accessing nested attributes in loops | Query methods returning needed data |
| Conditional checks | Checking nested object state | Ask objects about their state |
| Data extraction | Pulling data from deep structures | Presenter or value objects |
| Test assertions | Testing internal object structure | Test through public interface |
Ruby Implementation Patterns
# Forwardable delegation
require 'forwardable'
class Order
extend Forwardable
def_delegators :@customer, :name, :email
end
# Rails delegation
class Order < ApplicationRecord
delegate :name, :email, to: :customer, prefix: true
end
# Manual delegation with logic
class Order
def customer_display_name
customer.name || 'Guest'
end
end
# Dynamic delegation
class Wrapper
def method_missing(method, *args)
@object.public_send(method, *args)
end
def respond_to_missing?(method, include_private = false)
@object.respond_to?(method) || super
end
end
Decision Framework
| Question | Yes Answer | No Answer |
|---|---|---|
| Does client need this information? | Create delegation method | Skip |
| Is structure volatile? | Follow law strictly | Consider direct access |
| Is it a value object? | May allow direct access | Follow law |
| Is it a fluent interface? | Chain allowed | Evaluate for violations |
| Is it a test? | Test through public interface | Reconsider test approach |
| Multiple delegations needed? | Consider wrapper object | Individual delegations |