Purpose
The purpose of the design pattern
To separate strategies and to enable fast switching between them. Also this pattern is a good alternative to inheritance (instead of having an abstract class that is extended).
Essentially by passing an object into the construct (it’s recommended that you type hint to an interface) you can 'execute a strategy' (i.e do something) on the given object using data passed in via methods parameters.
Examples
Examples of how the design pattern can be used
- Sorting a list of objects, one strategy by date, the other by id
- Simplify unit testing: e.g. switching between file and in-memory storage
UML
UML design pattern diagram
Code
Code snippets
Comparator
Comparator is an Interface with a compare method signature, any class which implements Comparator must have a compare method.
namespace DesignPatterns\Behavioral\Strategy;
interface Comparator
{
/**
* @param mixed $a
* @param mixed $b
*/
public function compare($a, $b): int;
}
DateComparator
DateComparator contains a compare method which compares two given dates. It implements Comparator to ensure it contains a compare method.
namespace DesignPatterns\Behavioral\Strategy;
use DateTime;
class DateComparator implements Comparator
{
public function compare($a, $b): int
{
$aDate = new DateTime($a['date']);
$bDate = new DateTime($b['date']);
return $aDate <=> $bDate;
}
}
IdComparator
IdComparator contains a compare method which compares two given ids. It implements Comparator to ensure it contains a compare method.
namespace DesignPatterns\Behavioral\Strategy;
class IdComparator implements Comparator
{
public function compare($a, $b): int
{
return $a['id'] <=> $b['id'];
}
}
Context
Context is passed an object via construct which can referenced in the executeStrategy method. In the example below, we’re using the compare method to sort an array of elements.
However, executeStrategy could potentially do anything you want with the object passed into the construct. The executeStrategy method parameters also can change to whatever is required.
However, executeStrategy could potentially do anything you want with the object passed into the construct. The executeStrategy method parameters also can change to whatever is required.
namespace DesignPatterns\Behavioral\Strategy;
class Context
{
public function __construct(private Comparator $comparator)
{
}
public function executeStrategy(array $elements): array
{
uasort($elements, [$this->comparator, 'compare']);
return $elements;
}
}
Tests
Unit test examples
public function provideIntegers()
{
return [
[
[['id' => 2], ['id' => 1], ['id' => 3]],
['id' => 1],
],
[
[['id' => 3], ['id' => 2], ['id' => 1]],
['id' => 1],
],
];
}
public function provideDates()
{
return [
[
[['id' => 2], ['id' => 1], ['id' => 3]],
['id' => 1],
],
[
[['id' => 3], ['id' => 2], ['id' => 1]],
['id' => 1],
],
];
}
/**
* @dataProvider provideIntegers
*
* @param array $collection
* @param array $expected
*/
public function testIdComparator($collection, $expected)
{
$obj = new Context(new IdComparator());
$elements = $obj->executeStrategy($collection);
$firstElement = array_shift($elements);
$this->assertSame($expected, $firstElement);
}
/**
* @dataProvider provideDates
*
* @param array $collection
* @param array $expected
*/
public function testDateComparator($collection, $expected)
{
$obj = new Context(new DateComparator());
$elements = $obj->executeStrategy($collection);
$firstElement = array_shift($elements);
$this->assertSame($expected, $firstElement);
}