访问者模式.docx
《访问者模式.docx》由会员分享,可在线阅读,更多相关《访问者模式.docx(15页珍藏版)》请在冰豆网上搜索。
访问者模式
php
/**
*访问者模式
*表示一个作用于某对象结构中的各元素的操作,可以在不改变各元素的类的前提下定义作用于这些元素的新操作
*
*/
abstractclassVisitor//抽象访问者
{
abstractpublicfunctionvisitCroncreteElementA($element);
abstractpublicfunctionvisitCroncreteElementB($element);
}
classConcreteVisitor1extendsVisitor//具体访问者
{
publicfunctionvisitCroncreteElementA($element)
{
echoget_class($element)."visit1A
";
}
publicfunctionvisitCroncreteElementB($element)
{
echoget_class($element)."visit1B
";
}
}
classConcreteVisitor2extendsVisitor//具体访问者
{
publicfunctionvisitCroncreteElementA($element)
{
echoget_class($element)."visit2A
";
}
publicfunctionvisitCroncreteElementB($element)
{
echoget_class($element)."visit2B
";
}
}
abstractclassElement//抽象元素类
{
abstractpublicfunctionaccept($visitor);
}
classConcreteElementAextendsElement//具体元素类
{
publicfunctionaccept($visitor)
{
$visitor->visitCroncreteElementA($this);
}
}
classConcreteElementBextendsElement//具体元素类
{
publicfunctionaccept($visitor)
{
$visitor->visitCroncreteElementB($this);
}
}
classObjectStructure
{
private$_elements=array();
publicfunctionattach($element)
{
$this->_elements[]=$element;
}
publicfunctiondetach($element)
{
if($key=array_search($element,$this->_elements)!
==false)unset($this->_elements[$key]);
}
publicfunctionaccept($visitor)
{
foreach($this->_elementsas$element)
{
$element->accept($visitor);
}
}
}
$objOS=newObjectStructure();
$objOS->attach(newConcreteElementA());
$objOS->attach(newConcreteElementB());
$objCV1=newConcreteVisitor1();
$objCV2=newConcreteVisitor2();
$objOS->accept($objCV1);
$objOS->accept($objCV2);
结果:
ConcreteElementAvisit1A
ConcreteElementBvisit1B
ConcreteElementAvisit2A
ConcreteElementBvisit2B
在访问者模式中,主要包括下面几个角色:
1.抽象访问者:
抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是visit方法中的参数定义哪些对象是可以被访问的。
2.具体访问者:
实现抽象访问者所声明的方法,它影响到访问者访问到一个类后该干什么,要做什么事情。
3.抽象元素类:
接口或者抽象类,声明接受哪一类访问者访问,程序上是通过accept方法中的参数来定义的。
抽象元素一般有两类方法,一部分是本身的业务逻辑,另外就是允许接收哪类访问者来访问。
4.具体元素类:
实现抽象元素类所声明的accept方法,通常都是visitor.visit(this),基本上已经形成一种定式了。
5.结构对象:
一个元素的容器,一般包含一个容纳多个不同类、不同接口的容器,如List、Set、Map等,在项目中一般很少抽象出这个角色。
访问者模式的优点
1.符合单一职责原则:
凡是适用访问者模式的场景中,元素类中需要封装在访问者中的操作必定是与元素类本身关系不大且是易变的操作,使用访问者模式一方面符合单一职责原则,另一方面,因为被封装的操作通常来说都是易变的,所以当发生变化时,就可以在不改变元素类本身的前提下,实现对变化部分的扩展。
2.扩展性良好:
元素类可以通过接受不同的访问者来实现对不同操作的扩展。
访问者模式的适用场景
假如一个对象中存在着一些与本对象不相干(或者关系较弱)的操作,为了避免这些操作污染这个对象,则可以使用访问者模式来把这些操作封装到访问者中去。
假如一组对象中,存在着相似的操作,为了避免出现大量重复的代码,也可以将这些重复的操作封装到访问者中去。
但是,访问者模式并不是那么完美,它也有着致命的缺陷:
增加新的元素类比较困难。
通过访问者模式的代码可以看到,在访问者类中,每一个元素类都有它对应的处理方法,也就是说,每增加一个元素类都需要修改访问者类(也包括访问者类的子类或者实现类),修改起来相当麻烦。
也就是说,在元素类数目不确定的情况下,应该慎用访问者模式。
所以,访问者模式比较适用于对已有功能的重构,比如说,一个项目的基本功能已经确定下来,元素类的数据已经基本确定下来不会变了,会变的只是这些元素内的相关操作,这时候,我们可以使用访问者模式对原有的代码进行重构一遍,这样一来,就可以在不修改各个元素类的情况下,对原有功能进行修改。
访问者模式的优点
1、访问者模式使得增加新的操作变得很容易。
如果一些操作依赖于一个复杂的结构对象的话,那么一般而言,增加新的操作会很复杂。
而使用访问者模式,增加新的操作就意味着增加一个新的访问者类,因此,变得很容易。
访问者模式将有关的行为集中到一个访问者对象中,而不是分散到一个个的节点类中。
2、访问者模式可以跨过几个类的等级结构访问属于不同的等级结构的成员类。
迭代子只能访问属于同一个类型等级结构的成员对象,而不能访问属于不同等级结构的对象。
访问者模式可以做到这一点。
3、积累状态
每一个单独的访问者对象都集中了相关的行为,从而也就可以在访问的过程中将执行操作的状态积累在自己内部,而不是分散到很多的节点对象中。
这是有益于系统维护的优点。
访问者模式的缺点
1、增加新的节点类变得很困难。
每增加一个新的节点都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作。
2、破坏封装。
访问者模式要求访问者对象访问并调用每一个节点对象的操作,这隐含了一个对所有节点对象的要求:
它们必须暴露一些自己的操作和内部状态。
不然,访问者的访问就变得没有意义。
由于访问者对象自己会积累访问操作所需的状态,从而使这些状态不再存储在节点对象中,这也是破坏封装的。
涉及角色:
1.Visitor抽象访问者角色,为该对象结构中具体元素角色声明一个访问操作接口。
该操作接口的名字和参数标识了发送访问请求给具体访问者的具体元素角色,这样访问者就可以通过该元素角色的特定接口直接访问它。
2.ConcreteVisitor.具体访问者角色,实现Visitor声明的接口。
3.Element定义一个接受访问操作(accept()),它以一个访问者(Visitor)作为参数。
4.ConcreteElement具体元素,实现了抽象元素(Element)所定义的接受操作接口。
5.ObjectStructure结构对象角色,这是使用访问者模式必备的角色。
它具备以下特性:
能枚举它的元素;可以提供一个高层接口以允许访问者访问它的元素;如有需要,可以设计成一个复合对象或者一个聚集(如一个列表或无序集合)。
访问者模式的几个特点:
访问者模式把数据结构和作用于结构上的操作解耦合,使得操作集合可相对自由地演化。
访问者模式适用于数据结构相对稳定算法又易变化的系统。
因为访问者模式使得算法操作增加变得容易。
若系统数据结构对象易于变化,经常有新的数据对象增加进来,则不适合使用访问者模式。
访问者模式的优点是增加操作很容易,因为增加操作意味着增加新的访问者。
访问者模式将有关行为集中到一个访问者对象中,其改变不影响系统数据结构。
其缺点就是增加新的数据结构很困难。
适用情况:
1)一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作。
2)需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。
Visitor模式使得你可以将相关的操作集中起来定义在一个类中。
3)当该对象结构被很多应用共享时,用Visitor模式让每个应用仅包含需要用到的操作。
4)定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。
改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。
如果对象结构类经常改变,那么可能还是在这些类中定义这些操作较好。
11.4访问者模式
可以看到,许多模式遵循“组合比继承更灵活”的原则,在代码运行时建立一定的对象结构。
无处不在的组合模式便是一个很好的示例。
当使用对象集合时,你可能需要对结构上每一个单独的组件应用各种操作。
这样的操作可以内建于组件本身,毕竟在组件内部调用其他组件是最方便的。
但这种方式也存在问题,因为你并不知道所有可能需要执行的操作。
如果每增加一个操作,
就在类中增加一个对于新操作的支持,类就会变得越来越臃肿。
访问者模式可以解决这个问题。
11.4.1问题
回想一下上一章介绍的组合模式的示例。
我们在游戏中创建了可以由战斗单元(Unit)组成的军队,它的整体和局部可以互换处理。
我们也看到了操作可以在组件内部实现。
一般情况下,局部对象自己会执行操作,而组合对象则会调用它们的子对象来执行操作。
classArmyextendsCompositeUnit{//CompositeUnit:
组合战斗单元
functionbombardStrength(){
$ret=0;
foreach($this->units()as$unit){
$ret+=$unit->bombardStrength();
}
return$ret;
}
}
classLaserCannonUnitextendsUnit{//LaserCannonUnit(激光炮战斗单元)
functionbombardStrength(){
return44;
}
}
如果操作很容易整合到组合类中,那么这样做没有任何问题。
但是,还有更多的周边任务会
使接口并不是那么够用。
这里有个转储叶节点的文本信息的操作,该操作会被添加到抽象类Unit中。
//Unit
functiontextDump($num=0){
$ret="";
$pad=4*$num;
$ret.=sprintf("%{$pad}s","");
$ret.=get_class($this).":
";
$ret.=$this->bombardStrength()."\n";
return$ret;
}
然后这个方法可以在CompositeUnit类里被覆盖:
//CompositeUnit
functiontextDump($num=0){
$ret=parent:
:
textDump($num);
foreach($this->unitsas$unit){
$ret.=$unit->textDump($num+1);
}
return$ret;
}
我们可能还需要继续创建统计树中单元个数的方法、保存组件到数据库的方法和计算军队的食物消耗的方法。
为什么我们要在Composite的接口中加入这些方法呢?
只有一个答案有说服力:
添加这些不同的操作有利于在组合结构中较为轻松地访问相关节点。
虽然可以轻松遍历的确是组合模式的一大优势,但并非每个需要遍历对象树的操作都要在Composite接口中占据位置。
因此工作中的重点便是充分利用对象结构提供的轻松遍历的优势,但同时避免类过度膨胀。
11.4.2实现
让我们从接口开始。
首先在Unit抽象类中定义accept()方法:
functionaccept(ArmyVisitor$visitor){
$method="visit".get_class($this);
$visitor->$method($this);
}
protectedfunctionsetDepth($depth){
$this->depth=$depth;
}
functiongetDepth(){
return$this->depth;
}
可以看到,accept()方法要求一个ArmyVisitor对象作为参数。
PHP允许我们在ArmyVisitor上动态定义一个我们希望调用的方法。
这让我们不必在类的继承体系中每一个叶节点上实现accept()。
代码中接着添加了两个好用的getDepth和setDepth()方法,这两个方法可在树中类获取和设置一个单元的深度。
父单元通过CompositeUnit:
:
addUnit()添加子单元时,setDepth()方法会被调用。
functionaddUnit(Unit$unit){
foreach($this->unitsas$thisunit){
if($unit===$thisunit){
return;
}
$unit->setDepth($this->depth+l);
$this->units[]=$unit;
}
接下来,我们在抽象的组合类中定义另一个accept()方法:
functionaccept(ArmyVisitor$visitor){
$method="visit".get_class($this);
$visitor->$method($this);
foreach($this->unitsas$thisunit){
$thisunit->accept($visitor);
}
}
这个accept()方法和Unit:
:
accept()方法基本一样,但是多了一些内容。
它根据当前类的名称构造了一个方法名称,然后通过传入的ArmyVisitor对象来调用对应的方法。
因此如果当前
类是Army,则该方法调用ArmyVisitor:
:
visitArmy();如果当前类是TroopCarrier,则调用
ArmyVisitor:
:
visitTroopCarrier();依此类推。
在此之后,accept()方法会遍历调用accept()方法的所有子对象。
事实上,因为accept()覆盖了父类中的同名方法,所以这里我们可以去除重复代码:
functionaccept(ArmyVisitor$visitor){
parent:
:
accept($visitor);
foreach($this->unitsas$thisunit){
$thisunit->accept($visitor);
}
}
用这种方式消除重复代码还是非常令人满意的,尽管在本例中我们只节省了一行代码。
在这
两个例子中,accept()方法都允许我们做两件事情:
为当前组件调用正确的访问者方法;通过accept()方法将访问者对象传递给当前对象元素所有的子元素(假设当前组件是组合体)。
我们还需要定义ArmyVisitor接口。
accept()方法应该会给你一些提示。
访问者类也应该为继承体系中的每一个具体类定义一个accept()方法。
这样我们就能为不同的对象提供不同的功能。
下面的类中定义了一个默认的visit()方法,当类中没有为特定的Unit类提供特殊处理时,该方法会被自动调用。
abstractClassArmyVisitor{
abstractfunctionvisit(Unit$node){
functionvisitArcher(Archer$node){
$this->visit($node);
}
functionvisitCavalry(Cavalry$node){
$this->visit($node);
}
functionvisitLaserCannonUnit(LaserCannon$node){
$this->visit($node);
}
functionvisitTroopCarrierUnit(TroopCarrierUnit$node){
$this->visit($node);
}
functionvisitArmy(Army$node){
$this一>visit($node);
}
}
}
因此,现在剩余的工作只是给ArmyVisitor提供具体实现。
下面是ArmyVisitor对象的一个
实现,用于转储文本:
classTextDumpArmyVisitorextendsArmyVisitor{
private$text="";
functionvisit(Unit$node){
$ret="";
$pad=4*$node->getDepth();
$ret.=sprintf("%{$pad}s","");
$ret.=get_class($node).";";
$ret.="bombard:
".$node->bombardStrength()."\n";
$this->text.=$ret;
}
functiongetText(){
return$this->text;
}
}
让我们来看看客户端调用代码,然后走一下整个流程:
$main_army=newArmy();
$main_army->addUnit(newArcher());
$main_army->addUnit(newLaserCannonUnit());
$main_army->addUnit(newCavalry());
$textdump=newTextDumpArmyVisitor();
$main_army->accept($textdump);
print$textdump->getText();
以上代码执行后的结果如下:
Army:
bombard:
50
Archer:
bombard:
9
LaserCannonUnit:
bombard:
44
Cavalry:
bombard:
2
我们先创建一个Army对象。
因为Army是组合体,所以我们用它的addUnit()方法加入了更多Unit对象。
然后我们创建TextDumpArmyVisitor对象,并把它传给Army:
:
accept()。
accept()方法构造并调用TextDumpArmyVisitor:
:
visitArmy()方法。
在这里,我们没有给Army对象提供什么特殊的处理,因此调用通用的visit()方法,并将对Army对象的引用传给visit()。
visit()会调用Army对象的方法(包括一个新的getDepth()方法,它用于获得该对象在对象体系中的深度)来产生摘要数据。
这时完成对visitArmy()的调用,接着Army:
:
accept()操作会轮流调用子对象的accept(),同时将访问者传递给该方法。
这样,ArmyVisitor类会访问对象树中的每一个对象。
在这个简单的示例中,我们没有直接将Unit对象传给各种visit方法,而是利用了这些方法
的特殊特性来向不同类型的Unit对象征收不同的费用。
下面是客户端代码:
$mainarmy=newArmy();
$main_army->addUnit(newArcher());
$mainarmy->addUnit(newLaserCannonUnit());
$main_army->addUnit(newCavalzy());;
$taxcollector=newTaxCollectionVisitor();
$mainarmy->accept($taxcollector);
print"TOTAL:
”;
print$taxcollector->getTax().”\n";
TaxCollectionVisitor像之前一样被传给Army对象的accept()方法。
Army在调用其子对象的accept()之前,先将对自己的一个引用传给visitArmy()方法。
组件并不知道它们的访问者所执行的操作。
它们通过公共的接口简单协作,使每个组件都会正确地将自己传给对应自身类
型的方法。
除了在ArmyVisitor类中定义的方法以外,TaxCollectionVisitor还提供了两个摘要方法:
getReport()和getTax()。
调用这两个方法会得到你期望的数据:
TaxleviedArmy:
1
TaxleviedforArcher:
2
TaxleviedforLaserCannonUnit:
1
TaxleviedforCavalry:
3
TOTAL:
7
图11-7展示了示例中的各参与者。
11.4.3访问者模式的问题
访问者模式也是一个简单实用的模式,然而使用这个模式时有些地方需要注意。
首先,虽然完美地符合组合模式,但事实上访问者可以用于任何对象集合。
举例来说,你可以把访问者用于每个对象都保存对兄弟节点引用的一组对象。
其次,外部化操作可能破坏封装。
也就是说,你可能需要公开被访问对象的内部来让访问者能对它们做任何有用的操作。
例如,在第一个访问者例子中,为了给TextDumpArmyVisitor对象提供信息,我们被迫给Unit接口提供了一个额外的方法。
我们也在观察者模式中见过这种情况。
由于迭代(遍历)与访问者对象执行的操作是分离的,你必须在一定程度上放开控制力度。
比如,要创建一个在子节点迭代前后都能工作的Visit()方法不太容易。
一个解决方法就是把
迭代的职责转移到访问者对象上。
但这样做的问