Python Refactoring - Real Case Scenario: Restaurant Ordering System

By JoeVu, at: Nov. 15, 2024, 1:07 p.m.

Estimated Reading Time: 8 min read

Python Refactoring - Real Case Scenario: Restaurant Ordering System
Python Refactoring - Real Case Scenario: Restaurant Ordering System

Python Refactoring - Real Case Scenario: Restaurant Ordering System

 

1. Introduction

Messy code, filled with nested if-else statements, often creeps into real-world applications as they grow in complexity. Over time, this makes systems harder to maintain, debug, and scale. In this post, we'll tackle such a scenario: a restaurant ordering system, and show how refactoring it with object-oriented design can simplify the logic and improve scalability.

 

2. The Problem

Our restaurant ordering system handles:

  • Discounts based on customer type (VIP, first-time, etc.).
     
  • Surcharges for delivery, dine-in, or takeout orders.
     
  • Taxes that vary by location.
     

Here’s the original messy implementation:

class Order:
    def __init__(self, order_type, customer_type, base_price, location=None):
        self.order_type = order_type
        self.customer_type = customer_type
        self.base_price = base_price
        self.location = location

    def calculate_total(self):
        total = self.base_price
        
        # Discounts based on customer type
        if self.customer_type == "VIP":
            total *= 0.9  # 10% discount
        elif self.customer_type == "first-time":
            total *= 0.85  # 15% discount

        # Surcharge based on order type
        if self.order_type == "delivery":
            if self.location == "urban":
                total += 5
            elif self.location == "rural":
                total += 10
        elif self.order_type == "dine-in":
            total += 2

        # Tax
        if self.location in ["urban", "rural"]:
            total *= 1.05
        elif self.order_type == "takeout":
            total *= 1.02
        
        # Additional conditions
        if self.customer_type == "VIP" and self.order_type == "dine-in":
            total -= 5  # VIPs get $5 off for dine-in

        return total

order = Order(order_type="delivery", customer_type="VIP", base_price=100, location="rural")
print(f"Total Order Price: ${order.calculate_total():.2f}")

 

Why This Code is Bad:

  1. Deeply Nested Logic:

    • The multiple levels of if-else statements make the code difficult to read and follow.
       
    • The logic is scattered throughout the method, making it hard to understand what the code does at a glance.
       
  2. Low Scalability:

    • Adding new customer types, discounts, or order types requires modifying this already-complicated method.
       
    • This approach violates the Open-Closed Principle (code should be open for extension but closed for modification).
       
  3. Violation of Single Responsibility:

    • The calculate_total method handles discounts, surcharges, and taxes in one place, leading to a "God method" that does too much.
       
    • Any change in one part of the logic risks breaking unrelated parts of the method.
       
  4. Difficult to Test:

    • Testing this function requires covering a wide range of conditions for customer type, order type, and location.
       
    • Any bug in one conditional block can cascade through the entire method.
       
  5. Poor Maintainability:

    • If a business rule changes (e.g., a new discount or tax rate), you must carefully locate and update the relevant part of the code.
       
    • The risk of introducing errors during such changes is high.
       
  6. Tight Coupling:

    • All the business logic is tightly coupled within the calculate_total method.
       
    • It cannot be reused elsewhere, such as in a reporting tool or another service.

 

3. The Refactor

To address the issues identified in the original implementation, we refactor the system using strategy patterns and object-oriented design. These improvements focus on breaking down the logic into reusable classes, following key principles such as modularity, encapsulation, and separation of concerns.
 

Key Improvements in the Refactor

1. Introduce Strategy Classes for Discounts

  • The discount logic is extracted into separate classes based on customer type (e.g., VIPDiscount, FirstTimeDiscount).
     
  • This removes the need for nested if-else conditions within the Order class.
     
  • Improvement: Each discount strategy now encapsulates its specific behavior, making it reusable and easy to extend.
     

2. Create Strategy Classes for Order Types

  • Surcharge calculations for delivery, dine-in, and takeout are moved to their own strategy classes (e.g., DeliveryOrder, DineInOrder).
     
  • These classes handle location-based logic (e.g., urban or rural delivery fees) without cluttering the main Order class.
     
  • Improvement: Adding a new order type, like "CateringOrder," only requires creating a new strategy class without modifying existing code.
     

3. Simplify the Core Order Class

  • The Order class now serves as a lightweight orchestrator:
    • It delegates discount calculation to a DiscountStrategy.
       
    • It delegates surcharge calculation to an OrderTypeStrategy.
       
    • It handles tax calculation based on the results of these strategies.
       
  • Improvement: The Order class is now clean, focused, and adheres to the Single Responsibility Principle.
     

4. Use Composition Over Inheritance

  • Instead of hardcoding business logic into a single class, the refactor uses composition, combining strategies dynamically.
     
  • Improvement: This makes the system flexible and easier to customize for different scenarios.


Refactored Code

class DiscountStrategy:
    """Base class for discount strategies."""
    def apply_discount(self, order):
        return order.base_price

class VIPDiscount(DiscountStrategy):
    def apply_discount(self, order):
        return order.base_price * 0.9  # 10% discount for VIP

class FirstTimeDiscount(DiscountStrategy):
    def apply_discount(self, order):
        return order.base_price * 0.85  # 15% discount for first-time customers

class NoDiscount(DiscountStrategy):
    def apply_discount(self, order):
        return order.base_price


class OrderTypeStrategy:
    """Base class for order type strategies."""
    def apply_surcharge(self, order):
        return 0

class DeliveryOrder(OrderTypeStrategy):
    def apply_surcharge(self, order):
        return 5 if order.location == "urban" else 10  # Delivery fee based on location

class DineInOrder(OrderTypeStrategy):
    def apply_surcharge(self, order):
        return 2  # Service charge for dine-in orders

class TakeoutOrder(OrderTypeStrategy):
    def apply_surcharge(self, order):
        return 0  # No surcharge for takeout


class Order:
    def __init__(self, base_price, discount_strategy, order_type_strategy, location=None):
        self.base_price = base_price
        self.discount_strategy = discount_strategy
        self.order_type_strategy = order_type_strategy
        self.location = location

    def calculate_total(self):
        # Delegate discount and surcharge calculations
        discounted_price = self.discount_strategy.apply_discount(self)
        surcharge = self.order_type_strategy.apply_surcharge(self)
        
        # Tax logic
        tax_rate = 0.05 if self.location in ["urban", "rural"] else 0.02
        tax = discounted_price * tax_rate

        # Total calculation
        return discounted_price + surcharge + tax

 

4. Why It’s Better

The refactored code provides several advantages:

  • Readability: Each responsibility is clearly defined in its class.
     
  • Scalability: Adding new discounts or order types is easy—just create new classes.
     
  • Testability: Individual strategies can be tested independently.
     
  • Maintenance: Changes to one part of the logic won’t affect others.

 

5. Conclusion

By refactoring the restaurant ordering system, we've turned a messy, nested if-else block into a modular, object-oriented design. This approach ensures the code is clean, scalable, and easier to maintain.

If your system is drowning in complexity, consider adopting similar design principles to simplify and future-proof it!

 


Related

Outsourcing Experience

[TIPS] Writing Better Code - Not a big deal

Read more
Python Django

Django Application Cleanup

Read more
Subscribe

Subscribe to our newsletter and never miss out lastest news.