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)