Концептуальный пример
Этот пример показывает структуру паттерна Посетитель , а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.
index.php: Пример структуры паттерна
<?php
namespace RefactoringGuru\Visitor\Conceptual;
/**
* Интерфейс Компонента объявляет метод accept, который в качестве аргумента
* может получать любой объект, реализующий интерфейс посетителя.
*/
interface Component
{
public function accept(Visitor $visitor): void;
}
/**
* Каждый Конкретный Компонент должен реализовать метод accept таким образом,
* чтобы он вызывал метод посетителя, соответствующий классу компонента.
*/
class ConcreteComponentA implements Component
{
/**
* Обратите внимание, мы вызываем visitConcreteComponentA, что соответствует
* названию текущего класса. Таким образом мы позволяем посетителю узнать, с
* каким классом компонента он работает.
*/
public function accept(Visitor $visitor): void
{
$visitor->visitConcreteComponentA($this);
}
/**
* Конкретные Компоненты могут иметь особые методы, не объявленные в их
* базовом классе или интерфейсе. Посетитель всё же может использовать эти
* методы, поскольку он знает о конкретном классе компонента.
*/
public function exclusiveMethodOfConcreteComponentA(): string
{
return "A";
}
}
class ConcreteComponentB implements Component
{
/**
* То же самое здесь: visitConcreteComponentB => ConcreteComponentB
*/
public function accept(Visitor $visitor): void
{
$visitor->visitConcreteComponentB($this);
}
public function specialMethodOfConcreteComponentB(): string
{
return "B";
}
}
/**
* Интерфейс Посетителя объявляет набор методов посещения, соответствующих
* классам компонентов. Сигнатура метода посещения позволяет посетителю
* определить конкретный класс компонента, с которым он имеет дело.
*/
interface Visitor
{
public function visitConcreteComponentA(ConcreteComponentA $element): void;
public function visitConcreteComponentB(ConcreteComponentB $element): void;
}
/**
* Конкретные Посетители реализуют несколько версий одного и того же алгоритма,
* которые могут работать со всеми классами конкретных компонентов.
*
* Максимальную выгоду от паттерна Посетитель вы почувствуете, используя его со
* сложной структурой объектов, такой как дерево Компоновщика. В этом случае
* было бы полезно хранить некоторое промежуточное состояние алгоритма при
* выполнении методов посетителя над различными объектами структуры.
*/
class ConcreteVisitor1 implements Visitor
{
public function visitConcreteComponentA(ConcreteComponentA $element): void
{
echo $element->exclusiveMethodOfConcreteComponentA() . " + ConcreteVisitor1\n";
}
public function visitConcreteComponentB(ConcreteComponentB $element): void
{
echo $element->specialMethodOfConcreteComponentB() . " + ConcreteVisitor1\n";
}
}
class ConcreteVisitor2 implements Visitor
{
public function visitConcreteComponentA(ConcreteComponentA $element): void
{
echo $element->exclusiveMethodOfConcreteComponentA() . " + ConcreteVisitor2\n";
}
public function visitConcreteComponentB(ConcreteComponentB $element): void
{
echo $element->specialMethodOfConcreteComponentB() . " + ConcreteVisitor2\n";
}
}
/**
* Клиентский код может выполнять операции посетителя над любым набором
* элементов, не выясняя их конкретных классов. Операция принятия направляет
* вызов к соответствующей операции в объекте посетителя.
*/
function clientCode(array $components, Visitor $visitor)
{
// ...
foreach ($components as $component) {
$component->accept($visitor);
}
// ...
}
$components = [
new ConcreteComponentA(),
new ConcreteComponentB(),
];
echo "The client code works with all visitors via the base Visitor interface:\n";
$visitor1 = new ConcreteVisitor1();
clientCode($components, $visitor1);
echo "\n";
echo "It allows the same client code to work with different types of visitors:\n";
$visitor2 = new ConcreteVisitor2();
clientCode($components, $visitor2);
Output.txt: Результат выполнения
The client code works with all visitors via the base Visitor interface:
A + ConcreteVisitor1
B + ConcreteVisitor1
It allows the same client code to work with different types of visitors:
A + ConcreteVisitor2
B + ConcreteVisitor2
Пример из реальной жизни
В этом примере паттерн Посетитель помогает внедрить функцию отчётности в существующую иерархию классов: Компания > Отдел > Сотрудник
После реализации Посетителя вы можете легко добавлять в приложение другие подобные поведения без изменения существующих классов.
index.php: Пример из реальной жизни
<?php
namespace RefactoringGuru\Visitor\RealWorld;
/**
* Интерфейс Компонента объявляет метод принятия объектов-посетителей.
*
* В этом методе Конкретный Компонент вызывает конкретный метод Посетителя, с
* тем же типом параметра, что и у компонента.
*/
interface Entity
{
public function accept(Visitor $visitor): string;
}
/**
* Конкретный Компонент Компании.
*/
class Company implements Entity
{
private $name;
/**
* @var Department[]
*/
private $departments;
public function __construct(string $name, array $departments)
{
$this->name = $name;
$this->departments = $departments;
}
public function getName(): string
{
return $this->name;
}
public function getDepartments(): array
{
return $this->departments;
}
// ...
public function accept(Visitor $visitor): string
{
// Смотрите, Компонент Компании должен вызвать метод visitCompany. Тот
// же принцип применяется ко всем компонентам.
return $visitor->visitCompany($this);
}
}
/**
* Конкретный Компонент Отдела.
*/
class Department implements Entity
{
private $name;
/**
* @var Employee[]
*/
private $employees;
public function __construct(string $name, array $employees)
{
$this->name = $name;
$this->employees = $employees;
}
public function getName(): string
{
return $this->name;
}
public function getEmployees(): array
{
return $this->employees;
}
public function getCost(): int
{
$cost = 0;
foreach ($this->employees as $employee) {
$cost += $employee->getSalary();
}
return $cost;
}
// ...
public function accept(Visitor $visitor): string
{
return $visitor->visitDepartment($this);
}
}
/**
* Конкретный Компонент Сотрудника.
*/
class Employee implements Entity
{
private $name;
private $position;
private $salary;
public function __construct(string $name, string $position, int $salary)
{
$this->name = $name;
$this->position = $position;
$this->salary = $salary;
}
public function getName(): string
{
return $this->name;
}
public function getPosition(): string
{
return $this->position;
}
public function getSalary(): int
{
return $this->salary;
}
// ...
public function accept(Visitor $visitor): string
{
return $visitor->visitEmployee($this);
}
}
/**
* Интерфейс Посетителя объявляет набор методов посещения для каждого класса
* Конкретного Компонента.
*/
interface Visitor
{
public function visitCompany(Company $company): string;
public function visitDepartment(Department $department): string;
public function visitEmployee(Employee $employee): string;
}
/**
* Конкретный Посетитель должен предоставить реализации для каждого из классов
* Конкретных Компонентов.
*/
class SalaryReport implements Visitor
{
public function visitCompany(Company $company): string
{
$output = "";
$total = 0;
foreach ($company->getDepartments() as $department) {
$total += $department->getCost();
$output .= "\n--" . $this->visitDepartment($department);
}
$output = $company->getName() .
" (" . money_format("%i", $total) . ")\n" . $output;
return $output;
}
public function visitDepartment(Department $department): string
{
$output = "";
foreach ($department->getEmployees() as $employee) {
$output .= " " . $this->visitEmployee($employee);
}
$output = $department->getName() .
" (" . money_format("%i", $department->getCost()) . ")\n\n" .
$output;
return $output;
}
public function visitEmployee(Employee $employee): string
{
return money_format("%#6n", $employee->getSalary()) .
" " . $employee->getName() .
" (" . $employee->getPosition() . ")\n";
}
}
/**
* Клиентский код.
*/
$mobileDev = new Department("Mobile Development", [
new Employee("Albert Falmore", "designer", 100000),
new Employee("Ali Halabay", "programmer", 100000),
new Employee("Sarah Konor", "programmer", 90000),
new Employee("Monica Ronaldino", "QA engineer", 31000),
new Employee("James Smith", "QA engineer", 30000),
]);
$techSupport = new Department("Tech Support", [
new Employee("Larry Ulbrecht", "supervisor", 70000),
new Employee("Elton Pale", "operator", 30000),
new Employee("Rajeet Kumar", "operator", 30000),
new Employee("John Burnovsky", "operator", 34000),
new Employee("Sergey Korolev", "operator", 35000),
]);
$company = new Company("SuperStarDevelopment", [$mobileDev, $techSupport]);
setlocale(LC_MONETARY, 'en_US');
$report = new SalaryReport();
echo "Client: I can print a report for a whole company:\n\n";
echo $company->accept($report);
echo "\nClient: ...or for different entities " .
"such as an employee, a department, or the whole company:\n\n";
$someEmployee = new Employee("Some employee", "operator", 35000);
$differentEntities = [$someEmployee, $techSupport, $company];
foreach ($differentEntities as $entity) {
echo $entity->accept($report) . "\r\n";
}
// $export = new JSONExport();
// echo $company->accept($export);
Output.txt: Результат выполнения
Client: I can print a report for a whole company:
SuperStarDevelopment (USD550,000.00)
--Mobile Development (USD351,000.00)
$100,000.00 Albert Falmore (designer)
$100,000.00 Ali Halabay (programmer)
$ 90,000.00 Sarah Konor (programmer)
$ 31,000.00 Monica Ronaldino (QA engineer)
$ 30,000.00 James Smith (QA engineer)
--Tech Support (USD199,000.00)
$ 70,000.00 Larry Ulbrecht (supervisor)
$ 30,000.00 Elton Pale (operator)
$ 30,000.00 Rajeet Kumar (operator)
$ 34,000.00 John Burnovsky (operator)
$ 35,000.00 Sergey Korolev (operator)
Client: ...or for different entities such as an employee, a department, or the whole company:
35000 Some employee (operator)
Tech Support (199000)
70000 Larry Ulbrecht (supervisor)
30000 Elton Pale (operator)
30000 Rajeet Kumar (operator)
34000 John Burnovsky (operator)
35000 Sergey Korolev (operator)
SuperStarDevelopment (550000)
--Mobile Development (351000)
100000 Albert Falmore (designer)
100000 Ali Halabay (programmer)
90000 Sarah Konor (programmer)
31000 Monica Ronaldino (QA engineer)
30000 James Smith (QA engineer)
--Tech Support (199000)
70000 Larry Ulbrecht (supervisor)
30000 Elton Pale (operator)
30000 Rajeet Kumar (operator)
34000 John Burnovsky (operator)
35000 Sergey Korolev (operator)