访问者模式

目的

访问者模式允许你将对象的操作外包给其他对象,这样做的主要原因是为了保持关注点的分离。但是,类必须定义一个允许访问者访问的契约(比如,下面例子中的 Role::accept() 方法)。

这个契约是一个抽象类,但也可以是一个接口。在那种情况下,每个访问者必须选择对访问者调用哪个方法。

UML 类图

访问者模式类图

代码

RoleVisitor.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php declare(strict_types = 1);

namespace DesignPatterns\Behavioral\Visitor;

/**
* Note: the visitor must not choose itself which method to
* invoke, it is the visited object that makes this decision
*/
interface RoleVisitor
{
public function visitUser(User $role);

public function visitGroup(Group $role);
}

RecordingVisitor.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
<?php declare(strict_types = 1);

namespace DesignPatterns\Behavioral\Visitor;

class RecordingVisitor implements RoleVisitor
{
/**
* @var Role[]
*/
private array $visited = [];

public function visitGroup(Group $role)
{
$this->visited[] = $role;
}

public function visitUser(User $role)
{
$this->visited[] = $role;
}

/**
* @return Role[]
*/
public function getVisited(): array
{
return $this->visited;
}
}

Role.php

1
2
3
4
5
6
7
8
<?php declare(strict_types = 1);

namespace DesignPatterns\Behavioral\Visitor;

interface Role
{
public function accept(RoleVisitor $visitor);
}

User.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php declare(strict_types = 1);

namespace DesignPatterns\Behavioral\Visitor;

class User implements Role
{
private string $name;

public function __construct(string $name)
{
$this->name = $name;
}

public function getName(): string
{
return sprintf('User %s', $this->name);
}

public function accept(RoleVisitor $visitor)
{
$visitor->visitUser($this);
}
}

Group.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php declare(strict_types = 1);

namespace DesignPatterns\Behavioral\Visitor;

class Group implements Role
{
private string $name;

public function __construct(string $name)
{
$this->name = $name;
}

public function getName(): string
{
return sprintf('Group: %s', $this->name);
}

public function accept(RoleVisitor $visitor)
{
$visitor->visitGroup($this);
}
}

测试

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
<?php declare(strict_types = 1);

namespace DesignPatterns\Tests\Visitor\Tests;

use DesignPatterns\Behavioral\Visitor\RecordingVisitor;
use DesignPatterns\Behavioral\Visitor\User;
use DesignPatterns\Behavioral\Visitor\Group;
use DesignPatterns\Behavioral\Visitor\Role;
use DesignPatterns\Behavioral\Visitor;
use PHPUnit\Framework\TestCase;

class VisitorTest extends TestCase
{
private RecordingVisitor $visitor;

protected function setUp(): void
{
$this->visitor = new RecordingVisitor();
}

public function provideRoles()
{
return [
[new User('Dominik')],
[new Group('Administrators')],
];
}

/**
* @dataProvider provideRoles
*/
public function testVisitSomeRole(Role $role)
{
$role->accept($this->visitor);
$this->assertSame($role, $this->visitor->getVisited()[0]);
}
}