DEV Community

Dylan
Dylan

Posted on

Applying Software Design Principles in an Order Management System

Introduction
In the world of software development, design principles are essential for creating robust, scalable, and maintainable applications. These principles guide developers in making architectural decisions that not only solve immediate problems but also optimize code quality and efficiency in the long term. In this article, we will explore some of the most important software design principles and how they apply to a real-world case: building an order management system.

Using a practical example, we will demonstrate how these principles are implemented in Python, showcasing the importance of separation of concerns, modularity, dependency inversion, and code testing throughout the process.

Software Design Principles
There are several fundamental software design principles, among which the following are most important:

-Single Responsibility Principle (SRP).
-Open/Closed Principle (OCP).
-Liskov Substitution Principle (LSP).
-Interface Segregation Principle (ISP).
-Dependency Inversion Principle (DIP).

Throughout this article, we will apply these principles to our order management system.

Case Study: Order Management System
We will develop a simple system to handle customer orders, process payments, and send confirmation emails. As we implement each part, we will apply the design principles mentioned above to demonstrate their effectiveness.

1) Single Responsibility Principle (SRP)
The Single Responsibility Principle states that each class should have only one reason to change, meaning it should be responsible for a single task. In our system, each class has a clear and unique responsibility:

  • Order: Represents the order, storing customer details, products, and the total amount.
  • OrderRepository: Responsible for saving orders to a file.
  • PaymentService: Processes payment for the order.
  • EmailService: Responsible for sending confirmation emails.

Each class has a single responsibility, making the code easier to maintain and understand.

2) Open/Closed Principle (OCP)
The Open/Closed Principle states that a class should be open for extension but closed for modification. This means we should be able to add new functionality without modifying existing classes.

In our example, if we wanted to add a new payment method, such as cryptocurrency payments, we could extend the PaymentService class by creating a new class like CryptoPaymentService, without modifying the original PaymentService class. This approach allows us to extend functionality without breaking the existing code.

3) Dependency Inversion Principle (DIP)
The Dependency Inversion Principle states that high-level modules should not depend on low-level modules, but rather on abstractions. Instead of having our OrderRepository class directly depend on a specific file implementation, we could create an interface or base class for order storage, such as OrderStorage. Then, OrderRepository would depend on this interface, making the system more flexible and extensible.

4) Modularity and Unit Testing
Modularity is a key principle that helps us organize the code so that different parts of the application can evolve independently. Additionally, unit testing ensures that each module behaves as expected.

In our system, classes are clearly separated, and each one can be independently tested. For example, we can write unit tests to verify that payment processing works correctly without needing to run the entire system.

Code Explanation
Let's now see how we've implemented these ideas in the code.

Class Order (Single Responsibility)

class Order:
    def __init__(self, customer_name, customer_email, items, total_amount):
        self.customer_name = customer_name  # Customer's name
        self.customer_email = customer_email  # Customer's email
        self.items = items  # List of products in the order
        self.total_amount = total_amount  # Total order amount
Enter fullscreen mode Exit fullscreen mode

Explanation:
The Order class has the sole responsibility of storing order details. It contains attributes like the customer's name, email, list of items, and total amount. In line with the Single Responsibility Principle (SRP), this class does nothing more than store this data.

Class OrderRepository (Open/Closed)

class OrderRepository:
    def save(self, order):
        with open("orders.txt", "a") as f:
            f.write(f"Customer: {order.customer_name}, Email: {order.customer_email}, Items: {order.items}, Total: ${order.total_amount}\n")
        print(f"Order for {order.customer_name} saved.")
Enter fullscreen mode Exit fullscreen mode

Explanation:
The OrderRepository class is responsible for saving orders to a text file. If we wanted to add another storage method, such as using a database, we would simply create a new class (like DatabaseOrderRepository) that also implements a save() method. This way, our code is open for extension (adding more repositories) but closed for modification.

Class PaymentService (Single Responsibility)

class PaymentService:
    def process_payment(self, order):
        print(f"Processing payment of ${order.total_amount} for {order.customer_name}. Payment Successful!")
Enter fullscreen mode Exit fullscreen mode

Explanation:
The PaymentService class is solely responsible for processing payments. It doesn't do other tasks like sending emails or saving data to a file, which is consistent with the Single Responsibility Principle (SRP).

Class EmailService (Single Responsibility)

class EmailService:
    def send_confirmation_email(self, order):
        print(f"Sending confirmation email to {order.customer_email} for the order of {len(order.items)} items.")
Enter fullscreen mode Exit fullscreen mode

Explanation:
The EmailService class is responsible only for sending confirmation emails. This modular approach keeps the code clean and easy to understand, aligning with the Single Responsibility Principle (SRP).

Unit Testing
Unit testing allows us to ensure that each component of the system works as expected. Using the unittest module, we write tests that validate the functionality of each class.

Example Test for OrderRepository Class:

import unittest
from unittest.mock import patch, mock_open
from order import Order
from order_repository import OrderRepository

class TestOrderRepository(unittest.TestCase):
    def test_save_order(self):
        order = Order("John Doe", "john@example.com", ["Laptop", "Mouse"], 1500.0)
        repo = OrderRepository()

        with patch("builtins.open", mock_open()) as mocked_file:
            repo.save(order)
            mocked_file.assert_called_once_with("orders.txt", "a")
            handle = mocked_file()
            handle.write.assert_called_once()
Enter fullscreen mode Exit fullscreen mode

Explanation:
In this test, we verify that the save() method of OrderRepository correctly opens the orders.txt file and saves the order details. We use mocking to simulate file interactions.

Conclusion
Software design is a critical discipline that guides the creation of scalable, maintainable, and flexible applications. In this article, we applied some of the most important software design principles to an order management system. From single responsibility to dependency inversion, these principles ensure that our system is easy to understand, test, and extend in the future.

Implementing these principles effectively not only improves code quality but also prepares developers to handle more complex challenges and build robust applications.

You can find the complete code for this project in my GitHub repository:
https://github.com/DylanTapiaVargas999/order_management_system

Top comments (0)