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. 🤓
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. 🚀
For the first point, a method should only access its own class's methods:
Ruby1class Car 2 def start 3 check_fuel 4 ignite 5 end 6 7 private 8 9 def check_fuel 10 puts "Checking fuel level..." 11 end 12 13 def ignite 14 puts "Igniting the engine..." 15 end 16end
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.
Next, a method can interact with the objects it creates:
Ruby1class Library 2 def borrow_book(title) 3 book = Book.new(title) 4 book.issue 5 book 6 end 7end 8 9class Book 10 def initialize(title) 11 @title = title 12 end 13 14 def issue 15 puts "Book issued: #{@title}" 16 end 17end
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
. 📚
Continuing, let's look at interacting with objects passed as arguments:
Ruby1class Printer 2 def print(document) 3 document.send_to_printer 4 end 5end 6 7class Document 8 def send_to_printer 9 puts "Document is being printed..." 10 end 11end
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. 🖨️
Objects held in instance variables of a class can also be accessed:
Ruby1class House 2 def initialize 3 @door = Door.new 4 end 5 6 def lock_house 7 @door.close 8 end 9end 10 11class Door 12 def close 13 puts "Door is closed." 14 end 15end
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. 🏠
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:
Ruby1class TemperatureConverter 2 CONVERSION_FACTOR = 9.0 / 5.0 3 4 def celsius_to_fahrenheit(celsius) 5 (celsius * CONVERSION_FACTOR + 32).to_i 6 end 7end
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. 🌡️
Here's an example that violates the Law of Demeter:
Ruby1class Person 2 def initialize(address) 3 @address = address 4 end 5 6 def get_address_details 7 "Address: #{@address.first_name} #{@address.last_name}, #{@address.street}, " \ 8 "#{@address.city}, #{@address.country}, ZipCode: #{@address.zip_code}" 9 end 10end 11 12class Address 13 attr_reader :first_name, :last_name, :street, :city, :country, :zip_code 14 15 # Assume initialization for attributes here 16end
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.
Let's refactor the previous code to adhere to the Law of Demeter:
Ruby1class Person 2 def initialize(address) 3 @address = address 4 end 5 6 def get_address_details 7 @address.get_address_line 8 end 9end 10 11class Address 12 def get_address_line 13 "#{first_name} #{last_name}, #{street}, #{city}, #{country}, ZipCode: #{zip_code}" 14 end 15 16 private 17 18 # Assume private accessors or methods for attributes here 19end
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.
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! 🌟