Blog Cover

Strategy Pattern: solving problems selectively at scale

Author profile image
Aitor Alonso

Jun 26, 2022

5 min read

The Strategy pattern is a behavioral design pattern that lets you define a family of algorithms and use them selectively to solve the same problem. Therefore, strategy pattern is best used with problems that can be resolved in different ways, and we might need to use different algorithms for different situations. Examples of those are navigating from point A to point B, or integrating multiple third-party APIs to execute the same operation (e.g. being able to make a payment using multiple payment providers).

The problem

Let's imagine we are developing an urban-navigation app. We implement a routing algorithm to let people go from point A (e.g. their home) to point B (e.g. their work). Initially, it calculates the fastest route between the two points by car, and works very well. Later, we decide to implement routing using bikes, and right after that, we decide to implement routing using public transport. We start implementing subways, and later on we end integrating trains, trams, buses, etc.

While from a business perspective the app was a success, the technical part caused you many headaches. As you can imagine, the routing algorithm is very complex at this point, and it is hard to test. The Routing class, the responsible for the route calculation, has grow to a point that it's imposible to maintain. And new developers coming to the project complains that implementing a new feature or fixing a simple bug requires a lot of time and effort, and generates a lot of merge conflicts.

Just a stock photo to liven up the reading

Applying strategy pattern to solve the problem

To apply the strategy pattern we take a class that does something definite in multiple ways and separate all of these procedures into discrete classes called strategies.

The original class, called context, references one of the strategies via an attribute. The context delegates the work to the connected strategy class, rather than executing it directly.

The context isn't liable for choosing a proper strategy to do the job. Instead, the client passes to the context the desired strategy to be executed. The context has not insight about strategies, as it interacts with all strategies through the same generic interface, that exposes only one method to trigger the algorithm encapsulated in the selected strategy.

In this way, the context becomes independent of the concrete strategy, therefore you can add new ones or modify existing ones without changing the context code or other strategies. This is an example application of one S.O.L.I.D. principles: Dependency Inversion Principle.

Coming back to our example, each routing algorithm from our navigation app can be extracted to its own class, and each class implements the same interface that exposes a calculateRoute method. This method will always receive the same parameters (origin and destination), and will always return the same object (lets say a Route object) regardless of the programmed algorithm. It implements an interface after all.

A quick view to the pseudocode

The above could be implemented as:

interface RoutingStrategy {
  calculateRoute(origin: Point, destination: Point): Route;
}

class CarRoutingStrategy implements RoutingStrategy {
  calculateRoute(origin: Point, destination: Point): Route {
    // calculate the fastest route using car
    return new Route();
  }
}

class BikeRoutingStrategy implements RoutingStrategy {
  calculateRoute(origin: Point, destination: Point): Route {
    // calculate the fastest route using bike
    return new Route();
  }
}

class BusRoutingStrategy implements RoutingStrategy {
  calculateRoute(origin: Point, destination: Point): Route {
    // calculate the fastest route using bus
    return new Route();
  }
}

class RoutingContext {
  private strategy: RoutingStrategy;

  public setStrategy(strategy: RoutingStrategy) {
    this.strategy = strategy;
  }

  public executeStrategy(origin: Point, destination: Point): Route {
    return this.strategy.calculateRoute(origin, destination);
  }
}

class ExampleApplication {
  public static main() {
    const context = new RoutingContext();
    context.setStrategy(new CarRoutingStrategy());
    const route = context.executeStrategy(new Point(0, 0), new Point(10, 10));
    console.log(route);
  }
}

Pros and cons of the strategy pattern

Applying the strategy pattern has, as everything in life, its pros and cons.

Pros are:

  • We can change algorithms used within a class at runtime.
  • We can isolate the implementation details of an algorithm from the code that uses it.
  • We can replace inheritance with composition.
  • It complies with S.O.L.I.D. principles such as the the Open/Close principle, as we can introduce new strategies without changing the context.

And cons are:

  • If we only have a few algorithms and they change infrequently, it would be an overengineering to complicate the program with the classes and interfaces that requires this pattern.
  • Clients need to know the differences between strategies in order to choose the right one.
  • Many modern programming languages allows us to implement different versions of an algorithm in a set of anonymous functions. We can then use these functions as we would with a strategy class, but without bloating our codebase with extra classes and interfaces.

Summary

To sum it up, there are a few situations where implementing a strategy pattern could be a good idea, but it is up to you to identify if this will be the best option for your project, or if it will overcomplicated it instead. Some good applications could be:

  • When you want to use different implementations of algorithms in an class and want to switch from one algorithm to another at runtime.
  • When your class contains large conditional statements that switch between different variants of the same algorithm.
  • When you want to isolate the business logic of the context class from the implementation details of the algorithms (strategies) that may not be important in the context of that logic.
  • When you have many similar classes that differ only in the way they perform certain behaviors, refactoring them into a strategy pattern could be a good idea to reduce duplicated code.