High-Level Techniques and Real-World Scenarios using Law of Demeter
In the first part of this series, we explored the foundational principles of the Law of Demeter (LoD). We looked at its benefits, simple examples, and how it can help you write cleaner, more maintainable code. Now, let’s delve deeper into advanced applications, examining real-world scenarios, design patterns, and professional-level techniques to implement this principle in complex systems. This article is intended for seasoned developers and architects seeking to elevate their craft.
Before diving into the advanced topics, let’s briefly recap the core principle:
In practice, this means reducing method chaining and ensuring objects communicate only with those they directly own or interact with, encapsulating internal details.
Check the introduction here: Introduction to the Law of Demeter.
Scenario: Designing a Banking System
Consider a banking application where you need to retrieve a customer's account balance through a transaction system. Here’s an example of violating the LoD:
Violation:
class Account:
def __init__(self, balance):
self.balance = balance
def get_balance(self):
return self.balance
class Customer:
def __init__(self, account):
self.account = account
def get_account(self):
return self.account
class TransactionSystem:
def get_customer_balance(self, customer):
# Violates LoD: Accesses nested objects directly
return customer.get_account().get_balance()
Here, the TransactionSystem accesses Customer's Account and retrieves the balance. This creates a tight coupling between TransactionSystem and Account.
Solution:
class Account:
def __init__(self, balance):
self.balance = balance
def get_balance(self):
return self.balance
class Customer:
def __init__(self, account):
self._account = account
def get_balance(self):
return self._account.get_balance()
class TransactionSystem:
def get_customer_balance(self, customer):
# Complies with LoD: Talks directly to Customer
return customer.get_balance()
Several design patterns naturally align with the principles of the Law of Demeter. Let’s explore a few:
The Facade pattern provides a simplified interface to a complex subsystem, preventing clients from accessing its internal components directly.
Example: System encapsulation
class SubsystemA:
def operation_a(self):
return "Operation A"
class SubsystemB:
def operation_b(self):
return "Operation B"
class Facade:
def __init__(self):
self._subsystem_a = SubsystemA()
self._subsystem_b = SubsystemB()
def perform_operations(self):
result_a = self._subsystem_a.operation_a()
result_b = self._subsystem_b.operation_b()
return f"{result_a} + {result_b}"
# Usage
facade = Facade()
print(facade.perform_operations())
The Facade hides the complexity of the subsystems, ensuring the client interacts only with the Facade interface.
A DTO is a simple object used to transfer data between layers of an application, reducing direct dependencies on internal structures.
Example: Using DTOs
class CustomerDTO:
def __init__(self, name, balance):
self.name = name
self.balance = balance
class CustomerService:
def get_customer_details(self):
# Simulate fetching data
return CustomerDTO("John Doe", 1500.0)
class Client:
def display_customer_details(self, service):
dto = service.get_customer_details()
print(f"Name: {dto.name}, Balance: ${dto.balance}")
# Usage
service = CustomerService()
client = Client()
client.display_customer_details(service)
Here, the Client only interacts with the CustomerDTO, not the underlying data structures.
While the Law of Demeter promotes encapsulation, excessive adherence can lead to over-encapsulation or unnecessary complexity. Here’s how to find balance:
Over-encapsulation can result in classes with too many delegation methods, making them bloated and harder to maintain. For instance:
class Address:
def __init__(self, city):
self.city = city
def get_city(self):
return self.city
class Customer:
def __init__(self, address):
self._address = address
def get_city(self):
return self._address.get_city()
# Over-encapsulation
customer = Customer(Address("New York"))
print(customer.get_city())
Here, Customer simply delegates the method without adding real value. It may be better to allow direct access to Address if practical.
Use tools like SonarQube or Checkstyle to identify violations of the Law of Demeter automatically. These tools can analyze method chains and flag potential issues.
Write unit tests to ensure objects interact only with their direct collaborators. Mock objects can help verify compliance.
from unittest.mock import Mock
class Service:
def fetch_data(self):
pass
class Client:
def __init__(self, service):
self._service = service
def get_data(self):
return self._service.fetch_data()
# Test
mock_service = Mock()
mock_service.fetch_data.return_value = "Mock Data"
client = Client(mock_service)
assert client.get_data() == "Mock Data"
The Law of Demeter is a cornerstone of clean code and maintainable design. By applying advanced techniques, leveraging design patterns, and finding the right balance, you can build robust systems that are easier to scale and evolve. Whether you’re designing APIs, microservices, or monoliths, the principles of LoD will help you minimize coupling and maximize cohesion.
Are you ready to take your understanding of the Law of Demeter to the next level?
Blog Statistics