Course Content
Software Architecture Fundamentals
Software Architecture Fundamentals
Method and Function Design
When designing a system at the low level, it is essential to structure methods and functions thoughtfully. A well-designed method ensures that each part of the system remains cohesive, easy to maintain, and works independently.
Poorly designed methods can introduce unnecessary complexity, increase dependencies between classes, and make the code difficult to debug or extend.
Good function design focuses on several principles:
- Clarity: methods should clearly express their purpose;
- Simplicity: each method should perform one task well;
- Low coupling: functions should minimize unnecessary dependencies between objects;
- Encapsulation: internal details should be hidden from other parts of the system.
Designing functions that adhere to these principles ensures the software remains flexible and easy to extend. However, even with these principles in mind, managing dependencies between objects can still be tricky.
This is where the Law of Demeter becomes essential—it provides specific guidance to limit how objects interact, ensuring cleaner communication and reducing potential issues from tight coupling.
What is the Law of Demeter?
The Law of Demeter, also known as the "principle of least knowledge," is a design guideline for developing software, particularly in object-oriented programming. Its main aim is to promote loose coupling between classes. Here's a clear formulation of the Law of Demeter:
Generally, this law is based on the following principles:
- Only interact with immediate friends: an object should only communicate with its direct collaborators (i.e., its immediate fields or dependencies) and not with the internals of those collaborators;
- Limit chain calls: avoid chains of method calls to access data or behavior of an object. Instead of calling a method on an object that returns another object and then calling a method on that returned object, use direct methods on the original object;
- Promote encapsulation: each class should encapsulate its state and behavior, exposing only what is necessary through well-defined interfaces. This reduces dependencies and helps prevent changes in one part of the code from affecting others.
Example
When a customer places an order, the OrderService
charges the payment through a PaymentGateway
.
Below is an example of what violating the Law of Demeter looks like:
class PaymentInfo: def __init__(self, account_number, balance): self.account_number = account_number self.balance = balance def has_sufficient_funds(self, amount): return self.balance >= amount def debit(self, amount): if self.has_sufficient_funds(amount): self.balance -= amount print(f"Debited {amount}. Remaining balance: {self.balance}.") else: raise ValueError("Insufficient funds.") class Customer: def __init__(self, name, payment_info): self.name = name self.payment_info = payment_info def get_payment_info(self): return self.payment_info class Order: def __init__(self, customer, total_amount): self.customer = customer self.total_amount = total_amount class OrderService: def process_order(self, order): # Violates Law of Demeter by navigating deep object chains. payment_info = order.customer.get_payment_info() # Accessing Customer if payment_info.has_sufficient_funds(order.total_amount): # Accessing PaymentInfo payment_info.debit(order.total_amount) # Accessing PaymentInfo again print("Order processed successfully!") else: print("Payment failed.") # Example usage payment_info = PaymentInfo(account_number="12345678", balance=100.00) customer = Customer(name="Jane Doe", payment_info=payment_info) order = Order(customer=customer, total_amount=50.00) order_service = OrderService() order_service.process_order(order)
There are some problems with this code:
- Deep navigation: the
OrderService
class directly accesses theCustomer
and then navigates toPaymentInfo
, leading to a violation of the Law of Demeter, which recommends that an object should only interact with its direct dependencies; - Tight coupling: the business logic within
OrderService
is tightly coupled with the structure of theCustomer
andPaymentInfo
classes. Any changes in the internal representation of these classes may require changes inOrderService
; - Reduced maintainability: this design can lead to difficulties in maintaining and extending the code, as it relies on the specific structure of objects rather than focusing on their behaviors.
class PaymentInfo: def __init__(self, account_number, balance): self.account_number = account_number self.balance = balance def has_sufficient_funds(self, amount): return self.balance >= amount def debit(self, amount): if self.has_sufficient_funds(amount): self.balance -= amount print(f"Debited {amount}. Remaining balance: {self.balance}.") else: raise ValueError("Insufficient funds.") class Customer: def __init__(self, name, payment_info): self.name = name self.payment_info = payment_info def can_make_payment(self, amount): """Method to check if the customer can make a payment.""" return self.payment_info.has_sufficient_funds(amount) def make_payment(self, amount): """Method to make a payment, handling the debit internally.""" self.payment_info.debit(amount) class Order: def __init__(self, customer, total_amount): self.customer = customer self.total_amount = total_amount class OrderService: def process_order(self, order): # Adheres to the Law of Demeter by interacting through Customer's methods if order.customer.can_make_payment(order.total_amount): order.customer.make_payment(order.total_amount) print("Order processed successfully!") else: print("Payment failed.") # Example usage payment_info = PaymentInfo(account_number="12345678", balance=100.00) customer = Customer(name="Jane Doe", payment_info=payment_info) order = Order(customer=customer, total_amount=50.00) order_service = OrderService() order_service.process_order(order)
This code is better for several reasons:
- Encapsulation: the
Customer
class encapsulates the logic for checking if a payment can be made and for making a payment. This hides the internal details of thePaymentInfo
class from theOrderService
; - Reduced coupling:
OrderService
now interacts withCustomer
through well-defined methods, minimizing dependencies on the internal structure ofPaymentInfo
. This adheres to the Law of Demeter and allows for easier changes in the internal classes without impacting others; - Improved maintainability: the design promotes easier maintenance and extension. If changes are needed in payment processing, they can be handled within the
PaymentInfo
orCustomer
classes without requiring updates toOrderService
.
Thanks for your feedback!