`
xyheqhd888
  • 浏览: 404070 次
  • 性别: Icon_minigender_1
  • 来自: 秦皇岛
社区版块
存档分类
最新评论

Visitor(访问者)模式

阅读更多

   如何扩展一个现有的类层次结构来实现新的需求呢?一般的做法是添加新的方法以满足新需求.但是,有时候新需求可能与现有对象模型根本就不兼容.另外,有可能你无权看到已有代码.在这些情况下,无法修改类层次结构内的代码,我们要想实现对类层次结构行为的扩展,那几乎是不可能的.但是,如果开发人员能在设计类层次结构的过程中应用Visitor模式,这将大大夺方便其他开发人员在以后的软件开发过程中扩展该类层次结构的行为.

    同Interpreter模式一样,Visitor模式通常位于Composite模式之上.

    Visitor模式的意图在于让代码用户能够在不修改现有类层次结构的前提下,定义该类层次结构的操作.

 

1.Visitor模式机制:

   借助于Visitor模式,在开发类层次结构时稍微深思远虑,就可以增强类层次结构的灵活性,以便后来无权访问源代码的开发者对该类层次结构进行扩展.Visitor模式的机制简单描述如下:

(1)给类层次结构中部分或者全部类添加accept()操作.该方法的每种实现都能接收一个特殊参数,这个参数的类型是你将创建的接口.

(2)用共享相同名称(通常使用visit)的操作集合来创建接口-----但是操作的参数类型可能不同.为希望扩展的类层次结构中每个类声明这样的一个操作.

图1给出修改后支持Visitor模式的MachineComponent类层次结构.


为保证MachineComponent类层次结构支持Visitor模式,应该添加本图所示的

accept()方法和MachineVisitor接口

 

 

     图1的类图并没有解释visitor模式的工作原理;将在下一部分中做出说明.它只是给出了应用Visitor模式的一些基本原理.

     注意,并不是MachineComponent类层次结构中的所有类都实现accept()方法.Visitor模式并不要求类层次结构中每个类都实现accept()方法.正如我们看到的那样,实现accept()方法的每个类应该充当访问器接口中声明的visit()方法的参数.

   MachineComponent类中的accept()方法是抽象的.其子类使用几乎类似的代码来实现这个方法:

public void accept(MachineVisitor v){
   v.visit(this);
}

 

突破题:请问对Machine类和MachineComposite类的accept()方法,Java编译器会发现什么区别?

答:它们的区别在于this对象的类型不同.两个accept()方法都调用MachineVisitor对象的visit()方法.不过,Machine类中的accept()方法是根据方法头visit(:Machine)来查找visit()方法的,而MachineComposite类中的accept()方法是根据visit(:MachineComposite)来查找visit()方法的.

 

   另外,需要在MachineVisitor接口的实现类中定义访问Machine和MachineComposite的方法:

package com.oozinoz.machine;
public interface MachineVisitor
{
	void visit(Machine m);
	void visit(MachineComposite mc);
}

借助于MachineComponent类的accept()方法MachineVisitor接口,开发者就可以向类层次结构中加入新的操作.

 

2.常见的Visitor模式:

      假设位于爱尔兰都柏林的新Oozinoz工厂的开发人员已经为新工厂的机器组合建立了对象模型,并且允许外界使用OozinozFactory类的dublin()静态方法来访问该模型.为了显示这种组合结构,开发人员创建了一个MachineTreeModel(位于com.oozinoz.dublin包中)类,它会调整对象模型的信息,以满足JTree对象的要求.

     为了显示新工厂的机器,需要创建工厂机器组合中MachineTreemodel类的一个实例,并且要把该模型包装在Swing组件中:

package app.visitor;
import javax.swing.JScrollPane;
import javax.swing.JTree;

import com.oozinoz.machine.OozinozFactory;
import com.oozinoz.ui.SwingFacade;

public class ShowMachineTreeModel {

    public ShowMachineTreeModel() {
        MachineTreeModel model = new MachineTreeModel(OozinozFactory.dublin());
        JTree tree = new JTree(model);
        tree.setFont(SwingFacade.getStandardFont());
        SwingFacade.launch(new JScrollPane(tree), " A New Oozinoz Factory");
    }

    public static void main(String[] args) {
        new ShowMachineTreeModel();
    }
}

这段代码生成一个如下图所示的树状浏览器.

 

 

对于机器组合,我们可能需要很多种行为.例如,假设我们要在工厂的模型中查找某个特定的机器.为在不修改MachineComponent类层次结构的情况下增加这种功能,我们可以新建一个FindVisitor类,如图3所示:



FindVisitor类向MachineComponent类层次结构中加入find()方法

 

visit()方法并不返回对象,所以FindVisitor类在其found实例变量中记录查找的结果:

package app.visitor;

import com.oozinoz.machine.*;
import java.util.*;

public class FindVisitor implements MachineVisitor
{
	private int soughtId;
	private MachineComponent found;
	
	public MachineComponent find(MachineComponent mc,int id){
	   found = null;
	   soughtId = id;
	   mc.accept(this);
	   return found;
	}

	public void visit(Machine m){
		if(found == null && m.getId() == soughtId)
			found = mc;
	}
	
	public void visit(MachineComposite mc){
		if(found == null && mc.getId() == soughtId){
			found = mc;
			return;
		}
		Iterator iter = mc.getComponents().iterator();
		while(found == null && iter.hasNext())
			((MachineComponent)iter.next()).accept(this);
	}
}

visit()方法检查found变量,只要找到了所需的组件,树的遍历就会结束.

 

突破题:写一段程序,将OozinozFactory.dublin()返回的MachineComponent实例中的StartPress:3404对象找到并打印出来.

答:代码如下:

package app.visitor;

import com.oozinoz.machine.MachineComponent;
import com.oozinoz.machine.OozinozFactory;

public class ShowFindVisitor{
   public static void main(String[] args){
       MachineComponent factory = OozinozFactory.dublin();
       MachineComponent machine = new FindVisitor().find(factory,3404);
       System.out.println(machine != null ? machine.toString() : "Not Found!");
   }

    find()方法不必关心收到的MachineComponent参数是Machine类实例,还是MachineComposite类实例.find()方法只需简单地调用accept()方法,而accept()方法又会调用visit()方法.

    注意,visit(:MachineComposite)方法中的循环看起来并不关心子组件是Machine的实例,还是MachineComposite类实例.visit()方法只是调用每个组件的accept()操作.本调用执行什么方法取决于子对象的类型.图4显示了方法调用的典型顺序.


FindVisitor对象调用MachineComponent对象的accept()方法,以决定执行哪个visitor()方法

 

   当visit(:MachineComposite)方法执行时,对每个组合结构的子对象分别调用accept()操作.子对象接着会调用Visitor对象的visit()操作.正如图4所示,从FindVisitor对象到接收accept()调用的对象的一次来回调用,便获得了接收对象的具体类型.这种技术被称为两次分发,保证了能够调用FindVisitor类的恰当的visit()方法.

   在Visitor模式中使用两次分发技术使得访问器类可以针对被访问类层次结构中不同类的类型提供不同的方法.

   如果能够掌握源代码,你几乎可以通过访问器添加任何行为.下面举个例子,一个访问器类搜索机器组件类层次结构中的所有机器(叶节点).

package app.visitor;
import com.oozionz.machine.*;
import java.util.*;

public class RekeVisitor implements MachineVisitor
{
	private Set leaves;

	public Set getLeaves(MachineComponent mc){
		leaves = new HashSet();
		mc.accept(this);
		return leaves;
	}

	public void visit(Machine m){
		leaves.add(m);
	}

	public void visit(MachineComposite m){
		Iterator iter = mc.getComponents().iterator();
		while(iter.hasNext())
			((MachineComponent) iter.next().accept(this);
	}
}

突破题:RackVisitor类用于遍历机器组件类层次结构中的所有叶节点.请写出该类的实现代码.(上面这段代码已经完成).

 

借助于下面这段简短的程序,能找到机器组件中的叶节点,并打印出来.

 

 

package app.visitor;

import com.oozinoz.machine.*;
import java.io.*;
import com.oozionoz.filter.WrapFilter;

public class ShowRakeVisitor
{
	public static void main(String[] args) throws IOException{
		MachineComponent f = OozinozFactory.dublin();
		Writer out = new PrintWriter(System.out);
		out = new WrapFilter(new BufferedWriter(out),60);
		out.write(new RakeVisitor().getLeaves(f).toString());
		out.close();
	}
}

这段程序使用逗号换行过滤器.

 

 

 

FindVisitor和RakeVisitor类都向MachineComponent类层次结构中加入了新的方法,它们看起来都能正常工作.然而,存在这样一个危险:为了编写访问器,我们需要了解所要扩展的类层次结构.类层次结构一旦发生改变,我们编写的访问器就可能失效.另外,开始的时候,我们也可能会错误地理解类层次结构.特别是当被访问的组合结构存在环的时候,我们必须对其中的环进行处理.

 

3.Visitor模式循环:

  Oozinoz公司使用ProcessComponent类层次结构对生产流程进行建模.该层次结构也是一个组合结构,我们可以对之进行重构以便支持Visitor模式,这样就可以轻松地对该类层次结构进行扩展.与机器类组合结构不同的是,生产流程组合结构通常会包含环状结构.因而在遍历流程组件的时候,Visitor必须小心谨慎以免陷入死循环.图5显示了ProcessComponent类层次结构:


和MachineComponent类层次结构一样,ProcessComponent类层次结构也通过内部修改来支持Visitor模式

 

假设我们期望以一种"优美的"或者首行缩排的格式输出流程组件.在上章中,使用了一个迭代器来输出流程的步骤.输出的结果就像这样:

  Make an aerial shell

          Build inner shell

          Inspect

          Rework inner shell, or complete shell

                  Rework

                          Disassemble

                  Finish:Attach lift, insert fusing,wrap

  对一个焰火弹进行返工包含这样两个步骤:先是"拆分焰火弹",接着是重新"制作高空焰火弹".输出结果并没有显示拆分焰火弹之后的步骤,这是因为迭代器已经发现这个步骤之前已经出现过一次了.然而,如果能够指出拆分焰火弹之后的步骤是制作高空焰火弹,从而表明生产流程进入了一个循环,这样做将为用户提供更多的信息.另外,如果能够指示出哪些组合成分是交替出现而不是顺序出现的,这也将为用户提供方便.

   为了将生产流程优美地打印出来,我们可以创建一个访问器类,并由它来初始化一个StringBuilding对象,并在访问流程中各个组件的过程中,将节点信息添加到该缓冲区内.为了指出某个步骤会产生分支,访问器类可以在这个步骤名前面加一个?.为了指出某个步骤在前面已经出现,访问器类可以在这个步骤的后面加上一个省略号(……)。通过这些修改,焰火弹生产流程输出结果会变成:

   Make an aerial shell

          Build inner shell

          Inspect

          ?Rework inner shell, or complete shell

                  Rework

                          Disassemble

                          Make an aerial shell......

                  Finish:Attach lift, insert fusing,wrap

 过程组件的访问器必须留意其中可能出现的循环。可以使用一个Set对象来记录访问器类已经访问过的节点,这样就可以轻易地保证不会出现死循环。相关代码如下所示:

 

package app.visitor;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
import com.oozinoz.process.*;

public class PrettyVisitor implements ProcessVisitor
{
	public static final String INDENT_STIRNG="   ";
	private StringBuffer buf;
	private int depth;
	private Set visited;
	public StringBuffer getPretty(ProcessComponent pc){
		buf = new StringBuffer();
		visited = new HashSet();
		depth=0;
		pc.accept(this);
		return buf;
	}

	protected void printIndentedString(String s){
		for(int i=0;i<depth;i++)
			buf.append(INDENT_STRING);
		buf.append(s);
		buf.append("\n");
	}

	//...visit() methods
}

该类使用getPretty()方法来初始化某实例的变量,并且去除访问器算法。随着算法愈加深入到组合结构中,printIndentedString()方法输出的缩进会更加多。当访问ProcessStep对象时,这部分代码会简单地输出步骤的名称:

public void visit(ProcessStep s){
	printIndentedString(s.getName());
}

或许你已经注意到,在图5中,ProcessComposite类并没有实现accept()方法,但是其子类却实现了。访问交替过程或者顺序过程需要差不多类似的处理逻辑,如下所示:

public void visit(ProcessAlternation a){
	visitComposite("?",a);
}

public void visit(ProcessSequence s){
	visitComposite("",a);
}

protected void visitComposite(String prefix,ProcessComposite c){
	if(visited.contains(c)){
		printIndentedString(prefix + c.getName() + "...");
	}else{
		visited.add(c);
		printIndentedString(prefix + c.getName());
		depth++;

		List children = c.getChildren();
		for(int i=0;i<children.size();i++){
			ProcessComponent child = (ProcessComponent)children.get(i);
			Children.accept(this);
		}

		depth--;
	}
}

      顺序访问和交替访问的差别在于,交替访问会输出问号作为前缀。对于任何类型的组合结构,如果该算法以前已经访问过这个节点,就会输出其名称和省略号。否则,代码会把这个节点添加到被访问节点集合中,输出其前缀---问号或者空---并“接收”这个节点的子节点。对于典型的Visitor模式应用实例,这部分代码会使用多态性来决定子节点是ProcessStep类、ProcessAlternation类还是ProcessSequence类的实例。

      现在一个简单的程序就能优美地打印出流程来: 

package app.visitor;

import com.oozinoz.process.ProcessComponent;
import com.oozinoz.process.ShellProcess;

public class ShowPrettyVisitor
{
	public static void main(String[] args){
		ProcessComponent p = ShellProcess.make();
		PrettyVisitor v = new PrettyVisitor();
		System.out.println(v.getPretty(p));
	}
}

运行该程序的打印输出如下:

   Make an aerial shell  上述步骤包含更多信息,比最早简单迭代过程模型的输出效果好得多。出现的问题表示这个组合结构步骤是交替出现的。同样,第二次出现的Make后跟省略号,这样比仅仅省略重复步骤要强得多。

          Build inner shell

          Inspect

          ?Rework inner shell, or complete shell

                  Rework

                          Disassemble

                          Make an aerial shell......

                  Finish:Attach lift, insert fusing,wrap

 

   通过在ProcessComponent类层次结构中提供accept()方法以及定义ProcessVisitor接口,开发者就可以内置实现对Visitor模式的支持。开发人员必须清楚在遍历流程的时候,应该避免出现死循环。如PrettyVisitor类所示,访问器的开发人员必须清楚流程组件中可能存在环。如果开发人员能够在对Visitor模式提供支持的过程中加入循环管理机制,这将有助于防止错误的发生。

 

突破题:请问ProcessComponent的开发人员怎样才能在支持Visitor模式的类层次结构中加入对循环管理的支持?

答:一种解决方案是在所有的accept()方法和visit()方法中加入一个Set参数,用来传递已访问节点的集合。这样,当一个节点属于已访问节点集合时,那么我们就可以不再访问这些节点。ProcessComponent类在实现accept()方法的时候,可以以一个新的Set对象为参数调用它的抽象accept()方法。具体代码如下:

public void accept(ProcessVisitor v){
     accept(v,new HashSet());
}

在ProcessComponent类层次结构中,ProcessAlternation子类、ProcessSequence子类以及ProcessStep子类的accept()方法的具体代码如下:

public void accept(ProcessVisitor v,Set visited){
	v.visit(this,visited);
}

现在,访问者类的开发人员必须创建含有visit()方法的类,而且这个visit()方法必须能够接收Set类型的visited集合。使用这个集合是一个很好的思路,不过,要让用户使用访问者类,访问者类的开发人员必须要让用户知道存在这样一个集合。

 

4.Visitor模式危机:   

  Visitor模式是一个备受争议的模式。有些开发人员希望能够避免使用这个模式;而一些开发人员则为其进行辩护,并提出了各种方案来加强这个模式,当然通常情况下,这些附加的方案使得Visitor模式更加复杂。实际上,Visitor模式确实引发了很多设计问题。

  本章的例子也体现出了Visitor模式的弱点。例如,在MachineComponent类层次结构中,开发人员区分了Machine节点和MachineComposite节点,但并没有区分Machine的子类。如果我们期望访问器类能够对不同类型的机器进行区分,那么就必须借助于类型检查或其他的技术,识别visit()方法收到的参数具体属于哪种机器类型。当然,也许会有人争辩说,类层次结构的开发人员应该考虑所有的机器类型,并在访问器接口中提供一个通用的visit(:Machine)方法。但是新的机器类型总是会出现,因此这个方法看起来也并不够健壮。

  Visitor模式是否是个很好的选择,取决于变化的特性:如果类层次结构是稳定的,附加的行为需要变化,这种选择就是合适的。如果行为是稳定的,类层次结构就需要变化,这种选择则不合适,因为你必须返工,更新已有的访问器,以支持新的代码类型

  体现Visitor模式脆弱性的另一个例子是ProcessComponent类层次结构。该类层次结构的开发人员知道潜伏在流程模型中的循环的危险性。但是开发人员怎样才能将这种情况传达给访问器类的开发人员呢?

  这就暴露了使用Visitor模式的一个基本问题:扩展一个类层次结构的行为时,通常都需要我们对该类层次结构的设计有深入的了解。如果了解不够透彻,我们可能步入陷阱,比如可能会无法避免流程中的死循环。然而,即使我们对类层次结构有足够的了解,也可能会为该类层次结构引入某种危险的依赖关系;而一旦类层次结构发生变化,这种依赖关系就会失效。如果无法将类层次结构的专业知识应用于对具体代码的控制,那么Visitor模式可能会成为一个危险的模式。

  Visitor模式能够很好的工作,且不会造成后续问题的一个典型例子是计算机语言解析器。当开发一个语言解析器的时候,我们可能会安排解析器创建一棵抽象语法树,它是一个根据语言语法将输入文本进行相应组织的结构。我们可能还想在这些树上开发一系列的行为,Visitor模式正是实现这些目标的一个有效方法。值得注意的是,在这个典型例子中,被访问的类层次结构通常只含有很少的行为。因此,访问器类要负责设计所有需要的行为,从而避免了像本章范例那样将责任分散的情况。

  和其他任何模式一样,Visitor模式也不是必须要使用的模式;如果必须要使用某种设计模式,那么我们就别无他选,只能使用这个模式。然而,对于Visitor模式而言,我们总能找到一个更为健壮的替换方案。

 

 突破题:本章范例将Visitor模式分别加入到Oozinoz公司的机器类层次结构中和生产流程类层次结构中,请列出两种替代方案。

答:下面给出Visitor模式的几种替代方案:

(1)将我们需要的行为加入到原有的类层次结构中。如果能够与类层次结构的开发人员进行良好的沟通,就可以使用这个方案;如果类层次结构是购买的,而且我们不认识它的开发人员,那么这种方案就不可行。

(2)如果某个料必须对机器类层次结构或者流程类层次结构进行操作,那么我们可以让它通过遍历该结构来实现。如果想获取组合结构子对象的类型,可以使用instanceof操作符,或者使用Boolean函数,诸如isLeaf()和isComposite()。

(3)如果我们期望加入的行为与现有的行为差别很大,那么可以创建一个并行的层次结构。例如,在Factory Method模式里曾经描述了MachinePlanner类,该类将机器的规划行为放在独立的类层次结构中。

 

那么,你应该使用Visitor模式吗?在如下情况,最好使用Visitor模式:

(1)节点类型的集合是稳定的;

(2)公共变更添加应用于各种节点的新功能;

(3)新功能必须适应所有节点类型。

 

5.小结

   Visitor模式可以让我们在不改变类层次结构中的类的情况下,为该类层次结构定义新的操作。Visitor模式的机制包括为各个访问器类定义一个接口,在类层次结构中添加访问器将调用的accept()方法。accept()方法通过采用“两次分发”技术将调用结果返回给访问器类。visit()方法定义在访问器类中,类层次结构中的某个类对象可以根据其类型调用合适的visit()方法。

  访问器类的开发人员必须清楚将要访问的类层次结构的全部或者部分设计细节。另外,在设计访问器类的时候,我们必须特别注意被访问的对象模型中可能会出现环状结构。考虑到这些问题,一些开发人员常常会有意避免使用Visitor模式,习惯性地使用其他替换方案。一般而言,软件开发团队需要根据自己所采用的软件开发方法学,根据项目组以及具体项目的具体情况来决定是否使用Visitor模式。

  • 大小: 4.7 KB
  • 大小: 4.4 KB
  • 大小: 4 KB
  • 大小: 7.3 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics