DEV Community

Cover image for Unraveling the Strategy Pattern: A Practical Case Study
Pedro Henrique
Pedro Henrique

Posted on

Unraveling the Strategy Pattern: A Practical Case Study

⚠️ Understanding the problem

Bicycle, maps and city

🚲 Wheels and Wonders: A Deliverer's Odyssey

Imagine you are the owner of a company that deals with deliveries through bicycles. In a small town, your company is able to operate seamlessly, with no reported issues or delays in deliveries. As your company began to grow, you decided to open a new branch in a slightly larger city.

🚳 Pedaling to the Future: When Bicycles Become Troubles

However, bicycle deliveries became a bit slower due to the increased distances between pickup and drop-off points. Soon, you realized that you should have handled this type of delivery with motorcycles or cars, and as a result, your company began to work with three types of transportation for deliveries.

πŸͺ² Great powers changes come great responsibilities challenges

Your company continued to grow, and the volume of deliveries became increasingly substantial. The cars had to travel twice to the drop-off point to complete a single delivery

To tackle this, you decided to implement a new type of transportation. Deliveries with a large volume are now handled by trucks, allowing you to successfully meet the demands

Delivery from Brazil to Portugal

But what if your company keeps growing and you need, for instance, to make deliveries from Brazil to Portugal? You will again realize that you need to implement other types of transportation, such as planes or ships.

Do you agree that, depending on the type of delivery you're working on, the approach β€” that is, the delivery strategy β€” will be different, but the action will always be the same (sending a product from one place to another)?


πŸ€Ήβ€β™‚οΈ Juggling Code: Balancing Conditions in Code

If we were to implement a code that describes the delivery process of your company, we could represent it this way:

class Destination:
    """
    Class that represents a destination

    Attributes:
        - distance: distance to the destination in meters
        - is_connected_by_road: True if the destination is connected by road
        - is_connected_by_sea: True if the destination is connected by sea
    """
    def __init__(self, distance, is_connected_by_road, is_connected_by_sea):
        self.distance = distance
        self.is_connected_by_road = is_connected_by_road
        self.is_connected_by_sea = is_connected_by_sea


class Order:
    """
    Class that represents an order

    Attributes:
        - weight: weight of the order in kilograms
        - volume: volume of the order in cubic meters
    """
    def __init__(self, weight, volume):
        self.weight = weight
        self.volume = volume


class DeliverySystem:
    """
    Class that represents a delivery system

    Methods:
        - execute_delivery: executes the delivery of an order to a destination
            - order: order to be delivered
            - destination: destination of the order
    """
    def execute_delivery(self, order: Order, destination: Destination):
        if destination.is_connected_by_road:
            if order.weight < 2 and order.volume < 2 and destination.distance < 1000:
                print("Delivering by bike")
            elif order.weight < 10 and order.volume < 10 and destination.distance < 10000:
                print("Delivering by car")
            else:
                print("Delivering by truck")
        elif destination.is_connected_by_sea and destination.distance < 7000 * 10^3:
            if order.weight < 10 and order.volume < 10:
                print("Delivering by boat")
            else:
                print("Delivering by ship")
        else:
            print("Delivering by plane")
Enter fullscreen mode Exit fullscreen mode

Note that there is a certain number of conditional statements that may increase if new delivery methods emerge or if the delivery rules change for any of the specified approaches

🌧️ The Saga of the Cyclist Who Didn't Want to Get Wet

Also, there is another issue: imagine there is a delivery that could be done by bicycle, but it's raining. It would be impractical for a cyclist to make the delivery; therefore, it should be carried out by a car. However, the code doesn't allow this because the item doesn't meet the delivery requirements for cars.


πŸŠβ€β™€οΈ When you play the game of code, you embrace the Strategy Pattern or you drown in conditionals

In other words, the code has some issues, and adding more elements to it could lead to a lot of headaches if not thoroughly tested.

However, how do we solve that while still avoiding the nesting of conditional statements and keeping the code clean and easy to maintain?

πŸ›Ÿ Strategy: the buoy to avoid drowning

Buoy inside if-sea

