Design Patterns: Strategy Pattern

Design Patterns: Strategy Pattern

In a previous blog post Intro to Design Patterns I used strategy pattern as an example for behavioral patterns. In this post, I will give a deeper explanation to strategy pattern and give examples to it.

You know how you can go to the grocery store through many different routes? Or how you can play music on both your phone and your laptop despite them being different devices? This is where strategy pattern shines.

Strategy pattern is a behavioral design pattern that is very helpful for when you have similar but different objects where each object has a different way to do the same task or when something can be achieved in more than one way and you need to accommodate for all these ways in your application. Let's explain with an example.

Problem

Let's assume that you have an application that calculates the area of a square. You can have a class for the square that looks like this:

class Square {
  side double;

  function area () {
    return this.side * this.side;
  }
}

and your main function would be:

function main() {
  squareInstance = new Square();
  squareInstance.side = 5;
  print(squareInstance.area);
}

Now a new requirement comes up where you also need to handle triangles. One way to do that is to add a "type" field to the class and calculate the area according to the type which would look like this:

class Shape {
  type string;
  side double;
  base double;
  height double;

  function area () {
    switch this.type {
      case "triangle": return 0.5 * this.base * this.height;
      case "square" : return this.side * this.side;
    }
  }
}

this would work but what if we add a circle to the class? as you can probably notice, this is a confusing solution and it will get more confusing as we add more types. This solution would introduce a lot of problems like:

  1. We needed to add variables that are specific to a single shape and are irrelevant to other shapes. The triangle has a base and a height while the square has a side.
  2. Initializing an instance of the class is error prone. A client can simply create a shape of type "triangle" and give it base and side instead of a height.
  3. Handling object behavior requires switch statements or if statements. This will cause problems as the project gets larger because whenever you add a new type, you will find yourself adding a new if/case statement to every function that handles the object and before you know it, you will have a massive piece of code that is basically conditional statements.

Solution

The strategy pattern aims to allow for similar objects to have different behaviors without having to know the context. It also keeps the code clean and organized and allows for adding new behaviors seamlessly. Here is how it works:

  1. Move the function that has different behaviors to an interface.
  2. Split the different "types" to different classes.
  3. each of the new classes should implement the interface.

The code will look like this:

interface AreaStrategy {
  function area(){};
}

class Square implements AreaStrategy {
  side double;
  function area() {
    return this.side * this.side;
  }
}

class Triangle implements AreaStrategy {
  base double;
  height double;
  function area() {
    return 0.5 * this.base * this.height;
  }
}

Now calculating the area can be done without having the context. You can have a function that takes a shape and calculate the area directly without the function having to know what the shape is. Since all classes implement AreaStrategy, shape can simply be of type AreaStrategy. The function would look like this:

 function calculateArea(shape AreaStrategy){
  return shape.area()
}

function main() {
  squareInstance = new Square();
  squareInstance.side = 5;
  print(calculateArea(squareInstance))
  // prints "25" i.e. 5 * 5

  triangleInstance = new Triangle();
  triangleInstance.base = 10;
  triangleInstance.height = 20;
  print(calculateArea(triangleInstance))
  // prints "100" i.e. 0.5 * 10 * 20
}

By using abstraction, a function needs to only have the object to work properly without having to know the object's type. It doesn't even have to know that there are types. As long as the object implements the interface it will work without adding anything to the function.

Now if we have a new shape, all we need to do is to create a new class and make it implement the AreaStrategy interface and it will work without having to make modifications to other parts of the code.

Conclusion

By using abstraction, strategy pattern makes it a lot easier and cleaner to have different behaviors/algorithms for the same task. Simply implement an interface and you're good to go. No context needed. It simply allows you to focus on the task rather than the method it's done.

Photo by Alvaro Reyes on Unsplash