![Visitor](/images/patterns/cards/visitor-mini.png?id=854a35a62963bec1d75eab996918989b)
Visitor en PHP
Visitor es un patrón de diseño de comportamiento que permite añadir nuevos comportamientos a una jerarquía de clases existente sin alterar el código.
Lee por qué el patrón Visitor no puede simplemente sustituirse por la sobrecarga de métodos, en nuestro artículo Visitor y envío doble.
Complejidad:
Popularidad:
Ejemplos de uso: El patrón Visitor no es muy habitual en el código PHP debido a su complejidad y limitada aplicabilidad.
Ejemplo conceptual
Este ejemplo ilustra la estructura del patrón de diseño Visitor y se centra en las siguientes preguntas:
- ¿De qué clases se compone?
- ¿Qué papeles juegan esas clases?
- ¿De qué forma se relacionan los elementos del patrón?
Después de conocer la estructura del patrón, será más fácil comprender el siguiente ejemplo basado en un caso de uso real de PHP.
index.php: Ejemplo conceptual
<?php
namespace RefactoringGuru\Visitor\Conceptual;
/**
* The Component interface declares an `accept` method that should take the base
* visitor interface as an argument.
*/
interface Component
{
public function accept(Visitor $visitor): void;
}
/**
* Each Concrete Component must implement the `accept` method in such a way that
* it calls the visitor's method corresponding to the component's class.
*/
class ConcreteComponentA implements Component
{
/**
* Note that we're calling `visitConcreteComponentA`, which matches the
* current class name. This way we let the visitor know the class of the
* component it works with.
*/
public function accept(Visitor $visitor): void
{
$visitor->visitConcreteComponentA($this);
}
/**
* Concrete Components may have special methods that don't exist in their
* base class or interface. The Visitor is still able to use these methods
* since it's aware of the component's concrete class.
*/
public function exclusiveMethodOfConcreteComponentA(): string
{
return "A";
}
}
class ConcreteComponentB implements Component
{
/**
* Same here: visitConcreteComponentB => ConcreteComponentB
*/
public function accept(Visitor $visitor): void
{
$visitor->visitConcreteComponentB($this);
}
public function specialMethodOfConcreteComponentB(): string
{
return "B";
}
}
/**
* The Visitor Interface declares a set of visiting methods that correspond to
* component classes. The signature of a visiting method allows the visitor to
* identify the exact class of the component that it's dealing with.
*/
interface Visitor
{
public function visitConcreteComponentA(ConcreteComponentA $element): void;
public function visitConcreteComponentB(ConcreteComponentB $element): void;
}
/**
* Concrete Visitors implement several versions of the same algorithm, which can
* work with all concrete component classes.
*
* You can experience the biggest benefit of the Visitor pattern when using it
* with a complex object structure, such as a Composite tree. In this case, it
* might be helpful to store some intermediate state of the algorithm while
* executing visitor's methods over various objects of the structure.
*/
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";
}
}
/**
* The client code can run visitor operations over any set of elements without
* figuring out their concrete classes. The accept operation directs a call to
* the appropriate operation in the visitor object.
*/
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: Resultado de la ejecución
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
Ejemplo del mundo real
En este ejemplo, el patrón Visitor ayuda a introducir una función de reporte en una jerarquía de clases existente: Compañía > Departamento > Empleado
Una vez que la infraestructura se añade a la aplicación, puedes añadir fácilmente otros comportamientos similares sin cambiar las clases existentes.
index.php: Ejemplo del mundo real
<?php
namespace RefactoringGuru\Visitor\RealWorld;
/**
* The Component interface declares a method of accepting visitor objects.
*
* In this method, a Concrete Component must call a specific Visitor's method
* that has the same parameter type as that component.
*/
interface Entity
{
public function accept(Visitor $visitor): string;
}
/**
* The Company Concrete Component.
*/
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
{
// See, the Company component must call the visitCompany method. The
// same principle applies to all components.
return $visitor->visitCompany($this);
}
}
/**
* The Department Concrete Component.
*/
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);
}
}
/**
* The Employee Concrete Component.
*/
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);
}
}
/**
* The Visitor interface declares a set of visiting methods for each of the
* Concrete Component classes.
*/
interface Visitor
{
public function visitCompany(Company $company): string;
public function visitDepartment(Department $department): string;
public function visitEmployee(Employee $employee): string;
}
/**
* The Concrete Visitor must provide implementations for every single class of
* the Concrete Components.
*/
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";
}
}
/**
* The client code.
*/
$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: Resultado de la ejecución
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)