計算機組件(CPU、內存、鍵盤)需要支持多種操作:打印名稱、保存數據庫、顯示價格... 若在基類不斷添加方法,違反開閉原則,類會無限膨脹。
將數據結構(元素)與作用於其上的操作(訪問者)分離,使操作集合可自由演化而不修改元素類。
abstract class Unit {
abstract public function getName();
// 關鍵:接受訪問者,回調訪問者對應方法
public function accept(Visitor $visitor) {
$method = 'visit' . get_class($this); // visitCpu, visitMemory...
if (method_exists($visitor, $method)) {
$visitor->$method($this);
}
}
}
class Cpu extends Unit {
public function getName() { return 'i am cpu'; }
}
class Memory extends Unit {
public function getName() { return 'i am memory'; }
}
class Keyboard extends Unit {
public function getName() { return 'i am keyboard'; }
}
interface Visitor {
public function visitCpu(Cpu $cpu);
public function visitMemory(Memory $memory);
public function visitKeyboard(Keyboard $keyboard);
}
// 打印訪問者
class PrintVisitor implements Visitor {
public function visitCpu(Cpu $cpu) { echo "hello, " . $cpu->getName() . "\n"; }
public function visitMemory(Memory $m) { echo "hello, " . $m->getName() . "\n"; }
public function visitKeyboard(Keyboard $k) { echo "hello, " . $k->getName() . "\n"; }
}
// 數據庫保存訪問者(新增操作無需修改元素類)
class SaveVisitor implements Visitor {
public function visitCpu(Cpu $cpu) { /* save cpu */ }
public function visitMemory(Memory $m) { /* save memory */ }
public function visitKeyboard(Keyboard $k) { /* save keyboard */ }
}
// 價格訪問者
class PriceVisitor implements Visitor {
public function visitCpu(Cpu $cpu) { echo "CPU: $299\n"; }
public function visitMemory(Memory $m) { echo "Memory: $129\n"; }
public function visitKeyboard(Keyboard $k) { echo "Keyboard: $79\n"; }
}
class Computer {
private $_items = [];
public function add(Unit $unit) { $this->_items[] = $unit; }
public function accept(Visitor $visitor) {
foreach ($this->_items as $item) {
$item->accept($visitor); // 雙分派核心
}
}
}
$computer = new Computer(); $computer->add(new Cpu()); $computer->add(new Memory()); $computer->add(new Keyboard()); // 打印 $computer->accept(new PrintVisitor()); // hello, i am cpu // hello, i am memory // hello, i am keyboard // 保存 $computer->accept(new SaveVisitor()); // 顯示價格 $computer->accept(new PriceVisitor());
Client → Computer.accept(Visitor)
↓
Unit.accept(Visitor) ← 第一次分派:根據元素類型
↓
Visitor.visitCpu(Cpu) ← 第二次分派:根據訪問者類型
↓
具體業務邏輯
| 優點 | 缺點 |
|---|---|
| 新增操作只需新增 Visitor,不修改元素類 | 新增元素類需修改所有 Visitor 接口及實現 |
| 符合單一職責、開閉原則 | 破壞封裝(訪問者需訪問元素內部狀態) |
| 相關操作集中在一個 Visitor 中 | 適用於元素結構穩定、操作頻繁變化的場景 |
method_exists() 實現動態分派,減少接口約束IteratorAggregate 遍歷複雜結構