Notice: This page requires JavaScript to function properly.
Please enable JavaScript in your browser settings or update your browser.
Method and Function Design | Low Level Design
Software Architecture Fundamentals
course content

Зміст курсу

Software Architecture Fundamentals

Software Architecture Fundamentals

1. Introduction to System Design
2. High Level Design
3. Low Level Design
4. Additional Design Approaches

bookMethod 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:

123456789101112131415161718192021222324252627282930313233343536373839404142434445
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)
copy

There are some problems with this code:

  • Deep navigation: the OrderService class directly accesses the Customer and then navigates to PaymentInfo, 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 the Customer and PaymentInfo classes. Any changes in the internal representation of these classes may require changes in OrderService;
  • 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.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
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)
copy

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 the PaymentInfo class from the OrderService;
  • Reduced coupling: OrderService now interacts with Customer through well-defined methods, minimizing dependencies on the internal structure of PaymentInfo. 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 or Customer classes without requiring updates to OrderService.
In the improved version of example code, which class encapsulates the payment logic?

In the improved version of example code, which class encapsulates the payment logic?

Виберіть правильну відповідь

Все було зрозуміло?

Як ми можемо покращити це?

Дякуємо за ваш відгук!

Секція 3. Розділ 2
some-alt