In general, we use the Strategy Pattern when we can do something in several ways depending on some condition. Suppose we have to create some image resize class using GD library.
We could create some code like this:
class ImageResizer
{
public function resize(string $filename, string extension, int $newWidth, int $newHeight)
{
switch ($extension) {
case 'PNG':
$source = imagecreatefrompng($filename);
$dest = imagescale($source, $newWidth, $newHeight);
imagepng($dest, $filename);
return;
case 'JPG':
$source = imagecreatefromjpeg($filename);
$dest = imagescale($source, $newWidth, $newHeight);
imagejpeg($dest, $filename);
return;
case 'BMP':
$source = imagecreatefrombmp($filename);
$dest = imagescale($source, $newWidth, $newHeight);
imagebmp($dest, $filename);
return;
default:
throw new \Exception('Unhandled extension');
}
}
This code will work, but it's not perfect. Everything is written in the same class in a single method. This code does too many things.
First of all let's create separate classes for each resize strategy.
Here is an easy and clean example for JPG resize strategy.
class ImageJpgResizeStrategy implements ImageResizeStrategyInterface
{
public function resize(string $filename, int $newWidth, int $newHeight)
{
$source = imagecreatefromjpeg($filename);
$dest = imagescale($source, $newWidth, $newHeight);
imagejpeg($dest, $filename);
}
}
The rest strategies can look similar. Because of this, it's a good moment to create common interface for all these strategies:
interface ImageResizeStrategyInterface
{
public function resize(string $filename, int $newWidth, int $newHeight);
}
So our image resize class could be changed now:
class ImageResizer
{
private ImageResizeStrategyInterface $strategy;
public function resize(string $filename, string $extension, int $newWidth, int $newHeight)
{
switch ($extension) {
case 'PNG':
$strategy = new ImagePngResizeStrategy();
break
case 'JPG':
$strategy = new ImageJpgResizeStrategy();
break
case 'BMP':
$strategy = new ImageBmpResizeStrategy();
break
default:
throw new \Exception('Unhandled extension');
}
$strategy->resize($filename, $newWidth, $newHeight)
}
And that's Strategy Pattern for! We moved our resize logic for each image type somewhere else and made our code clearer!
What about Symfony
But could we get rid of this ugly switch-case construction?
Yes! In Symfony we can use reference tagged services.
Put in your services.yml
these lines
_instanceof:
App\SomePathHere\ImageResizeStrategyInterface:
tags: [ 'image_resize_strategy' ]
App\SomePathHere\ImageResizer:
arguments:
- !tagged_iterator { tag: 'image_resize_strategy' }
This will cause that our ImageResizer
class will get an array or strategies as a parameter in constructor:
class ImageResizer
{
private array $strategies;
public function __construct(iterable $strategies)
{
$this->strategies = $strategies instanceof \Traversable ? iterator_to_array($strategies) : $strategies;
}
...
But now all strategies are in a plain array and there is no sign what is each strategy for. To separate them we need some method in each strategy which will give us some information about it
Let's change our interface and add getImageType
method there:
interface ImageResizeStrategyInterface
{
public function resize(string $filename, int $newWidth, int $newHeight);
public function getImageType(): string;
}
Then we have to add this method in each strategy. Something like this:
public static function getImageType(): string
{
return 'JPG'; // Or something else
}
Now we can rewrite our resize method using getImageType function:
public function resize(string $filename, string $extension, int $newWidth, int $newHeight)
{
foreach($this->strategies as $strategy) {
if($strategy->getImageType() === $extension) {
$strategy->resize($filename, $newWidth, $newHeight);
return;
}
}
throw new \Exception('Unhandled extension');
}
Or we can add index to the strategies array, just add default_index_method
option in your services.yml
- !tagged_iterator { tag: 'image_resize_strategy', default_index_method: 'getImageType' }
This will make your strategies array indexed. So the resize method of the class can look this way:
public function resize(string $filename, string $extension, int $newWidth, int $newHeight)
) {
if(!isset($this->stategies[$extension])) {
throw new \Exception('Unhandled extension');
}
$strategy = $this->stategies[$extension];
$strategy->resize($filename, $newWidth, $newHeight)
}
Easy reading, clear and beautiful code!
Top comments (1)
Salut
Sur symfony 6 dans services.yaml il faut mettre _instanceof:
_instanceof:
App\SomePathHere\ImageResizeStrategyInterface:
tags: [ 'image_resize_strategy' ]
App\SomePathHere\ImageResizer:
arguments:
- !tagged_iterator { tag: 'image_resize_strategy' }