Strategy Design Pattern

Introduction

Strategy is a behavioural design pattern that helps us define a family of interchangeable algorithms and encapsulate each one as a separate class. Depending on the specific context, we can select algorithms at runtime. So the main goal of the strategy pattern is to promote flexibility and maintainability by separating the algorithms from the client code. This way, we can easily add new algorithms without modifying the existing client code.

Real-world analogy

Suppose we have multiple options for travelling like driving a car, taking a bus, riding a bicycle, or walking. Each mode has its own advantages and we can choose these different ways based on factors like distance, traffic, weather, or time constraints. In this analogy, the different modes of transportation represent different strategies (algorithms) and factors influencing our decision represent the context that affects the selection of a strategy.

Key components and structure

Strategy pattern components and UML structure

  • Strategy: A common interface for all the concrete strategies which declares a method that Context uses to execute a strategy.
  • Concrete Strategies: Implement the Strategy interface, where each concrete strategy encapsulates a specific algorithm.
  • Context: Maintain a reference to one of the Concrete Strategy objects and use this object to delegate the algorithm execution via the Strategy interface.
  • Client: Creates a specific strategy object and passes it to the set method (setStrategy) of the Context. This will help the client to replace the strategy associated with the context at runtime.

Note: When there is a need to run the algorithm, it is the responsibility of the Context to call the execution method on the associated strategy object each time. Here Context doesn’t know what type of strategy it works with or how the algorithm is executed.

Problem statement 

Suppose, we are developing an e-commerce platform that provides a feature to display products in a sorted order based on different criteria.

  • Our solution should dynamically select and apply the appropriate sorting strategy based on various criteria like price, ratings, or name.
  • Integration of new sorting algorithms should be easy, without tightly coupling them to the existing codebase.

Steps to implement strategy pattern

  1. Identify the varying behaviour: Determine the specific behaviour that needs to be encapsulated and made interchangeable. This behaviour should have multiple variations.
  2. Define the strategy interface: Create an interface or abstract class that defines the contract for all the strategies. This contract should specify the methods that each strategy must implement.
  3. Implement the concrete strategies: Create concrete classes that implement the strategy interface. Each class represents a specific variation or algorithm of the behaviour. Implement the necessary logic for each strategy within its respective class.
  4. Integrate the strategies: Create a context class that will maintain a reference to one of the strategy objects. This class should work with all strategies via the strategy interface and doesn’t know about the concrete class of a strategy.
  5. Provide a mechanism to set the strategy: Add a set method in the context class to set or change the current strategy. This method should accept an instance of the strategy interface and update the internal reference of the context class accordingly.
  6. Invoke the strategy: Use the selected strategy to perform the desired behaviour. We can do this by calling the methods defined in the strategy interface.
  7. Test and iterate: Test the implementation to ensure that the strategies can be easily switched. If new strategies need to be added in the future, create new classes that implement the strategy interface and integrate them into the existing codebase.

Solution of the problem statement using strategy pattern

Implementation code Java

Step 1: We first define a Product class with product attributes, constructor and get methods. 

class Product {
    private String name;
    private double price;
    private double rating;

    public Product(String name, double price, double rating) {
        this.name = name;
        this.price = price;
        this.rating = rating;
    }

    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }

    public double getRating() {
        return rating;
    }
}

Step 2: In the above problem, sorting is a varying behaviour. So we should separate this from the client implementation. For this, we first define the SortingStrategy interface, which defines the contract for concrete sorting strategies.

interface SortingStrategy {
    void sort(List<Product> products);
}

Step 3: Now, we implement each sorting criterion (price, rating, name) as a separate concrete strategy class that implements the SortingStrategy interface.

class SortByPrice implements SortingStrategy {
    @Override
    public void sort(List<Product> products) {
        // logic to sort products based on price
    }
}

class SortByRating implements SortingStrategy {
    @Override
    public void sort(List<Product> products) {
        // logic to sort products based on rating
    }
}

class SortByName implements SortingStrategy {
    @Override
    public void sort(List<Product> products) {
        // logic to sort products based on name
    }
}

Step 4: Now we define the context class (ECommercePlatform) that manages the list of products and performs the sorting based on the selected sorting strategy. In this class:

  • We store the reference of one of the concrete sorting strategies.
  • We also define the setSortingStrategy() method to set or change the current sorting strategy.
class ECommercePlatform {
    private List<Product> products;
    private SortingStrategy sortingStrategy;

    public ECommercePlatform() {
        products = new ArrayList<>();
    }

    public void addProduct(Product product) {
        products.add(product);
    }

    public void setSortingStrategy(SortingStrategy sortingStrategy) {
        this.sortingStrategy = sortingStrategy;
    }

