3.6. Flyweight

3.6.1. Intent

According to the Gang of Four, the Flyweight pattern is a way to “use sharing to support large numbers of fine-grained objects efficiently” (Design Patterns: Elements of Reusable Object-Oriented Software, 2013, p. 195).

3.6.2. When to use it?

Flyweight pattern should be used when you have a large number of objects to deal with in you application! You have to be aware that each Flyweight object must be divided into two parts to be able to use the pattern: the intrinsic state (stored in the Flyweight, independent of the context) and the extrinsic state (dependent of the context, not shareable).

Note that the pattern also allows to decrease objects’ memory footprint and increase overall performance of the application.

3.6.3. Diagram

Created using PhpStorm and yFiles.

Class diagram of the Flyweight pattern

3.6.4. Implementation

BulletFactory.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
<?php

namespace Phpatterns\Structural\Flyweight;

use Phpatterns\Structural\Flyweight\Bullet;

class BulletFactory
{
    /** @var BulletInterface[] */
    private static $bullets  = [];

    /**
     * @param $type
     * @return BulletInterface
     */
    public static function getBullet($type)
    {
        if (! array_key_exists($type, self::$bullets)) {
            if ($type === 'BlankBullet') {
                self::$bullets[$type] = new Bullet\BlankBullet();
            } elseif ($type === 'ExpandingBullet') {
                self::$bullets[$type] = new Bullet\ExpandingBullet();
            }
        }

        return self::$bullets[$type];
    }
}

BulletInterface.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php

namespace Phpatterns\Structural\Flyweight;

interface BulletInterface
{
    /**
     * @param int $position
     */
    public function setPositionInMagazine($position);

    /**
     * @return int
     */
    public function getPositionInMagazine();
}

BlankBullet.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
<?php

namespace Phpatterns\Structural\Flyweight\Bullet;

use Phpatterns\Structural\Flyweight;

class BlankBullet implements Flyweight\BulletInterface
{
    /** @var int */
    private $damage; //intrinsic state

    /** @var int */
    private $positionInMagazine = null; //extrinsic state

    public function __construct()
    {
        $this->damage = 0;
    }

    /**
     * Setting extrinsic state
     * @param int $positionInMagazine
     */
    public function setPositionInMagazine($positionInMagazine)
    {
        $this->positionInMagazine = $positionInMagazine;
    }

    /**
     * @return int
     */
    public function getPositionInMagazine()
    {
        return $this->positionInMagazine;
    }
}

ExpandingBullet.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
<?php

namespace Phpatterns\Structural\Flyweight\Bullet;

use Phpatterns\Structural\Flyweight;

class ExpandingBullet implements Flyweight\BulletInterface
{
    /** @var int */
    private $damage; //intrinsic state

    /** @var int */
    private $positionInMagazine = null; //extrinsic state

    public function __construct()
    {
        $this->damage = 200;
    }

    /**
     * Setting extrinsic state
     * @param int $positionInMagazine
     */
    public function setPositionInMagazine($positionInMagazine)
    {
        $this->positionInMagazine = $positionInMagazine;
    }

    /**
     * @return int
     */
    public function getPositionInMagazine()
    {
        return $this->positionInMagazine;
    }
}

Gun.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
<?php

namespace Phpatterns\Structural\Flyweight;

class Gun
{
    /** @var BulletInterface[] */
    private $bullets;

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

    /**
     * @param int $maxBullets
     */
    public function __construct($maxBullets)
    {
        $this->bullets = [];
        $this->maxBullets = $maxBullets;
    }

    /**
     * @return BulletInterface[]
     */
    public function getBullets()
    {
        return $this->bullets;
    }

    /**
     * @return int
     */
    public function getMaxBullets()
    {
        return $this->maxBullets;
    }

    /**
     * @param string $bulletType
     */
    public function reload($bulletType)
    {
        for ($bulletsCount = count($this->bullets); $bulletsCount < $this->maxBullets; $bulletsCount++) {
            $this->bullets[] = BulletFactory::getBullet($bulletType);
        }
    }

    /**
     * @return string
     */
    public function fire()
    {
        if ($bulletsCount = count($this->bullets)) {
            /** @var BulletInterface $bullet */
            $bullet = array_shift($this->bullets);
            $bullet->setPositionInMagazine(($this->maxBullets - $bulletsCount) + 1);

            return sprintf(
                'Bullet n°%d fired! (%s)',
                $bullet->getPositionInMagazine(),
                get_class($bullet)
            );
        }

        return 'Reload gun!';
    }
}

3.6.5. Tests

FlyweightTest.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
<?php

namespace Test\Phpatterns\Structural\Flyweight;

use Phpatterns\Structural\Flyweight;

class FlyweightTest extends \PHPUnit_Framework_TestCase
{
    public function testReloading()
    {
        $gun = new Flyweight\Gun(6);

        $this->assertEmpty($gun->getBullets());
        $gun->reload('ExpandingBullet');

        $this->assertEquals(
            $gun->getMaxBullets(),
            count($gun->getBullets())
        );
    }

    /**
     * Testing only one instance of BlankBullet has been created
     */
    public function testBulletsAreSameObject()
    {
        $gun = new Flyweight\Gun(4);
        $gun->reload('BlankBullet');

        foreach ($gun->getBullets() as $bullet) {
            $this->assertSame(
                Flyweight\BulletFactory::getBullet('BlankBullet'),
                $bullet
            );
        }
    }

    public function testExtrinsicState()
    {
        $gun = new Flyweight\Gun(2);
        $gun->reload('BlankBullet');

        $this->assertSame(
            'Bullet n°1 fired! (Phpatterns\Structural\Flyweight\Bullet\BlankBullet)',
            $gun->fire()
        );

        $this->assertSame(
            'Bullet n°2 fired! (Phpatterns\Structural\Flyweight\Bullet\BlankBullet)',
            $gun->fire()
        );

        $this->assertSame(
            'Reload gun!',
            $gun->fire()
        );
    }
}