 
                PHP로 작성된 비지터
비지터는 기존 코드를 변경하지 않고 기존 클래스 계층구조에 새로운 행동들을 추가할 수 있도록 하는 행동 디자인 패턴입니다.
제 설명글 [비지터와 이중 디스패치]{비지터와 이중 디스패치}에서 왜 단순히 비지터들을 메서드 오버로딩으로 대체할 수 없는지 알아보세요.
복잡도:
인기도:
사용 사례들: 비지터는 복잡하고 적용 범위가 좁으므로 PHP 언어에서 매우 일반적으로 사용되는 패턴이 아닙니다.
개념적인 예시
이 예시는 비지터 패턴의 구조를 보여주고 다음 질문에 중점을 둡니다:
- 패턴은 어떤 클래스들로 구성되어 있나요?
- 이 클래스들은 어떤 역할을 하나요?
- 패턴의 요소들은 어떻게 서로 연관되어 있나요?
이 패턴의 구조를 배우면 실제 PHP 사용 사례를 기반으로 하는 다음 예시를 더욱 쉽게 이해할 수 있을 것입니다.
index.php: 개념적인 예시
<?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: 실행 결과
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;
/**
 * 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: 실행 결과
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)