Welcome to the third lesson of the "Applying Clean Code Principles in C++" course. On our journey so far, we've discussed the importance of the DRY (Don't Repeat Yourself) principle in eliminating 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. 🤓
The Law of Demeter was introduced by Karl J. Lieberherr and 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 should only call methods of:
- Its own class
- An object it creates
- An object passed as an argument
- An object stored in an instance variable
- A static resource
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:
C++1class Car { 2public: 3 void start() { 4 checkFuel(); 5 ignite(); 6 } 7 8private: 9 void checkFuel() { 10 std::cout << "Checking fuel level..." << std::endl; 11 } 12 13 void ignite() { 14 std::cout << "Igniting the engine..." << std::endl; 15 } 16};
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:
C++1class Book { 2public: 3 Book(const std::string& title) : title(title) {} 4 5 void issue() { 6 std::cout << "Book issued: " << title << std::endl; 7 } 8 9private: 10 std::string title; 11}; 12 13class Library { 14public: 15 Book borrowBook(const std::string& title) { 16 Book book(title); 17 book.issue(); 18 return book; 19 } 20};
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:
C++1class Document { 2public: 3 void sendToPrinter() { 4 std::cout << "Document is being printed..." << std::endl; 5 } 6}; 7 8class Printer { 9public: 10 void print(Document& document) { 11 document.sendToPrinter(); 12 } 13};
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:
C++1class Door { 2public: 3 void close() { 4 std::cout << "Door is closed." << std::endl; 5 } 6}; 7 8class House { 9public: 10 House() : door() {} 11 12 void lockHouse() { 13 door.close(); 14 } 15 16private: 17 Door door; 18};
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. 🏠
Finally, let's see a method interacting with static fields. While static fields are convenient, they should generally be used cautiously since they can lead to shared state issues in larger applications:
C++1class TemperatureConverter { 2public: 3 static int celsiusToFahrenheit(double celsius) { 4 return static_cast<int>((celsius * conversionFactor) + 32); 5 } 6 7private: 8 static constexpr double conversionFactor = 9.0 / 5.0; 9};
Here, conversionFactor
is defined as a constant to ensure correct calculations. Accessing static fields in this manner is compliant with the Law of Demeter. 🌡️
Here's an example that violates the Law of Demeter:
C++1class Address { 2public: 3 // Assume initialization of all fields here 4 std::string getFirstName() { return firstName; } 5 std::string getLastName() { return lastName; } 6 std::string getStreet() { return street; } 7 std::string getCity() { return city; } 8 std::string getCountry() { return country; } 9 std::string getZipCode() { return zipCode; } 10 11private: 12 std::string firstName, lastName, street, city, country, zipCode; 13}; 14 15class Person { 16public: 17 Person() : address() {} 18 19 std::string getAddressDetails() { 20 return "Address: " + address.getFirstName() + " " + address.getLastName() + ", " + 21 address.getStreet() + ", " + address.getCity() + ", " + 22 address.getCountry() + ", ZipCode: " + address.getZipCode(); 23 } 24 25private: 26 Address address; 27};
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:
C++1class Address { 2public: 3 // Assume initialization of all fields here 4 std::string getAddressLine() { 5 return firstName + " " + lastName + ", " + street + ", " + 6 city + ", " + country + ", ZipCode: " + zipCode; 7 } 8 9private: 10 std::string firstName, lastName, street, city, country, zipCode; 11}; 12 13class Person { 14public: 15 Person() : address() {} 16 17 std::string getAddressDetails() { 18 return address.getAddressLine(); 19 } 20 21private: 22 Address address; 23};
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.
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! 🌟