3.13. Odwiedzający (Visitor)

3.13.1. Przeznaczenie

Wzorzec Odwiedzającego pozwala wydzielić wykonywanie operacji na obiektach do innych obiektów. Głównym powodem stosowania tego wzorca jest podział odpowiedzialności (ang. separation of concerns). Poszczególne klasy muszą definiować kontrakt pozwalający Wizytatorowi na dostęp (w poniższym przykładzie za pomocą metody Role::accept)

W poniższym przykładzie kontrakt jest klasą abstrakcyjną, ale może to być również interfejs. Tworząc taki kontrakt obiekt odwiedzany musi zdecydować jaką metodę wywołać na Wizytatorze.

3.13.2. Diagram UML

Alt Visitor UML Diagram

3.13.3. Kod

Ten kod znajdziesz również na GitHub.

RoleVisitor.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Visitor;
 6
 7/**
 8 * Note: the visitor must not choose itself which method to
 9 * invoke, it is the visited object that makes this decision
10 */
11interface RoleVisitor
12{
13    public function visitUser(User $role);
14
15    public function visitGroup(Group $role);
16}

RecordingVisitor.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Visitor;
 6
 7class RecordingVisitor implements RoleVisitor
 8{
 9    /**
10     * @var Role[]
11     */
12    private array $visited = [];
13
14    public function visitGroup(Group $role)
15    {
16        $this->visited[] = $role;
17    }
18
19    public function visitUser(User $role)
20    {
21        $this->visited[] = $role;
22    }
23
24    /**
25     * @return Role[]
26     */
27    public function getVisited(): array
28    {
29        return $this->visited;
30    }
31}

Role.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Visitor;
 6
 7interface Role
 8{
 9    public function accept(RoleVisitor $visitor);
10}

User.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Visitor;
 6
 7class User implements Role
 8{
 9    public function __construct(private string $name)
10    {
11    }
12
13    public function getName(): string
14    {
15        return sprintf('User %s', $this->name);
16    }
17
18    public function accept(RoleVisitor $visitor)
19    {
20        $visitor->visitUser($this);
21    }
22}

Group.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Visitor;
 6
 7class Group implements Role
 8{
 9    public function __construct(private string $name)
10    {
11    }
12
13    public function getName(): string
14    {
15        return sprintf('Group: %s', $this->name);
16    }
17
18    public function accept(RoleVisitor $visitor)
19    {
20        $visitor->visitGroup($this);
21    }
22}

3.13.4. Testy

Tests/VisitorTest.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Tests\Visitor\Tests;
 6
 7use DesignPatterns\Behavioral\Visitor\RecordingVisitor;
 8use DesignPatterns\Behavioral\Visitor\User;
 9use DesignPatterns\Behavioral\Visitor\Group;
10use DesignPatterns\Behavioral\Visitor\Role;
11use DesignPatterns\Behavioral\Visitor;
12use PHPUnit\Framework\TestCase;
13
14class VisitorTest extends TestCase
15{
16    private RecordingVisitor $visitor;
17
18    protected function setUp(): void
19    {
20        $this->visitor = new RecordingVisitor();
21    }
22
23    public function provideRoles()
24    {
25        return [
26            [new User('Dominik')],
27            [new Group('Administrators')],
28        ];
29    }
30
31    /**
32     * @dataProvider provideRoles
33     */
34    public function testVisitSomeRole(Role $role)
35    {
36        $role->accept($this->visitor);
37        $this->assertSame($role, $this->visitor->getVisited()[0]);
38    }
39}