11.4 访问者模式
11.4.1 问题
我们来看之前创建的战斗单元的游戏:
class Army extends CompositeUnit {
function bombardStrength() {
$ret = 0;
foreach ($this->units() as $unit) {
$ret += $unit->bombardStrength();
}
return $ret;
}
}
class LaserCannonUnit extends Unit {
function bombardStrength() {
return 10;
}
}
在这个游戏中,组合对象会调用它们的子对象来进行操作,而它们的子对象则会自己完成调用操作,即组合对象通过子对象完成的操作形成了自己的操作。
这个方式在操作比较容易的情况下比较容易实现,但是还有很多周边任务就会显得不那么好实现了。
比如,有一个转储叶节点的文本信息的操作,该操作会被添加到抽象类 Unit 中。
// Unit类 其余的内容省略
function textDump($num=0) {
$ret = "";
$pad = 4*$num;
$ret .= sprintf("%{$pad}s","");
$ret .= get_class($this).": ";
$ret .= "bombard: ".$this->bombardStrength();
return $ret;
}
然后这个方法可以在 CompositeUnit 中被覆盖:
// CompositeUnit类 其余的内容省略
function textDump($num=0) {
$ret = parent::textDump($num);
foreach ($this->units as $unit) {
$ret .= $unit->textDump($num+1);
}
return $ret;
}
我们可能还需要继续创建统计树种单元个数的方法、保存组件到数据库的方法和计算军队的食物消耗的方法。
虽然可以轻松遍历组合模式,但并非每个需要遍历对象树的操作都要在 Composite 接口中占据位置。因此,工作的重心便是充分利用对象结构提供的轻松遍历的优势,但同时避免类过度膨胀。
11.4.2 实现
首先在 Unit 抽象类中定义一个 accept() 方法:
// Unit类 其余的内容省略
function accept(ArmyVisitor $visit) {
$method = "visit".get_class($this);
$visit->$method($this);
}
protected function setDepth($depth) {
$this->depth = $depth;
}
function getDepth() {
return $this->depth;
}
accept() 方法要求一个 ArmyVisitor 对象作为参数,函数内动态定义一个我们希望调用的方法,这让我们不必在类的继承体系中每一个叶节点上实现 accept()。同时, 添加两个 getDepth() 和 setDepth() 方法,这两个方法在树中类获取和设置一个单元的深度,父单元通过 CompositeUnit::addUnit() 添加子单元时,setDepth() 方法被调用。
function addUnit(Unit $unit) {
foreach ($this->units as $thisunit) {
if ($unit == $thisunit) {
return;
}
$unit->setDepth($this->depth+1);
$this->units[] = $unit;
}
}
接下来我们在抽象的组合类中定义另一个 accept() 方法:
function accept(ArmyVisitor $visit) {
$method = "visit".get_class($this);
$visit->$method($this);
foreach ($this->units as $thisunit) {
$thisunit->accept($visit);
}
}
这个 accept() 方法和 Unit::accept() 方法基本一样,但是多了些内容。它根据当前类的名称构造了一个方法名称,然后通过传入的 ArmyVisitor 对象来调用对应的方法。因此,如果当前类是 Army,则该方法调用ArmyVisitor::visitArmy();如果当前类是TroopCarrier,则调用ArmyVisitor::visitTroopCarrier();依次类推。在此之后,accept() 方法会遍历所有子对象,并调用子对象的accept() 方法。
所以在子对象中,我们可以做一些去除重复性的代码:
function accept(ArmyVisitor $visitor) {
/* 通过调用抽象类Unit的accept()来去重 */
parent::accept($visitor);
foreach ($this->units as $thisunit) {
$thisunit->accept($visitor);
}
}
我们还需要定义一个 ArmyVisitor 接口。
abstract class ArmyVisitor {
/* 定义一个抽象的visit()方法,具体实现交给子类实现 */
abstract function visit(Unit $node) {}
function visitArcher(Archer $node) {
$this->visit($node);
}
function visitCavalry(Cavalry $node) {
$this->visit($node);
}
function visitLaserCannonUnit(LaserCannonUnit $node) {
$this->visit($node);
}
function visitTroopCarrierUnit(TroopCarrierUnit $node) {
$this->visit($node);
}
function visitArmy(Army $node) {
$this->visit($node);
}
}
现在,我们具体实现一个子类TextDumpVisitor,用于转储文本:
class TextDumpArmyVisitor extends ArmyVisitor {
private $text = "";
function visit(Unit $node) {
$ret = "";
$pad = 4*$node->getDepth();
$ret .= sprintf("%{$pad}s","");
$ret .= get_class($node).": ";
$ret .= "bombard: ".$node->bombardStrength()."\n";
$this->text .= $ret;
}
function getText() {
return $this->text;
}
}
我们通过客户端调用:
/* 创建一个Army对象,并向其中添加Unit对象 */
$main_army = new Army();
$main_army->addUnit(new Archer());
$main_army->addUnit(new LaserCannonUnit());
$main_army->addUnit(new Cavalry());
/* 创建一个TextDumpArmyVisitor对象,并将其添加到Army::accept(TextDumpArmyVisitor)中,这里面会产生很多调用
首先会产生一个TextDumpArmyVisitor::visitArmy()方法,这个方法会产生一次调用TextDumpArmyVisitor::visit()方法
然后会进入foreach循环,依次调用
TextDumpArmyVisitor::visitArcher()方法,这个方法产生一次调用TextDumpArmyVisitor::visit()方法
TextDumpArmyVisitor::visitLaserCannonUnit()方法,这个方法产生一次调用TextDumpArmyVisitor::visit()方法
TextDumpArmyVisitor::visitCavalry()方法,这个方法产生一次调用TextDumpArmyVisitor::visit()方法
visit()方法最终会写入到$text变量中,最后通过echo输出,就会有四行输出内容
*/
$textDump = new TextDumpArmyVisitor();
$main_army->accept($textDump);
echo $textDump->getText();
首先创建一个 Army 对象,并向其中添加 Unit 对象,再创建一个 TextDumpArmyVisitor 对象,并将其添加到 Army::accept(TextDumpArmyVisitor) 中,这里面会产生很多调用:
- 首先会产生一个 TextDumpArmyVisitor::visitArmy() 方法,这个方法会产生一次调用 TextDumpArmyVisitor::visit() 方法;
- 然后会进入 foreach 循环,依次调用:
TextDumpArmyVisitor::visitArcher() 方法,这个方法产生一次调用 TextDumpArmyVisitor::visit() 方法;
TextDumpArmyVisitor::visitLaserCannonUnit() 方法,这个方法产生一次调用TextDumpArmyVisitor::visit() 方法;
TextDumpArmyVisitor::visitCavalry() 方法,这个方法产生一次调用 TextDumpArmyVisitor::visit() 方法; - visit() 方法最终会写入到$text变量中,最后通过echo输出,就会有四行输出内容。
以上代码最终输出成:
// 输出
Army: bombard: 15
Archer: bombard: 1
LaserCannonUnit: bombard: 10
Cavalry: bombard: 4
现在我们进行新的扩展,假设军队需要缴纳税金。征税者(即访问者visitor)访问军队,并向找到的每个单位征税,不同单位的税率不同,因此,我们定义一个TaxCollectionVistior 类,用于处理对每一个单位征税不同的税。
class TaxCollectionVisitor extends ArmyVisitor {
private $due=0;
private $report="";
function visit(Unit $node) {
$this->levy($node,1);
}
function visitArcher(Archer $node) {
$this->levy($node,2);
}
function visitCavalry(Cavalry $node) {
$this->levy($node,3);
}
function visitTroopCarrierUnit(TroopCarrierUnit $node) {
$this->levy($node,5);
}
private function levy(Unit $unit, $amount) {
$this->report .= "Tax levied for ".get_class($unit);
$this->report .= ": $amount";
$this->due = $amount;
}
function getReport() {
return $this->report;
}
function getTax() {
return $this->due;
}
}
调用的方式和之前的一样:
/* 创建一个Army对象,并向其中添加Unit对象 */
$main_army = new Army();
$main_army->addUnit(new Archer());
$main_army->addUnit(new LaserCannonUnit());
$main_army->addUnit(new Cavalry());
/* 创建一个TaxCollectionVisitor对象,并将其添加到Army::accept(TaxCollectionVisitor)中,这里面会产生很多调用
首先会产生一个TaxCollectionVisitor::visitArmy()方法,这个方法会产生一次调用TaxCollectionVisitor::visit()方法
然后会进入foreach循环,依次调用
TaxCollectionVisitor::visitArcher()方法,这个方法产生一次调用TaxCollectionVisitor::visit()方法
TaxCollectionVisitor::visitLaserCannonUnit()方法,这个方法产生一次调用TaxCollectionVisitor::visit()方法
TaxCollectionVisitor::visitCavalry()方法,这个方法产生一次调用TaxCollectionVisitor::visit()方法
visit()方法最终会写入到$due变量中,最后通过echo输出,就会有四行输出内容
*/
$taxcollector = new TaxCollectionVisitor();
$main_army->accept($taxcollector);
echo $taxcollector->getTax();
最终输出:
// 输出
Tax levied for Army: 1
Tax levied for Archer: 2
Tax levied for LaserCannonUnit: 3
Tax levied for Cavalry: 4