PHP: Відвідувач

Visitor Відвідувач Visitor

Відвідувач — це поведінковий патерн, який дозволяє додати нову операцію для цілої ієрархії класів, не змінюючи код цих класів.

Детальніше про те, чому Відвідувач не можна замінити звичайним перевантаженням методів читайте в статті Відвідувач і Double Dispatch.

Детальніше про Відвідувача

Особливості паттерна на PHP

Складність:

Популярність:

Застосування: Відвідувач нечасто зустрічається в PHP-коді внаслідок своєї складності та особливостей реалізації.

Приклад: Структура патерну

Цей приклад показує структуру патерну Відвідувач, а саме — з яких класів він складається, які ролі ці класи виконують і як вони взаємодіють один з одним. Після ознайомлення зі структурою, вам буде легше сприймати наступний приклад, що розглядає реальний випадок використання патерну в світі PHP.

VisitorStructural.php: Приклад структури патерну

<?php

namespace RefactoringGuru\Visitor\Structural;

/**
 * The Component interface declares an `accept` method that should take the base
 * visitor interface as an argument.
 */
interface Component
{
    function accept(Visitor $visitor);
}

/**
 * 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.
     */
    function accept(Visitor $visitor)
    {
        $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.
     */
    function exclusiveMethodOfConcreteComponentA()
    {
        return "A";
    }
}

class ConcreteComponentB implements Component
{
    /**
     * Same here: visitConcreteComponentB => ConcreteComponentB
     */
    function accept(Visitor $visitor)
    {
        $visitor->visitConcreteComponentB($this);
    }

    function specialMethodOfConcreteComponentB()
    {
        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);

    public function visitConcreteComponentB(ConcreteComponentB $element);
}

/**
 * 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)
    {
        print($element->exclusiveMethodOfConcreteComponentA()." + ConcreteVisitor1\n");
    }

    public function visitConcreteComponentB(ConcreteComponentB $element)
    {
        print($element->specialMethodOfConcreteComponentB()." + ConcreteVisitor1\n");
    }
}

class ConcreteVisitor2 implements Visitor
{
    public function visitConcreteComponentA(ConcreteComponentA $element)
    {
        print($element->exclusiveMethodOfConcreteComponentA()." + ConcreteVisitor2\n");
    }

    public function visitConcreteComponentB(ConcreteComponentB $element)
    {
        print($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(),
];

print("The client code works with all visitors via the base Visitor interface:\n");
$visitor1 = new ConcreteVisitor1();
clientCode($components, $visitor1);
print("\n");

print("It allows the same client code to work with different types of visitors:\n");
$visitor2 = new ConcreteVisitor2();
clientCode($components, $visitor2);

Output.txt: Результат виконання

Client code works with visitors through a common Visitor interface:
A + ConcreteVisitor1
B + ConcreteVisitor1

Same client code can work with different visitors:
A + ConcreteVisitor1
B + ConcreteVisitor2

Приклад: Приклад з життя

У цьому прикладі патерн Відвідувач допомагає впровадити функцію звітності в існуючу ієрархію класів: Компанія > Відділ > Співробітник

Після реалізації Відвідувача ви можете легко додавати в програму інші подібні поведінки не змінюючи існуючі класи.

VisitorRealWorld.php: Приклад з життя

<?php

namespace RefactoringGuru\Visitor\RealWorld;

/**
 * Visitor Design Pattern
 *
 * Intent: Represent an operation to be performed over elements of an object
 * structure. The Visitor pattern lets you define a new operation without
 * changing the classes of the elements on which it operates.
 *
 * Example: In this example, the Visitor pattern helps to introduce a reporting
 * feature into an existing class hierarchy:
 *
 * Company > Department > Employee
 *
 * Once the Visitor is addred to the app, you can easily add other similar
 * behaviors to app, without changing the existing classes.
 */

/**
 * 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);
}

/**
 * The Company Concrete Component.
 */
class Company implements Entity
{
    private $name;

    /**
     * @var Department[]
     */
    private $departments;

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

    public function getName()
    {
        return $this->name;
    }

    public function getDepartments()
    {
        return $this->departments;
    }

    // ...

    public function accept(Visitor $visitor)
    {
        // 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($name, $employees)
    {
        $this->name = $name;
        $this->employees = $employees;
    }

    public function getName()
    {
        return $this->name;
    }

    public function getEmployees()
    {
        return $this->employees;
    }

    public function getCost()
    {
        $cost = 0;
        foreach ($this->employees as $employee) {
            $cost += $employee->getSalary();
        }

        return $cost;
    }

    // ...

    public function accept(Visitor $visitor)
    {
        return $visitor->visitDepartment($this);
    }
}

/**
 * The Employee Concrete Component.
 */
class Employee implements Entity
{
    private $name;

    private $position;

    private $salary;

    public function __construct($name, $position, $salary)
    {
        $this->name = $name;
        $this->position = $position;
        $this->salary = $salary;
    }

    public function getName()
    {
        return $this->name;
    }

    public function getPosition()
    {
        return $this->position;
    }

    public function getSalary()
    {
        return $this->salary;
    }

    // ...

    public function accept(Visitor $visitor)
    {
        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);

    public function visitDepartment(Department $department);

    public function visitEmployee(Employee $employee);
}

/**
 * The Concrete Visitor must provide implementations for every single class of
 * the Concrete Components.
 */
class SalaryReport implements Visitor
{
    public function visitCompany(Company $company)
    {
        $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)
    {
        $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)
    {
        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();

print("Client: I can print a report for a whole company:\n\n");
print($company->accept($report));

print("\nClient: ...or just for a single department:\n\n");
print($techSupport->accept($report));

// $export = new JSONExport(); 
// print($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 just for a single department:

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)