DEV Community

Cover image for Become a Python Design Strategist using the Strategy Pattern
Horace FAYOMI
Horace FAYOMI

Posted on

Become a Python Design Strategist using the Strategy Pattern

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

But, what is the problem with that implementation?

There are mainly 2 problems here:

  1. 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.
  2. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
swalkthewalk profile image
Shannon Walker

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])