Python Refactoring - Real Case Scenario: Restaurant Ordering System
By JoeVu, at: 13:07 Ngày 15 tháng 11 năm 2024
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:
-
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.
- The multiple levels of
-
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).
- Adding new customer types, discounts, or order types requires modifying this already-complicated method.
-
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.
- The
-
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.
- Testing this function requires covering a wide range of conditions for customer type, order type, and location.
-
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.
- 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.
-
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.
- All the business logic is tightly coupled within the
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 theOrder
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
, andtakeout
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.
- It delegates discount calculation to a
- 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!