This is where the Design Pattern called Strategy comes into play, presenting an effective solution to the problem. By using the Strategy, we can delegate to our main class the task of receiving the desired approach to perform the delivery, simply by invoking the corresponding method to carry out the transportation.

This way, we transfer the responsibility of choosing the delivery strategy outside the main class, providing the flexibility to be decided even by the customer.

The structure and implementation of the Strategy for the problem are quite simple. The first step is to define an interface with methods and attributes that will serve as a model for delivery approaches.

Image description

In other words, each delivery approach will be named as a Strategy and they must implement the methods of the interface according to their own behavior.

The main class, called Context, will receive which Strategy will perform the delivery and simply call the delivery method.

Now, moving on to the code implementation, we have the following:

  • Interface implementation
from abc import ABC, abstractmethod

class DeliveryStrategyInterface(ABC):
    """
    Interface that represents a delivery strategy
    """
    @abstractmethod
    def execute(self, order: Order, destination: Destination):
        pass
Enter fullscreen mode Exit fullscreen mode
  • Implementation of each Strategy that can be used in the application
class BikeDelivery(DeliveryStrategyInterface):
    """
    Strategy that represents a bike delivery
    """
    def execute(self, order: Order, destination: Destination):
        print("Delivering by bike")


class CarDelivery(DeliveryStrategyInterface):
    """
    Strategy that represents a car delivery
    """
    def execute(self, order: Order, destination: Destination):
        print("Delivering by car")


class TruckDelivery(DeliveryStrategyInterface):
    """
    Strategy that represents a truck delivery
    """
    def execute(self, order: Order, destination: Destination):
        print("Delivering by truck")


class BoatDelivery(DeliveryStrategyInterface):
    """
    Strategy that represents a boat delivery
    """
    def execute(self, order: Order, destination: Destination):
        print("Delivering by boat")


class ShipDelivery(DeliveryStrategyInterface):
    """
    Strategy that represents a ship delivery
    """
    def execute(self, order: Order, destination: Destination):
        print("Delivering by ship")


class PlaneDelivery(DeliveryStrategyInterface):
    """
    Strategy that represents a plane delivery
    """
    def execute(self, order: Order, destination: Destination):
        print("Delivering by plane")
Enter fullscreen mode Exit fullscreen mode
  • Implementation of the main class (Context)
class DeliverySystemContext:
    """
    Class that represents a delivery system context

    Attributes:
        - delivery_strategy: delivery strategy to be used
    """
    delivery_strategy = None

    def set_strategy(self, delivery_strategy: DeliveryStrategyInterface):
        self.delivery_strategy = delivery_strategy

    def execute_delivery(self, order: Order, destination: Destination):
        if self.delivery_strategy is None:
            raise Exception("Delivery strategy is not set")

        self.delivery_strategy.execute(order, destination)
Enter fullscreen mode Exit fullscreen mode

πŸͺ– Is an army of classes really necessary?

Note that, despite introducing a larger quantity of code, this implementation is significantly easier to maintain and allows a simplified addition/removal of delivery methods.

Suppose your company decides to incorporate a new means of transportation for deliveries; in this scenario, you can simply implement a new class that inherits from the interface without the need to alter the code of the main class.

This approach not only makes our code more readable and reduces the chances of bugs, but also adheres to the Open-Closed principle of SOLID: our main class is open for extension but closed for modification.

This contrasts with the previous implementation, where each addition or removal of a delivery method would require extensive changes and revisions throughout the entire code.


🏁 In the end…

In summary, the Strategy is a powerful approach when dealing with code that can change its behavior depending on certain factors; hence, it is labeled as a behavioral Design Pattern.

A key consideration to keep in mind is that each strategy within this pattern can be dynamically configured during code execution. This eliminates the need for unnecessary validations or excessive nesting of conditionals, significantly reducing the risk of introducing bugs into the system.


πŸ«‚ Credits

Design Pattern: Strategy

Images and Icons by Vectorportal.com

Top comments (2)

Collapse
 
jackson541 profile image
Jackson Alves

Very nice article!

Collapse
 
sreno77 profile image
Scott Reno

This is very informative. Thanks!