2.4. Prototype

2.4.1. Intent

According to the Gang of Four, the Prototype pattern is a way to “specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype” (Design Patterns: Elements of Reusable Object-Oriented Software, 2013, p. 117).

The pattern is quite simple. First, the Prototype interface declares a method for cloning itself and then the concrete Prototype will implement the method for cloning itself.

2.4.2. When to use it?

The Prototype pattern should be used when creating an instance of a class is expensive or complicated and you want to hide that complexity from the client

2.4.3. Diagram

Created using PhpStorm and yFiles.

Class diagram of the Prototype pattern

2.4.4. Implementation

AbstractShapePrototype.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php

namespace Phpatterns\Creational\Prototype;

abstract class AbstractShapePrototype
{
    /** @var int */
    protected $x;

    /** @var int */
    protected $y;

    public function __construct($x, $y)
    {
        $this->x = $x;
        $this->y = $y;
    }

    /**
     * @param int $x
     * @return $this
     */
    public function setX($x)
    {
        $this->x = $x;
        return $this;
    }

    /**
     * @param int $y
     * @return $this
     */
    public function setY($y)
    {
        $this->y = $y;
        return $this;
    }

    /**
     * Subclasses must implement that method
     */
    abstract public function __clone();
}

Circle.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?php

namespace Phpatterns\Creational\Prototype\Shape;

use Phpatterns\Creational\Prototype;

class Circle extends Prototype\AbstractShapePrototype
{
    /** @var int */
    private $radius;

    /** @var Prototype\Color */
    private $color;

    public function __construct($x, $y, $radius)
    {
        parent::__construct($x, $y);
        $this->radius = $radius;
    }

    /**
     * @param int $radius
     * @return $this
     */
    public function setRadius($radius)
    {
        $this->radius = $radius;
        return $this;
    }

    /**
     * @param Prototype\Color $color
     * @return $this
     */
    public function setColor($color)
    {
        $this->color = $color;
        return $this;
    }

    /**
     * @return Prototype\Color
     */
    public function getColor()
    {
        return $this->color;
    }

    /**
     * Making a deep copy of Circle because property 'color' is an object
     * Have a look at: http://php.net/manual/en/language.oop5.cloning.php
     */
    public function __clone()
    {
        $this->color = clone $this->color;
    }
}

Rectangle.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php

namespace Phpatterns\Creational\Prototype\Shape;

use Phpatterns\Creational\Prototype;

class Rectangle extends Prototype\AbstractShapePrototype
{
    /** @var int */
    private $width;

    /** @var int */
    private $height;

    public function __construct($x, $y, $width, $height)
    {
        parent::__construct($x, $y);
        $this->width = $width;
        $this->height = $height;
    }

    /**
     * @param int $width
     * @return $this
     */
    public function setWidth($width)
    {
        $this->width = $width;
        return $this;
    }

    /**
     * @param int $height
     * @return $this
     */
    public function setHeight($height)
    {
        $this->height = $height;
        return $this;
    }

    /**
     * Don't do anything, it's just a simple copy.
     */
    public function __clone()
    {

    }
}

Color.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php

namespace Phpatterns\Creational\Prototype;

class Color
{
    /** @var int */
    private $red;

    /** @var int */
    private $green;

    /** @var int */
    private $blue;

    public function __construct($red, $green, $blue)
    {
        $this->red = $red;
        $this->green = $green;
        $this->blue = $blue;
    }

    /**
     * @param int $red
     * @return $this
     */
    public function setRed($red)
    {
        $this->red = $red;
        return $this;
    }

    /**
     * @param int $green
     * @return $this
     */
    public function setGreen($green)
    {
        $this->green = $green;
        return $this;
    }

    /**
     * @param int $blue
     * @return $this
     */
    public function setBlue($blue)
    {
        $this->blue = $blue;
        return $this;
    }
}

ShapeManager.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<?php

namespace Phpatterns\Creational\Prototype;

use Phpatterns\Creational\Prototype\Shape;

class ShapeManager
{
    /** @var Shape\Circle */
    private $circlePrototype;

    /** @var Shape\Rectangle */
    private $rectanglePrototype;

    /** @var array */
    private $shapes;

    public function __construct(Shape\Circle $circlePrototype, Shape\Rectangle $rectanglePrototype)
    {
        $this->circlePrototype = $circlePrototype;
        $this->rectanglePrototype = $rectanglePrototype;
        $this->shapes = [];
    }

    public function addCircle($x, $y, $radius)
    {
        /** @var $circleClone Shape\Circle */
        $circleClone = clone $this->circlePrototype;
        $this->shapes[] = $circleClone
            ->setX($x)
            ->setY($y)
            ->setRadius($radius);
    }

    public function addRectangle($x, $y, $width, $height)
    {
        /** @var $rectangleClone Shape\Rectangle */
        $rectangleClone = clone $this->rectanglePrototype;
        $this->shapes[] = $rectangleClone
            ->setX($x)
            ->setY($y)
            ->setWidth($width)
            ->setHeight($height);
    }

    /**
     * @return array
     */
    public function getShapes()
    {
        return $this->shapes;
    }

    /**
     * @return Shape\Circle
     */
    public function getCirclePrototype()
    {
        return $this->circlePrototype;
    }

    /**
     * @return Shape\Rectangle
     */
    public function getRectanglePrototype()
    {
        return $this->rectanglePrototype;
    }

    /**
     * @param array $shapes
     */
    public function setShapes($shapes)
    {
        $this->shapes = $shapes;
    }
}

2.4.5. Tests

ShapePrototypeTest.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?php

namespace Test\Phpatterns\Creational\Prototype;

use Phpatterns\Creational\Prototype;
use Phpatterns\Creational\Prototype\Shape;

class ShapePrototypeTest extends \PHPUnit_Framework_TestCase
{
    /** @var Prototype\ShapeManager */
    private $shapeManager;

    protected function setUp()
    {
        $this->shapeManager = new Prototype\ShapeManager(
            (new Shape\Circle(0, 0, 0))->setColor(
                new Prototype\Color(255, 255, 255) //white
            ),
            new Shape\Rectangle(0, 0, 0, 0)
        );
    }

    public function testCircleCloningMechanism()
    {
        $this->assertEmpty($this->shapeManager->getShapes());

        $this->shapeManager->addCircle(2, 5, 8);
        /** @var $circleClone Shape\Circle */
        $circleClone = $this->shapeManager->getShapes()[0];

        $this->assertNotEquals(
            $circleClone,
            $this->shapeManager->getCirclePrototype()
        );

        $this->assertNotSame(
            $circleClone->getColor(),
            $this->shapeManager->getCirclePrototype()->getColor()
        );

        $this->assertNotEmpty($this->shapeManager->getShapes());
    }

    public function testRectangleCloningMechanism()
    {
        $this->assertEmpty($this->shapeManager->getShapes());

        $this->shapeManager->addRectangle(7, 4, 100, 200);
        $this->assertNotEquals(
            $this->shapeManager->getShapes()[0],
            $this->shapeManager->getRectanglePrototype()
        );

        $this->assertNotEmpty($this->shapeManager->getShapes());
    }
}