Applying Clean Code Principles with Ruby: Understanding the Law of Demeter

Introduction

Welcome to the third lesson of the "Applying Clean Code Principles" course. In our journey so far, we've discussed the importance of the DRY (Don't Repeat Yourself) principle to eliminate redundancy in code. We followed that with the KISS (Keep It Simple, Stupid) principle, which highlights the value of simplicity in software development. Today, our spotlight is on the Law of Demeter — a key guideline in object-oriented programming. By limiting the knowledge that an object has about other objects, this lesson will guide you in crafting more maintainable and modular code. 🤓

Understanding the Law of Demeter

The Law of Demeter suggests that an object should only communicate with its immediate collaborators, avoiding the entire system. By reducing dependency between parts, you'll find your code easier to maintain and scale. In simple terms, a method in a class should only call methods of:

  • The class itself
  • An object created within the method
  • An object passed as an argument to the method
  • An object held in an instance variable of the class
  • A class constant

With these principles, you control how parts of your application interact, leading to a more organized structure. Let's explore how this works with examples. 🚀

First Rule Example

For the first point, a method should only access its own class's methods:

class Car
  def start
    check_fuel
    ignite
  end

  private

  def check_fuel
    puts "Checking fuel level..."
  end

  def ignite
    puts "Igniting the engine..."
  end
end

In this example, the start method interacts solely with methods within the Car class itself. This shows how you maintain clear boundaries adhering to the Law of Demeter.

Second Rule Example

Next, a method can interact with the objects it creates:

class Library
  def borrow_book(title)
    book = Book.new(title)
    book.issue
    book
  end
end

class Book
  def initialize(title)
    @title = title
  end

  def issue
    puts "Book issued: #{@title}"
  end
end

Here, the Library class creates a Book and calls the issue method on it. This usage pattern complies with the Law of Demeter, where Library interacts with the newly created Book. 📚

Third Rule Example

Continuing, let's look at interacting with objects passed as arguments:

class Printer
  def print(document)
    document.send_to_printer
  end
end

class Document
  def send_to_printer
    puts "Document is being printed..."
  end
end

The Printer class method print communicates with the Document object passed as an argument, aligning with the Law of Demeter by limiting communication to direct method parameters. 🖨️

Fourth Rule Example

Objects held in instance variables of a class can also be accessed:

class House
  def initialize
    @door = Door.new
  end

  def lock_house
    @door.close
  end
end

class Door
  def close
    puts "Door is closed."
  end
end

In this example, the House class interacts with its @door through the lock_house method, showcasing compliance by interacting with an object it holds in an instance variable. 🏠

Fifth Rule Example

Finally, let's see a method interacting with constants. Constants should generally be used cautiously since they can lead to shared state issues in larger applications:

class TemperatureConverter
  CONVERSION_FACTOR = 9.0 / 5.0

  def celsius_to_fahrenheit(celsius)
    (celsius * CONVERSION_FACTOR + 32).to_i
  end
end

Here, CONVERSION_FACTOR is defined as a constant to indicate that it's a constant and to ensure correct calculations. Accessing constants like this is compliant with the Law of Demeter. 🌡️

Violation Example

Here's an example that violates the Law of Demeter:

class Person
  def initialize(address)
    @address = address
  end

  def get_address_details
    "Address: #{@address.first_name} #{@address.last_name}, #{@address.street}, " \
    "#{@address.city}, #{@address.country}, ZipCode: #{@address.zip_code}"
  end
end

class Address
  attr_reader :first_name, :last_name, :street, :city, :country, :zip_code

  # Assume initialization for attributes here
end

In this case, Person is directly accessing multiple fields through Address, leading to tight coupling. Person relies on the internal structure of Address, which might result in fragile code.

Refactored Example

Let's refactor the previous code to adhere to the Law of Demeter:

class Person
  def initialize(address)
    @address = address
  end

  def get_address_details
    @address.get_address_line
  end
end

class Address
  def get_address_line
    "#{first_name} #{last_name}, #{street}, #{city}, #{country}, ZipCode: #{zip_code}"
  end

  private

  # Assume private accessors or methods for attributes here
end

By encapsulating all the address details within the get_address_line method in the Address class, the dependency is minimized, and Person no longer accesses Address's internals directly.

Summary and Next Steps

The Law of Demeter plays a vital role in writing clean, modular code by ensuring objects only interact with their closest dependencies. By understanding and implementing these guidelines, you enhance the modularity and maintainability of your code. As you move on to the practice exercises, challenge yourself to apply these principles and evaluate your code's interactions. Keep these lessons in mind as essential steps toward mastering clean code! 🌟

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal