Design patterns in PHP: Strategy

This tutorial describes a TDD approach to refactor code to Strategy pattern. Starting from working code and some test, refactoring steps are applied to obtain cleaner code

Category: Development
Subcategories: Tutorials
21/08/2019
5409 times
Design patterns in PHP: Strategy

Introduction

In the previous article I described how to use the Refactor to the Replace Constructors with creational methods pattern, which is used to create a cleaner and more descriptive interface for similar objects creation.

In this tutorial, I will go further with the same example and introduce another common Design Pattern, the Strategy pattern. Its main purpose is to replace conditionals logic with classes implementing a delegate method, each implementing the calculation of each variant. This leads to the following advantages:

  • Decreases complexity from code by eliminating conditionals
  • Enables runtim algorithm hot-swap
  • Simplifies class by moving algorithms to hiearachies, thus increasing level of abstraction

Initial setup

We start from the arrival point of the previous article, using the following class ParamManager

class ParamManager
{
private $param1;
private $param2;

public function __construct($param1 = null, $param2 = null)
{
$this->param1 = $param1;
$this->param2 = $param2;
}

public static function withTwoArguments($param1, $param2)
{
return new ParamManager($param1, $param2);
}

public static function withOneArgument($param1)
{
return new ParamManager($param1);
}

public static function withNoArguments()
{
return new ParamManager();
}

public function getValue()
{
if ($this->param1 && $this->param2) {
return 'both';
}

if ($this->param1) {
return 'first';
}

return 'none';
}
}

which has three creational methods for the three variants:

  • Both param1 and param2 set
  • Only param1 set
  • No params set

Depending on the values of param1 and param2, the getValue() methods returns a different value. The purpose of the Strategy pattern is to eliminate this conditional.

We also have a backing test ensuring that the refactoring steps don't change code behavior:

class ExampleTest extends TestCase
{
/** @test */
public function it_builds_param_manager_with_two_parameters()
{
$manager = ParamManager::withTwoArguments('a', 'b');

$this->assertEquals('both', $manager->getValue());
}

/** @test */
public function it_builds_param_manager_with_one_parameter()
{
$manager = ParamManager::withOneArgument('a');

$this->assertEquals('first', $manager->getValue());
}

/** @test */
public function it_builds_param_manager_with_no_parameters()
{
$manager = ParamManager::withNoArguments();

$this->assertEquals('none', $manager->getValue());
}
}

Refactoring

In order to implement the pattern we must follow these steps:

1. Create a strategy concrete class

Create a strategy concrete class by naming it as one of your intended strategy, for example ParametersStrategy

class ParametersStrategy
{

}

2. Create the strategy method

Create the strategy method which will perform the calculation and move the original calculation to the Strategy:

class ParametersStrategy
{
+ public function getValue($param1, $param2)
+ {
+ if ($param1 && $param2) {
+ return 'both';
+ }
+
+ if ($param1) {
+ return 'first';
+ }
+
+ return 'none';
+ }
}
class ParamManager
{
private $param1;
private $param2;

public function __construct($param1 = null, $param2 = null)
{
$this->param1 = $param1;
$this->param2 = $param2;
}

public static function withTwoArguments($param1, $param2)
{
return new ParamManager($param1, $param2);
}

public static function withOneArgument($param1)
{
return new ParamManager($param1);
}

public static function withNoArguments()
{
return new ParamManager();
}

public function getValue()
{
+ return (new ParametersStrategy())->getValue($this->param1, $this->param2);

- if ($this->param1 && $this->param2) {
- return 'both';
- }
-
- if ($this->param1) {
- return 'first';
- }
-
- return 'none';
}
}

Note that, since both $param1 and $param2 are fields of ParamManager, they must be passed. Optionally we could have passed the whole ParamManager instance and get their value by creating getters or making them public.

3. Make ParametersStrategy instance a field and creates it in the creational methods.

This step is crucial for the next step.

class ParamManager
{
private $param1;
private $param2;
+ private $parametersStrategy;

+ public function __construct(ParametersStrategy $parametersStrategy, $param1 = null, $param2 = null)
- public function __construct($param1 = null, $param2 = null)
{
+ $this->parametersStrategy = $parameterStrategy;
$this->param1 = $param1;
$this->param2 = $param2;
}

public static function withTwoArguments($param1, $param2)
{
+ return new ParamManager(new ParametersStrategy(), 'a', 'b');
- return new ParamManager('a', 'b');
}

public static function withOneArgument($param1)
{
+ return new ParamManager(new ParametersStrategy(), 'a');
- return new ParamManager('a');
}

public static function withNoArguments()
{
+ return new ParamManager(new ParametersStrategy());
- return new ParamManager();
}

public function getValue()
{
+ return $this->parametersStrategy->getValue($this->param1, $this->param2);
- return (new ParametersStrategy())->getValue($this->param1, $this->param2);
}
}

4. Replace conditionals with polymorphism

At the current step we have just delegated the computation to another class without removing the conditionals. In this step we make the ParametersStrategy and its getValue() method abstract and create a concrete subclass for each algorithm variation, thus removing the conditionals:

+abstract class ParametersStrategy
-class ParametersStrategy
{
+ public abstract function getValue($param1, $param2);
- public function getValue($param1, $param2)
- {
- if ($param1 && $param2) {
- return 'both';
- }
-
- if ($param1) {
- return 'first';
- }
-
- return 'none';
- }
}

-

class TwoParametersStrategy extends ParametersStrategy
{

public function getValue($param1, $param2)
{
return 'both';
}
}

-

class OneParameterStrategy extends ParametersStrategy
{

public function getValue($param1, $param2)
{
return 'first';
}
}

-

class NoParametersStrategy extends ParametersStrategy
{

public function getValue($param1, $param2)
{
return 'none';
}
}

The last step is to change ParamManager creational methods to instantiate the correct concrete ParametersStrategy:

class ParamManager
{
private $param1;
private $param2;
private $parametersStrategy;

public function __construct(ParametersStrategy $parametersStrategy, $param1 = null, $param2 = null)
{
$this->parametersStrategy = $parameterStrategy;
$this->param1 = $param1;
$this->param2 = $param2;
}

public static function withTwoArguments($param1, $param2)
{
+ return new ParamManager(new TwoParametersStrategy(), 'a', 'b');
- return new ParamManager(new ParametersStrategy(), 'a', 'b');
}

public static function withOneArgument($param1)
{
+ return new ParamManager(new OneParameterStrategy(), 'a');
- return new ParamManager(new ParametersStrategy(), 'a');
}

public static function withNoArguments()
{
+ return new ParamManager(new NoParametersStrategy());
- return new ParamManager(new ParametersStrategy());
}

public function getValue()
{
return $this->parametersStrategy->getValue($this->param1, $this->param2);
}
}

5. Move parameters to their respective strategies

At this, point (as Roy stated in the comments section), there is no reason to keep the strategy parameters in ParamManager class, so I decided to move them in the respective strategies:

class ParamManager
{
- private $param1;
- private $param2;
private $parametersStrategy;


+ public function __construct(ParametersStrategy $parametersStrategy)
- public function __construct(ParametersStrategy $parametersStrategy, $param1 = null, $param2 = null)
{
$this->parametersStrategy = $parameterStrategy;
- $this->param1 = $param1;
- $this->param2 = $param2;
}

public static function withTwoArguments($param1, $param2)
{
+ return new ParamManager(new TwoParametersStrategy('a', 'b'));
- return new ParamManager(new TwoParametersStrategy(), 'a', 'b');
}

public static function withOneArgument($param1)
{
+ return new ParamManager(new OneParameterStrategy('a'));
- return new ParamManager(new OneParameterStrategy(), 'a');
}

public static function withNoArguments()
{
return new ParamManager(new NoParametersStrategy());
}

public function getValue()
{
return $this->parametersStrategy->getValue($this->param1, $this->param2);
}
}
+abstract class ParametersStrategy
-class ParametersStrategy
{
+ public abstract function getValue();
- public abstract function getValue($param1, $param2);

-

class TwoParametersStrategy extends ParametersStrategy
{
+ private $param1;
+ private $param2;

+ public function __construct($param1, $param2)
+ {
+ $this->param1 = $param1;
+ $this->param2 = $param2;
+ }

+ public function getValue()
- public function getValue($param1, $param2)
{
return 'both';
}
}

-

class OneParameterStrategy extends ParametersStrategy
{
+ private $param1;

+ public function __construct($param1)
+ {
+ $this->param1 = $param1;
+ }

+ public function getValue()
- public function getValue($param1, $param2)
{
return 'first';
}
}

-

class NoParametersStrategy extends ParametersStrategy
{

+ public function getValue()
- public function getValue($param1, $param2)
{
return 'none';
}
}

Final notes

Now, getValue() will execute the correct concrete strategy, and conditionals were removed, without changing client code (the tests remain the same and still pass)

Note that, in order to be compliant with abstract method definition, some (or even all) parameters need to be passed to strategy method even if they are not required for calculation.

If you have doubts, or corrections, please leave a comment below.

Related posts
State pattern implementation of finite state machine (FSM) with Laravel

In the previous article TDD IMPLEMENTATION OF FINITE STATE MACHINE (FSM) WITH LARAVEL I discussed a basic approach for implement a Finite State Machine (FSM) on a Laravel model. In this article I will further discuss the topic by applying a more engineered approach. It involves the usage of the State pattern

Design patterns in PHP: Replace Constructors with Creation Methods

This tutorial describes a TDD approach to refactor code to Replace Constructors with Creation Methods pattern. Starting from working code and some test, refactoring steps are applied to obtain cleaner code

PHP/Laravel Library for Tado API

I released on GitHub a PHP library for querying and editing data on the Tado Thermostat System. It is integrated in Laravel 5.x for quick setup in this excellent framework.

Vue component for Typing effect

A package for a vue plugin for rendering text-typing animation