Lesson 3
Applying the Law of Demeter in Kotlin
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 focus 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 was introduced by Karl J. Lieberherr and suggests that an object should only communicate with its immediate collaborators, avoiding reliance on the entire system. By reducing dependency between parts, you'll find your code easier to maintain and scale. In simple terms, a method X of class C should only call methods of:

  • Class C itself
  • An object created by X
  • An object passed as an argument to X
  • An object held in an instance variable of C
  • A companion object

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:

Kotlin
1class Car { 2 fun start() { 3 checkFuel() 4 ignite() 5 } 6 7 private fun checkFuel() { 8 println("Checking fuel level...") 9 } 10 11 private fun ignite() { 12 println("Igniting the engine...") 13 } 14}

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

Second Rule Example

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

Kotlin
1class Library { 2 fun borrowBook(title: String): Book { 3 val book = Book(title) 4 book.issue() 5 return book 6 } 7} 8 9class Book(private val title: String) { 10 fun issue() { 11 println("Book issued: $title") 12 } 13}

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:

Kotlin
1class Printer { 2 fun print(document: Document) { 3 document.sendToPrinter() 4 } 5} 6 7class Document { 8 fun sendToPrinter() { 9 println("Document is being printed...") 10 } 11}

The Printer class's 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:

Kotlin
1class House { 2 private val door = Door() 3 4 fun lockHouse() { 5 door.close() 6 } 7} 8 9class Door { 10 fun close() { 11 println("Door is closed.") 12 } 13}

In this example, the House class interacts with its door through the lockHouse 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 companion objects. While companions can act like static fields, they should be used cautiously:

Kotlin
1class TemperatureConverter { 2 companion object { 3 private const val conversionFactor = 9.0 / 5.0 4 } 5 6 fun celsiusToFahrenheit(celsius: Int): Int { 7 return (celsius * conversionFactor).toInt() + 32 8 } 9}

Here, conversionFactor resides within a companion object to indicate that it's a constant and to ensure correct calculations. Accessing companion objects like this is compliant with the Law of Demeter. 🌡️

Violation Example

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

Kotlin
1class Person(private val address: Address) { 2 fun getAddressDetails(): String { 3 return "Address: ${address.firstName} ${address.lastName}, " + 4 "${address.street}, ${address.city}, " + 5 "${address.country}, ZipCode: ${address.zipCode}" 6 } 7} 8 9class Address( 10 val firstName: String, 11 val lastName: String, 12 val street: String, 13 val city: String, 14 val country: String, 15 val zipCode: String 16)

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:

Kotlin
1class Person(private val address: Address) { 2 fun getAddressDetails(): String { 3 return address.getAddressLine() 4 } 5} 6 7class Address( 8 private val firstName: String, 9 private val lastName: String, 10 private val street: String, 11 private val city: String, 12 private val country: String, 13 private val zipCode: String 14) { 15 fun getAddressLine(): String { 16 return "$firstName $lastName, $street, $city, $country, ZipCode: $zipCode" 17 } 18}

By encapsulating all the address details within the getAddressLine 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! 🌟

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.