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
1class ParamManager
2{
3 private $param1;
4 private $param2;
5
6 public function __construct($param1 = null, $param2 = null)
7 {
8 $this->param1 = $param1;
9 $this->param2 = $param2;
10 }
11
12 public static function withTwoArguments($param1, $param2)
13 {
14 return new ParamManager($param1, $param2);
15 }
16
17 public static function withOneArgument($param1)
18 {
19 return new ParamManager($param1);
20 }
21
22 public static function withNoArguments()
23 {
24 return new ParamManager();
25 }
26
27 public function getValue()
28 {
29 if ($this->param1 && $this->param2) {
30 return 'both';
31 }
32
33 if ($this->param1) {
34 return 'first';
35 }
36
37 return 'none';
38 }
39}
which has three creational methods for the three variants:
- Both
param1andparam2set - Only
param1set - 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:
1class ExampleTest extends TestCase
2{
3 /** @test */
4 public function it_builds_param_manager_with_two_parameters()
5 {
6 $manager = ParamManager::withTwoArguments('a', 'b');
7
8 $this->assertEquals('both', $manager->getValue());
9 }
10
11 /** @test */
12 public function it_builds_param_manager_with_one_parameter()
13 {
14 $manager = ParamManager::withOneArgument('a');
15
16 $this->assertEquals('first', $manager->getValue());
17 }
18
19 /** @test */
20 public function it_builds_param_manager_with_no_parameters()
21 {
22 $manager = ParamManager::withNoArguments();
23
24 $this->assertEquals('none', $manager->getValue());
25 }
26}
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
1class ParametersStrategy
2{
3
4}
2. Create the strategy method
Create the strategy method which will perform the calculation and move the original calculation to the Strategy:
1class ParametersStrategy
2{
3+ public function getValue($param1, $param2)
4+ {
5+ if ($param1 && $param2) {
6+ return 'both';
7+ }
8+
9+ if ($param1) {
10+ return 'first';
11+ }
12+
13+ return 'none';
14+ }
15}
1class ParamManager
2{
3 private $param1;
4 private $param2;
5
6 public function __construct($param1 = null, $param2 = null)
7 {
8 $this->param1 = $param1;
9 $this->param2 = $param2;
10 }
11
12 public static function withTwoArguments($param1, $param2)
13 {
14 return new ParamManager($param1, $param2);
15 }
16
17 public static function withOneArgument($param1)
18 {
19 return new ParamManager($param1);
20 }
21
22 public static function withNoArguments()
23 {
24 return new ParamManager();
25 }
26
27 public function getValue()
28 {
29+ return (new ParametersStrategy())->getValue($this->param1, $this->param2);
30
31- if ($this->param1 && $this->param2) {
32- return 'both';
33- }
34-
35- if ($this->param1) {
36- return 'first';
37- }
38-
39- return 'none';
40 }
41}
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.
1class ParamManager
2{
3 private $param1;
4 private $param2;
5+ private $parametersStrategy;
6
7+ public function __construct(ParametersStrategy $parametersStrategy, $param1 = null, $param2 = null)
8- public function __construct($param1 = null, $param2 = null)
9 {
10+ $this->parametersStrategy = $parameterStrategy;
11 $this->param1 = $param1;
12 $this->param2 = $param2;
13 }
14
15 public static function withTwoArguments($param1, $param2)
16 {
17+ return new ParamManager(new ParametersStrategy(), 'a', 'b');
18- return new ParamManager('a', 'b');
19 }
20
21 public static function withOneArgument($param1)
22 {
23+ return new ParamManager(new ParametersStrategy(), 'a');
24- return new ParamManager('a');
25 }
26
27 public static function withNoArguments()
28 {
29+ return new ParamManager(new ParametersStrategy());
30- return new ParamManager();
31 }
32
33 public function getValue()
34 {
35+ return $this->parametersStrategy->getValue($this->param1, $this->param2);
36- return (new ParametersStrategy())->getValue($this->param1, $this->param2);
37 }
38}
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:
1+abstract class ParametersStrategy
2-class ParametersStrategy
3{
4+ public abstract function getValue($param1, $param2);
5- public function getValue($param1, $param2)
6- {
7- if ($param1 && $param2) {
8- return 'both';
9- }
10-
11- if ($param1) {
12- return 'first';
13- }
14-
15- return 'none';
16- }
17}
1class TwoParametersStrategy extends ParametersStrategy
2{
3
4 public function getValue($param1, $param2)
5 {
6 return 'both';
7 }
8}
1class OneParameterStrategy extends ParametersStrategy
2{
3
4 public function getValue($param1, $param2)
5 {
6 return 'first';
7 }
8}
1class NoParametersStrategy extends ParametersStrategy
2{
3
4 public function getValue($param1, $param2)
5 {
6 return 'none';
7 }
8}
The last step is to change ParamManager creational methods to instantiate the correct concrete ParametersStrategy:
1class ParamManager
2{
3 private $param1;
4 private $param2;
5 private $parametersStrategy;
6
7 public function __construct(ParametersStrategy $parametersStrategy, $param1 = null, $param2 = null)
8 {
9 $this->parametersStrategy = $parameterStrategy;
10 $this->param1 = $param1;
11 $this->param2 = $param2;
12 }
13
14 public static function withTwoArguments($param1, $param2)
15 {
16+ return new ParamManager(new TwoParametersStrategy(), 'a', 'b');
17- return new ParamManager(new ParametersStrategy(), 'a', 'b');
18 }
19
20 public static function withOneArgument($param1)
21 {
22+ return new ParamManager(new OneParameterStrategy(), 'a');
23- return new ParamManager(new ParametersStrategy(), 'a');
24 }
25
26 public static function withNoArguments()
27 {
28+ return new ParamManager(new NoParametersStrategy());
29- return new ParamManager(new ParametersStrategy());
30 }
31
32 public function getValue()
33 {
34 return $this->parametersStrategy->getValue($this->param1, $this->param2);
35 }
36}
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:
1class ParamManager
2{
3- private $param1;
4- private $param2;
5 private $parametersStrategy;
6
7
8+ public function __construct(ParametersStrategy $parametersStrategy)
9- public function __construct(ParametersStrategy $parametersStrategy, $param1 = null, $param2 = null)
10 {
11 $this->parametersStrategy = $parameterStrategy;
12- $this->param1 = $param1;
13- $this->param2 = $param2;
14 }
15
16 public static function withTwoArguments($param1, $param2)
17 {
18+ return new ParamManager(new TwoParametersStrategy('a', 'b'));
19- return new ParamManager(new TwoParametersStrategy(), 'a', 'b');
20 }
21
22 public static function withOneArgument($param1)
23 {
24+ return new ParamManager(new OneParameterStrategy('a'));
25- return new ParamManager(new OneParameterStrategy(), 'a');
26 }
27
28 public static function withNoArguments()
29 {
30 return new ParamManager(new NoParametersStrategy());
31 }
32
33 public function getValue()
34 {
35 return $this->parametersStrategy->getValue($this->param1, $this->param2);
36 }
37}
1+abstract class ParametersStrategy
2-class ParametersStrategy
3{
4+ public abstract function getValue();
5- public abstract function getValue($param1, $param2);
1class TwoParametersStrategy extends ParametersStrategy
2{
3+ private $param1;
4+ private $param2;
5
6+ public function __construct($param1, $param2)
7+ {
8+ $this->param1 = $param1;
9+ $this->param2 = $param2;
10+ }
11
12+ public function getValue()
13- public function getValue($param1, $param2)
14 {
15 return 'both';
16 }
17}
1class OneParameterStrategy extends ParametersStrategy
2{
3+ private $param1;
4
5+ public function __construct($param1)
6+ {
7+ $this->param1 = $param1;
8+ }
9
10+ public function getValue()
11- public function getValue($param1, $param2)
12 {
13 return 'first';
14 }
15}
1class NoParametersStrategy extends ParametersStrategy
2{
3
4+ public function getValue()
5- public function getValue($param1, $param2)
6 {
7 return 'none';
8 }
9}
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.