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…


This content originally appeared on DEV Community and was authored by Horace FAYOMI

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:

  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

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.


This content originally appeared on DEV Community and was authored by Horace FAYOMI


Print Share Comment Cite Upload Translate Updates
APA

Horace FAYOMI | Sciencx (2023-03-12T19:16:37+00:00) Become a Python Design Strategist using the Strategy Pattern. Retrieved from https://www.scien.cx/2023/03/12/become-a-python-design-strategist-using-the-strategy-pattern/

MLA
" » Become a Python Design Strategist using the Strategy Pattern." Horace FAYOMI | Sciencx - Sunday March 12, 2023, https://www.scien.cx/2023/03/12/become-a-python-design-strategist-using-the-strategy-pattern/
HARVARD
Horace FAYOMI | Sciencx Sunday March 12, 2023 » Become a Python Design Strategist using the Strategy Pattern., viewed ,<https://www.scien.cx/2023/03/12/become-a-python-design-strategist-using-the-strategy-pattern/>
VANCOUVER
Horace FAYOMI | Sciencx - » Become a Python Design Strategist using the Strategy Pattern. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2023/03/12/become-a-python-design-strategist-using-the-strategy-pattern/
CHICAGO
" » Become a Python Design Strategist using the Strategy Pattern." Horace FAYOMI | Sciencx - Accessed . https://www.scien.cx/2023/03/12/become-a-python-design-strategist-using-the-strategy-pattern/
IEEE
" » Become a Python Design Strategist using the Strategy Pattern." Horace FAYOMI | Sciencx [Online]. Available: https://www.scien.cx/2023/03/12/become-a-python-design-strategist-using-the-strategy-pattern/. [Accessed: ]
rf:citation
» Become a Python Design Strategist using the Strategy Pattern | Horace FAYOMI | Sciencx | https://www.scien.cx/2023/03/12/become-a-python-design-strategist-using-the-strategy-pattern/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.