CrackedRuby CrackedRuby

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:

  1. O itself - the object can call its own methods
  2. Parameters of M - methods can call methods on objects passed as arguments
  3. Objects created within M - methods can call methods on objects they instantiate
  4. 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