1.11. Visitor

1.11.1. Intent

According to the Gang of Four, the Visitor pattern is a way to “represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates” (Design Patterns: Elements of Reusable Object-Oriented Software, 2013, p. 331).

The Visitor Pattern requires the ability to determine (dynamically) the type of the Visitor and the type of the visited object to be able to find which operation to execute. To do that, it implements the double dispatch mechanism.

1.11.2. When to use it?

The Visitor pattern should be used in various cases:

  • an object structure contains different classes with different interfaces and you want to perform operations depending on their concrete classes.
  • you want to perform distinct and unrelated operations on objects without polluting their concrete classes.
  • the object structure will not change and you want to easily add new operations. Just add new Visitors and let the object structure unchanged.

1.11.3. Diagram

Created using PhpStorm and yFiles.

Class diagram of the Visitor pattern

1.11.4. Implementation

StoreVisitorInterface.php

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

namespace Phpatterns\Behavioral\Visitor;

use Phpatterns\Behavioral\Visitor\Store;

interface StoreVisitorInterface
{
    /**
     * Visit the Apple store.
     * @param Store\AppleStore $store
     */
    public function visitApple(Store\AppleStore $store);

    /**
     * Visit the Amazon store.
     * @param Store\AmazonStore $store
     */
    public function visitAmazon(Store\AmazonStore $store);
}

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

namespace Phpatterns\Behavioral\Visitor\StoreVisitor;

use Phpatterns\Behavioral\Visitor;
use Phpatterns\Behavioral\Visitor\Store;

class ProductSortVisitor implements Visitor\StoreVisitorInterface
{
    /**
     * Visit the Apple store and sort the products (ascending sort).
     * @param Store\AppleStore $store
     */
    public function visitApple(Store\AppleStore $store)
    {
        $products = $store->getProducts();
        sort($products);
        $store->setProducts($products);
    }

    /**
     * Visit the Amazon store and sort the products (descending sort).
     * @param Store\AmazonStore $store
     */
    public function visitAmazon(Store\AmazonStore $store)
    {
        $products = $store->getProducts();
        rsort($products);
        $store->setProducts($products);
    }
}

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

namespace Phpatterns\Behavioral\Visitor;

abstract class AbstractStore
{
    /** @var array */
    protected $products;

    /**
     * The "double dispatch" mechanism will live here.
     * @param StoreVisitorInterface $visitor
     */
    abstract public function acceptVisitor(StoreVisitorInterface $visitor);

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

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

AmazonStore.php

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

namespace Phpatterns\Behavioral\Visitor\Store;

use Phpatterns\Behavioral\Visitor;

class AmazonStore extends Visitor\AbstractStore
{
    public function __construct()
    {
        $this->products = ['1984 - George Orwell', 'Hamlet - William Shakespeare', 'The Stranger - Albert Camus'];
    }

    public function acceptVisitor(Visitor\StoreVisitorInterface $visitor)
    {
        $visitor->visitAmazon($this);
    }
}

AppleStore.php

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

namespace Phpatterns\Behavioral\Visitor\Store;

use Phpatterns\Behavioral\Visitor;

class AppleStore extends Visitor\AbstractStore
{
    public function __construct()
    {
        $this->products = ['MacBook Air', 'Mac Pro', 'Apple Watch'];
    }

    public function acceptVisitor(Visitor\StoreVisitorInterface $visitor)
    {
        $visitor->visitApple($this);
    }
}

1.11.5. Tests

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

namespace Test\Phpatterns\Behavioral\Visitor;

use Phpatterns\Behavioral\Visitor;
use Phpatterns\Behavioral\Visitor\Store;
use Phpatterns\Behavioral\Visitor\StoreVisitor;

class VisitorTest extends \PHPUnit_Framework_TestCase
{
    /** @var Visitor\StoreVisitorInterface */
    private $storeVisitor;

    protected function setUp()
    {
        $this->storeVisitor = new StoreVisitor\ProductSortVisitor();
    }

    /**
     * Testing the visitor mechanism
     * @param Store\AmazonStore $store
     * @param array $products
     * @dataProvider storeProvider
     */
    public function testVisitorMechanism($store, $products)
    {
        $this->assertNotSame($products, $store->getProducts());
        $store->acceptVisitor($this->storeVisitor);
        $this->assertSame($products, $store->getProducts());
    }

    public function storeProvider()
    {
        return [
            [
                new Store\AmazonStore(),
                ['The Stranger - Albert Camus', 'Hamlet - William Shakespeare', '1984 - George Orwell']
            ],
            [
                new Store\AppleStore(),
                ['Apple Watch', 'Mac Pro', 'MacBook Air']
            ]
        ];
    }
}