Skip to content

Posts › Development

Development

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

Design patterns in PHP: Replace Constructors with Creation Methods

Introduction

Replace Constructors with Creation Methods pattern is one of the most used creational design patterns. It is used mainly for these reasons:

  • Simplifies code structure by using more descriptive methods instead of generic constructor
  • Removes the limitation of having several constructors with the same signature

Initial setup

To describe the refactoring, I proposed a very simple use case. Suppose to have a ParamManager class with a constructor accepting from 0 to 2 parameters. It behaves as follows:

  • It returns the string 'both' if two arguments are provided
  • It returns the string 'first' if one argument is provided
  • It returns the string 'none' if no arguments are provided

The behaviour is described by the following tests:

php
 1class ExampleTest extends TestCase
 2{
 3    /** @test */
 4    public function it_builds_param_manager_with_two_parameters()
 5    {
 6        $manager = new ParamManager('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 = new ParamManager('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 = new ParamManager();
23
24        $this->assertEquals('none', $manager->getValue());
25    }
26}

and the ParamManager class is defined as:

php
 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 function getValue()
13    {
14        if ($this->param1 && $this->param2) {
15            return 'both';
16        }
17
18        if ($this->param1) {
19            return 'first';
20        }
21
22        return 'none';
23    }
24}

The main purpose of Creation Methods is to have more descriptive and easier to remember methods (w.r.t. constructors) for objects instantiation.

Refactoring

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

1. Locate a client class constructor call and extract it to a new method which will represent the creation method

For example, start from the first constructor call in the tests:

php
 1    /** @test */
 2    public function it_builds_param_manager_with_two_parameters()
 3    {
 4-       $manager = new ParamManager('a', 'b');
 5+       $manager = $this->withTwoArguments('a', 'b');
 6
 7        $this->assertEquals('both', $manager->getValue());
 8    }
 9
10+   private function withTwoArguments($param1, $param2)
11+   {
12+     return new ParamManager($param1, $param2);
13+   }

2. Move the method to a static or non-static method in the target class

Refactor the new method by moving it to the target class (ParamManager) making it public and, perhaps, static

php
 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 function getValue()
18    {
19        if ($this->param1 && $this->param2) {
20            return 'both';
21        }
22
23        if ($this->param1) {
24            return 'first';
25        }
26
27        return 'none';
28    }
29}

the client code then reduces to:

php
 1    /** @test */
 2    public function it_builds_param_manager_with_two_parameters()
 3    {
 4-        $manager = $this->withTwoArguments('a', 'b');
 5+        $manager = ParamManager::withTwoArguments('a', 'b');
 6
 7        $this->assertEquals('both', $manager->getValue());
 8    }
 9
10-   private function withTwoArguments($param1, $param2)
11-   {
12-     return new ParamManager($param1, $param2);
13-   }

3. Replace the same occurrence of the constructor (if any)

If any, remove any identical occurrence of the very same constructor, in the proposed example, no more occurrence exist

4. Repeat for the next constructor call

Using the same approach, generate creational methods for the other constructors' invocations, thus defining withOneArgument and withNoArguments methods:

php
 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}

and you are done!

Final notes

If you don't need the generic constructor anymore, I suggest making it private to enforce the creational method usage, thus avoiding a mix of the two instantiation alternatives (creational methods and constructors)

The resulting code is far from being clean, for example, you could remove conditionals in getValue() by applying another common design pattern, Strategy, which I will describe in the next tutorial.

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