Strategy – design pattern

Strategy (also known as Policy) is one of the most common design patterns. It’s a behavioral pattern. That means it’s concerned with the responsibilities of objects and communication between them. The Strategy pattern helps you create clean, extensible, and reusable code. It also complies with the Open/Closed Principle (OCP). In other words, it allows us to add new implementations (algorithms) without changing existing code.

Overview

The main goal of the Strategy is to encapsulate a family of algorithms with a common interface. These algorithms do the same job but in a slightly different way. Symfony encoders are a good example. They all implement EncoderInterface and encode and decode data in various formats (e.g. JSON, XML, YAML). You can use them interchangeably. Therefore this approach allows your application to choose the right algorithm at runtime. The class (called context) that contains and uses an encoder does not have to be aware of its concrete class. Let’s take a look at a simple diagram.

As you can see, the context takes an encoder as a parameter and store it in the $encoder property. The context knows only the interface of the encoder but still can use it.

Real-life example

Let’s suppose we want to implement a daily calorie calculator. The calculator estimates how many calories you need to maintain your weight (AMR). The equation is quite simple:

AMR = BMR x AF
  • AMR – Active Metabolic Rate
  • BMR – Basic Metabolic Rate – number of calories required to sustain life functions
  • AF – Activity Factor – multiplier based on activity level

Possible values of Activity Factor (AF):

  • no activity – 1.25
  • low activity – 1.375
  • moderate activity – 1.55
  • high activity – 1.725
  • very high activity – 1.9

There are several BMR estimation formulas. We want to implement some of them and let the application to chose the right one in runtime. The choice can be made by a user or some business logic. First, let’s look at the simplified diagram of our application.

As you can see, we are going to have a BmrCalculator interface with two implementations encapsulating two different BRM estimation algorithms. Firstly, let’s define the interface.

<?php

namespace App;

interface BmrCalculator
{
    public function calculateBmr(int $weight, int $height, int $age, int $gender): float;
}

Afterward, we can implement the algorithms. I chose 2 of them: Harris-Benedict and Mifflin St Jeor. You can find the formulas on Wikipedia.

<?php

namespace App\Bmr;

use App\BmrCalculator;
use App\Gender;

class HarrisBenedictBmrCalculator implements BmrCalculator
{
    public function calculateBmr(int $weight, int $height, int $age, int $gender): float
    {
        if($gender === Gender::MALE) {
            return $this->calculateBmrForMen($weight, $height, $age);
        } else {
            return $this->calculateBmrForWomen($weight, $height, $age);
        }
    }

    private function calculateBmrForMen(int $weight, int $height, int $age): float
    {
        return 66.5 + (13.75 * $weight) + (5.003 * $height) - (6.755 * $age);
    }

    private function calculateBmrForWomen(int $weight, int $height, int $age): float
    {
        return 655 + (9.563 * $weight) + (1.850 * $height) - (4.676 * $age);
    }
}
<?php

namespace App\Bmr;

use App\BmrCalculator;
use App\Gender;

class MiffinStJeorBmrCalculator implements BmrCalculator
{
    public function calculateBmr(int $weight, int $height, int $age, int $gender): float
    {
        if($gender === Gender::MALE) {
            return $this->calculateBmrForMen($weight, $height, $age);
        } else {
            return $this->calculateBmrForWomen($weight, $height, $age);
        }
    }

    private function calculateBmrForMen(int $weight, int $height, int $age): float
    {
        return (10 * $weight) + (6.25 * $height) - (5 * $age) + 5;
    }

    private function calculateBmrForWomen(int $weight, int $height, int $age): float
    {
        return (10 * $weight) + (6.25 * $height) - (5 * $age) - 161;
    }
}

In order to make the example more readable, I created two auxiliary interfaces containing constants.

<?php

namespace App;

interface ActivityLevel
{
    public const NO_ACTIVITY = 1.2;
    public const LIGHT_ACTIVITY = 1.375;
    public const MODERATE_ACTIVITY = 1.55;
    public const HIGH_ACTIVITY = 1.725;
    public const VERY_HIGH_ACTIVITY = 1.9;
}
<?php

namespace App;

interface Gender
{
    public const MALE = 0;
    public const FEMALE = 1;
}

Since BMR calculators are implemented, we can finally create a context class. In our case, that class is called CalorieCalculator. The class uses one of implemented BMR calculators and multiplies its result by the activity level (passed to the class as a parameter). As a result, we get our AMR.

<?php

namespace App;

class CalorieCalculator
{
    private BmrCalculator $bmrCalculator;

    public function __construct(BmrCalculator $bmrCalculator)
    {
        $this->bmrCalculator = $bmrCalculator;
    }

    // weight in kilograms, height in centimeters, age in years
    public function calculate(
        int $weight, 
        int $height, 
        int $age, 
        int $gender, 
        float $activityFactor
    ): int {
        $bmr = $this->bmrCalculator->calculateBmr($weight, $height, $age, $gender);
        $amr = $bmr * $activityFactor;

        return round($amr);
    }
}

And that’s a complete implementation of the Strategy pattern. The entire code from the post is stored in this repository. Besides the code, the repository contains an initialized composer and index.php file with an example of usage.

Conclusion

The strategy is a pattern that every programmer should know. It allows you to replace complex if and switch statements with simple interfaces with multiple implementations. Moreover, adding and removing implementations do not require any modifications in the existing code (OCP). Therefore, it’s very in many frameworks and libraries. If you would like to read more of my posts about design patterns, you will find them here.

You May Also Like

Leave a Reply

Your email address will not be published. Required fields are marked *