Hey Python Lover 😁, today you’ll become a Python strategist 😌 because we're going to illustrate with python a design pattern called the strategy pattern. The strategy pattern is a behavioral pattern that allows you to define a family of algorithms or a family of functions, and encapsulate them as objects to make them interchangeable. It helps having a code easy to change and then to maintain.
Let's imagine that you're building a food delivery application and you want to support many restaurants.
But each restaurant has its own API and you have to interact with each restaurant's API to perform diverse actions such as getting the menu, ordering food, or checking the status of an order. That means, we need to write custom code for each restaurant.
First, let's see how we can implement this using a simple and naive approach without bothering to think about the design or the architecture of our code:
from enum import Enum
class Restaurant(Enum):
GERANIUM = "Geranium. Copenhagen"
ATOMIX = "ATOMIX. New York"
LE_CLARENCE = "Le Clarence, Paris, France"
class Food(Enum):
SHANGHAI_DUMPLINGS = "Shanghai dumplings"
COQ_AU_VIN = "Coq au Vin"
CHEESEBURGER_FRIES = "Cheeseburger fries"
CHAWARMA = "Chawarma"
NAZI_GORENG = "Nasi goreng"
BIBIMBAP = "Bibimbap"
restaurants_map_foods = {
Restaurant.GERANIUM: [Food.BIBIMBAP, Food.SHANGHAI_DUMPLINGS, Food.COQ_AU_VIN],
Restaurant.ATOMIX: [Food.CHEESEBURGER_FRIES, Food.CHAWARMA, Food.NAZI_GORENG],
Restaurant.LE_CLARENCE: [Food.COQ_AU_VIN, Food.BIBIMBAP]
}
def get_restaurant_food_menu(restaurant: Restaurant) -> list[Food]:
"""Get the list of food available for a given restaurant."""
if restaurant == Restaurant.ATOMIX:
print(f".. call ATOMIX API to get it available food menu")
elif restaurant == Restaurant.LE_CLARENCE:
print(f".. call LE_CLARENCE API to get it available food menu")
elif restaurant == Restaurant.GERANIUM:
print(f".. call GERANIUM API to get it available food menu")
return restaurants_map_foods[restaurant]
def order_food(restaurant: Restaurant, food: Food) -> int:
"""Order food from a restaurant.
:returns: A integer representing the order ID.
"""
if restaurant == Restaurant.ATOMIX:
print(f".. send notification to ATOMIX restaurant API to order [{food}]")
elif restaurant == Restaurant.LE_CLARENCE:
print(f".. send notification to LE_CLARENCE restaurant API to order [{food}]")
elif restaurant == Restaurant.GERANIUM:
print(f".. send notification to GERANIUM restaurant API to order [{food}]")
order_id = 45 # Supposed to be retrieved from the right restaurant API call
return order_id
def check_food_order_status(restaurant: Restaurant, order_id: int) -> bool:
"""Check of the food is ready for delivery.
:returns: `True` if the food is ready for delivery, and `False` otherwise.
"""
if restaurant == Restaurant.ATOMIX:
print(f"... call ATOMIX API to check order status [{order_id}]")
elif restaurant == Restaurant.LE_CLARENCE:
print(f"... call LE_CLARENCE API to check order status [{order_id}]")
elif restaurant == Restaurant.GERANIUM:
print(f"... call GERANIUM API to check order status [{order_id}]")
food_is_ready = True # Supposed to be retrieved from the right restaurant API call
return food_is_ready
if __name__ == "__main__":
menu = get_restaurant_food_menu(Restaurant.ATOMIX)
print('- menu: ', menu)
order_food(Restaurant.ATOMIX, menu[0])
food_is_ready = check_food_order_status(Restaurant.ATOMIX, menu[0])
print('- food_is_ready: ', food_is_ready)
We have some Enum classes to keep information about the restaurants and Foods, and we have 3 functions:
-
get_restaurant_food_menu
to get the menu of a restaurant -
order_food
to order the food in a given restaurant -
check_food_order_status
to check if the food is ready. In the real world, we could create a task that will call that method each 2min, or any other timeframe periodically to check if the food is ready to be delivered and let the customer knows.
And, for simplicity reasons, we have just printed what the code is supposed to do rather than real API calls.
The code works well and you should see this output:
.. call ATOMIX API to get it available food menu
- menu: [<Food.CHEESEBURGER_FRIES: 'Cheeseburger fries'>, <Food.CHAWARMA: 'Chawarma'>, <Food.NAZI_GORENG: 'Nasi goreng'>]
.. send notification to ATOMIX restaurant API to order [Food.CHEESEBURGER_FRIES]
... call ATOMIX API to check order status [Food.CHEESEBURGER_FRIES]
- food_is_ready: True
But, what is the problem with that implementation?
There are mainly 2 problems here:
- The logic related to each restaurant is scattered. So to add or modify the code related to a restaurant we need to update all functions. That’s really bad.
- Each of these functions contains too much code
Remember I have simplified the code to just print what the code is supposed to do, but for each restaurant we have to write the code to handle the feature like calling the right API, handling errors, etc. Try to imagine the size of each of these functions if there were 10, 100 restaurants
And here is where the Strategy pattern enters the game.
Each time someone orders food from a given restaurant, we can consider that he is performing the action of ordering a food
using a given strategy. And the strategy here is the restaurant.
So each strategy and all the functions related to it, which constitute its family will be encapsulated into a class.
Let’s do that. First, we create our restaurant strategy class interface or abstract class. Let’s name it RestaurantManager
:
from abc import ABC, abstractmethod
class RestaurantManager(ABC):
"""Restaurants manager base class."""
restaurant: Restaurant = None
@abstractmethod
def get_food_menu(self) -> list[Food]:
"""Get the list of food available for a given restaurant."""
pass
@abstractmethod
def order_food(self, food: Food) -> int:
"""Order food from a restaurant.
:returns: A integer representing the order ID.
"""
pass
@abstractmethod
def check_food_order_status(self, order_id: int) -> bool:
"""Check of the food is ready for delivery.
:returns: `True` if the food is ready for delivery, and `False` otherwise.
"""
pass
Now each restaurant code will be grouped inside its own class which just inherits from the base RestaurantManager
and should implement all the required methods. And our business class doesn’t care which restaurant, or I mean, which strategy he is implementing, it just performs the needed action.
And to create a strategy for a restaurant, we just have to create a subclass of RestaurantManager
.
Here is the code for that ATOMIX
restaurant:
class AtomixRestaurantManager(RestaurantManager):
"""ATOMIX Restaurant Manager."""
restaurant: Restaurant = Restaurant.ATOMIX
def get_food_menu(self) -> list[Food]:
print(f".. call ATOMIX API to get it available food menu")
return restaurants_map_foods[self.restaurant]
def order_food(self, food: Food) -> int:
print(f".. send notification to ATOMIX API to order [{food}]")
order_id = 45 # Supposed to be retrieved from the right restaurant API call
return order_id
def check_food_order_status(self, order_id: int) -> bool:
print(f"... call ATOMIX API to check order status [{order_id}]")
food_is_ready = True # Supposed to be retrieved from the right restaurant API call
return food_is_ready
And we can add a business logic class that receives the strategy (the restaurant) :
class FoodOrderProcessor:
def __init__(self, restaurant_manager: RestaurantManager):
self.restaurant_manager = restaurant_manager
def get_food_menu(self):
return self.restaurant_manager.get_food_menu()
def order_food(self, food: Food) -> int:
return self.restaurant_manager.order_food(food)
def check_food_order_status(self, order_id: int) -> bool:
return self.restaurant_manager.check_food_order_status(order_id)
And here is our new __main__
code:
if __name__ == "__main__":
order_processor = FoodOrderProcessor(restaurant_manager=AtomixRestaurantManager())
menu = order_processor.get_food_menu()
print('- menu: ', menu)
order_processor.order_food(menu[0])
food_is_ready = order_processor.check_food_order_status(menu[0])
print('- food_is_ready: ', food_is_ready)
You can test it, it should still work.
Now, it’s easy to add a new restaurant or a new strategy. Let’s add for geranium
restaurant:
class GeraniumRestaurantManager(RestaurantManager):
"""Geranium Restaurant Manager."""
restaurant: Restaurant = Restaurant.GERANIUM
def get_food_menu(self) -> list[Food]:
print(f".. call GERANIUM API to get it available food menu")
return restaurants_map_foods[self.restaurant]
def order_food(self, food: Food) -> int:
print(f".. send notification to GERANIUM API to order [{food}]")
order_id = 45 # Supposed to be retrieved from the right restaurant API call
return order_id
def check_food_order_status(self, order_id: int) -> bool:
print(f"... call GERANIUM API to check order status [{order_id}]")
food_is_ready = True # Supposed to be retrieved from the right restaurant API call
return food_is_ready
And to change the strategy, we just have to replace the restaurant_manager
attribute of our business class with GeraniumRestaurantManager()
instead of AtomixRestaurantManager()
previously, and the remaining code is the same:
if __name__ == "__main__":
order_processor = FoodOrderProcessor(restaurant_manager=GeraniumRestaurantManager())
... # Remaining code
And that's it! We've successfully implemented the strategy pattern in Python to create interchangeable restaurant gateways for our delivery product.
Congratulations, you’re now a Python strategist 😉 😎.
I hope you found this explanation of the strategy pattern helpful. Remember, the strategy pattern is just one of many design patterns that can make your code more maintainable.
A video version of this tutorial is available here. Feel free to check it out and I see you in the next post. Take care.
Top comments (1)
Thank you.
I believe you made an error as the orderprocessor.order_food should return an order id that is then the argument to checking status (here menu[0] is the check food order status input).
food_is_ready =order_processor.check_food_order_status(menu[0])