    public void performSorting() {
        sortingStrategy.sort(products);
    }

    public void displayProducts() {
        for (Product product : products) {
            System.out.println(product.getName());
        }
    }
    
    // Other operations
}

Step 5: In the main method, we create an instance of the ECommercePlatform, add products to the platform, set the desired sorting strategy, perform the sorting, and display the sorted products. Here, we are dynamically changing the sorting strategy by calling setSortingStrategy() before performing the sorting operation.

public class Main {
    public static void main(String[] args) {
        // Create an instance of the e-commerce platform
        ECommercePlatform platform = new ECommercePlatform();

        // Add products to the platform
        platform.addProduct(new Product("Laptop", 999.99, 100, 4.5));
        platform.addProduct(new Product("Phone", 799.99, 200, 4.7));
        platform.addProduct(new Product("Headphones", 199.99, 150, 4.3));
        platform.addProduct(new Product("Camera", 1499.99, 50, 4.8));

        // Sort products by price
        platform.setSortingStrategy(new SortByPrice());
        platform.performSorting();
        System.out.println("Sorted by Price:");
        platform.displayProducts();
        System.out.println();

        // Sort products by rating
        platform.setSortingStrategy(new SortByRating());
        platform.performSorting();
        System.out.println("Sorted by Rating:");
        platform.displayProducts();
        System.out.println();

        // Sort products alphabetically
        platform.setSortingStrategy(new SortByName());
        platform.performSorting();
        System.out.println("Sorted Name:");
        platform.displayProducts();
    }
}

Key takeaway from the above implementation

  • This will promote code reusability because each sorting logic is encapsulated within individual strategy classes. So we can use the same sorting strategies in some other context.
  • We can easily add new sorting strategies by implementing the SortingStrategy interface. This promotes code extensibility without modifying the existing codebase. In other words, this will follow the Open-Closed Principle because ECommercePlatform class is open for extension (adding new strategies) and closed for modification (existing code doesn't need to change).
  • Each strategy class can have its own internal state. This will provide more flexibility in implementing complex sorting algorithms.

When should we apply strategy pattern?

  • When we have multiple algorithms that can be used for a specific task, and we want to switch between them dynamically.
  • When we want to encapsulate individual algorithms or behaviours into separate classes.
  • When we have a lot of similar classes that only differ in the way they execute some behaviour.
  • When we have complex conditional statements in a class that dictate different variants of the same algorithms based on some conditions. Using a strategy pattern, we can remove such conditional statements and implement all algorithms into separate classes. Now instead of implementing all variants of the algorithm, the original object delegates execution to one of these objects.

Pros and Cons of Strategy Pattern

Pros:

  • Promote Single Responsibility and Open Closed Principle.
  • Each strategy focuses on a specific algorithm, which makes it easier to understand, maintain, and extend.
  • We can easily alter the behaviour of the object at runtime by associating the object with different strategies which can perform specific tasks in different ways.
  • Promote code reusability because we can use the same strategies across different components or modules.
  • We can test each strategy independently because they are encapsulated in a separate class. 
  • Helps us isolate the code, data, and dependencies of various algorithms from the rest of the code.

Cons:

  • We are adding classes for each strategy. This can increase the number of classes in the codebase.
  • To choose the correct strategy, the client must know about the differences between strategies.
  • This pattern is beneficial when dealing with complex behaviours that require various types of implementations. If we have a simple scenario with only a few fixed behaviours, implementing the strategy pattern can introduce unnecessary complexity.

Relation with Other Patterns

  • Bridge, State and Strategy patterns are based on the idea of composition i.e. delegating tasks to other objects. But they are used to solve different problems.
  • We can use strategy pattern within the factory method pattern to decide the appropriate strategy. Here we can encapsulate the creation of the strategy objects based on certain conditions.
  • We can use strategy pattern with the decorator pattern. Here is an idea: Strategy pattern will define different strategies, and decorator pattern will provides a way to wrap the object with different strategies.
  • Template method pattern can apply the idea of strategy pattern to use different algorithms at various steps. Each step can have a strategy associated with it, and the template method controls the overall flow while delegating the specific operations to the strategies. 
  • We can use both command and strategy pattern to parameterize an object with some action. But the main difference here is the types of problem they solve: We use command pattern to convert an operation into an object. This will help us defer operation execution, store the history of commands and send command to remove services. But strategy pattern describes different ways of doing the same thing and helps us swap these algorithms within a single context class.

If you have any queries or feedback, please write us at contact@enjoyalgorithms.com. Enjoy learning, Enjoy OOPS!

More from EnjoyAlgorithms

Self-paced Courses and Blogs