在访问者模式中使用反射

本文由 ImportNew - 文 学敏 翻译自 javaworld。欢迎加入Java小组。转载请参见文章末尾的要求。

集合类型在面向对象编程中很常用,这也带来一些代码相关的问题。比如,“怎么操作集合中不同类型的对象?”

一种做法就是遍历集合中的每个元素,然后根据它的类型而做具体的操作。这会很复杂,尤其当你不知道集合中元素的类型时。如果y要打印集合中的元素,可以写一个这样的方法:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. publicvoid messyPrintCollection(Collection collection) {  
  2.     Iterator iterator = collection.iterator()  
  3.     while(iterator.hasNext())  
  4.         System.out.println(iterator.next().toString())  
  5. }  
看起来很简单。仅仅调用了Object.toString()方法并打印出了对象,对吧?但如果你的集合是一个包含hashtable的vector呢?那会变得更复杂。你必须检查集合返回对象的类型:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. publicvoid messyPrintCollection(Collection collection) {  
  2.     Iterator iterator = collection.iterator()  
  3.     while(iterator.hasNext()) {  
  4.         Object o = iterator.next();  
  5.         if(o instanceofCollection)  
  6.             messyPrintCollection((Collection)o);  
  7.         else  
  8.             System.out.println(o.toString());  
  9.         }  
  10. }  
好了,现在可以处理内嵌的集合对象,但其他对象返回的字符串不是你想要的呢?假如你想在字符串对象加上引号,想在Float对象后加一个f,你该怎么做?代码会变得更加复杂:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. publicvoid messyPrintCollection(Collection collection) {  
  2.         Iterator iterator = collection.iterator()  
  3.         while(iterator.hasNext()) {  
  4.             Object o = iterator.next();  
  5.             if(o instanceofCollection)  
  6.                 messyPrintCollection((Collection)o);  
  7.             elseif (o instanceofString)  
  8.                 System.out.println("'"+o.toString()+"'");  
  9.             elseif (o instanceofFloat)  
  10.                 System.out.println(o.toString()+"f");  
  11.             else  
  12.                 System.out.println(o.toString());  
  13.         }  
  14.     }  
代码很快就变杂乱了。你不想让代码中包含一大堆的if-else语句!怎么避免呢?访问者模式可以帮助你。

为实现访问者模式,你需要创建一个Visitor接口,为被访问的集合对象创建一个Visitable接口。接下来需要创建具体的类来实现Visitor和Visitable接口。这两个接口大致如下:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. publicinterface Visitor {  
  2.     publicvoid visitCollection(Collection collection);  
  3.     publicvoid visitString(String string);  
  4.     publicvoid visitFloat(Float float);  
  5. }  
  6. publicinterface Visitable {  
  7.     publicvoid accept(Visitor visitor);  
  8. }  
对于一个具体的String类,可以这么实现:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. publicclass VisitableString implementsVisitable {  
  2.     privateString value;  
  3.     publicVisitableString(String string) {  
  4.         value = string;  
  5.     }  
  6.     publicvoid accept(Visitor visitor) {  
  7.         visitor.visitString(this);  
  8.     }  
  9. }  
在accept方法中,根据不同的类型,调用visitor中对应的方法:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. visitor.visitString(this)  

具体Visitor的实现方式如下:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. publicclass PrintVisitor implementsVisitor {  
  2.     publicvoid visitCollection(Collection collection) {  
  3.         Iterator iterator = collection.iterator();  
  4.         while(iterator.hasNext()) {  
  5.             Object o = iterator.next();  
  6.             if(o instanceofVisitable)  
  7.                 ((Visitable)o).accept(this);  
  8.     }  
  9.     publicvoid visitString(String string) {  
  10.         System.out.println("'"+string+"'");  
  11.     }  
  12.     publicvoid visitFloat(Float float) {  
  13.         System.out.println(float.toString()+"f");  
  14.     }  
  15. }  
到时候,只要实现了VisitableFloat类和VisitableCollection类并调用合适的visitor方法,你就可以去掉包含一堆if-else结构的messyPrintCollection方法,采用一种十分清爽的方式实现了同样的功能。visitCollection()方法调用了Visitable.accept(this),而accept()方法又反过来调用了visitor中正确的方法。这就是双分派:Visitor调用了一个Visitable类中的方法,这个方法又反过来调用了Visitor类中的方法。

尽管实现visitor后,if-else语句不见了,但还是引入了很多附加的代码。你不得不将原始的对象——String和Float,打包到一个实现Visitable接口的类中。虽然很烦人,但这一般来说不是个问题。因为你可以限制被访问集合只能包含Visitable对象。

然而,这还有很多附加的工作要做。更坏的是,当你想增加一个新的Visitable类型时怎么办,比如VisitableInteger?这是访问者模式的一个主要缺点。如果你想增加一个新的Visitable类型,你不得不改变Visitor接口以及每个实现Visitor接口方法的类。你可以不把Visitor设计为接口,取而代之,可以把Visitor设计为一个带有空操作的抽象基类。这与Java GUI中的Adapter类很相似。这么做的问题是你会用尽单次继承,而常见的情形是你还想用继承实现其他功能,比如继承StringWriter类。这同样只能成功访问实现Visitable接口的对象。
幸运的是,Java可以让你的访问者模式更灵活,你可以按你的意愿增加Visitable对象。怎么实现呢?答案是使用反射。使用反射的ReflectiveVisitor接口只需要一个方法:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. publicinterface ReflectiveVisitor {  
  2.     publicvoid visit(Object o);  
  3. }  
好了,上面很简单。Visitable接口先不动,待会我会说。现在,我使用反射实现PrintVisitor类。

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. publicclass PrintVisitor implementsReflectiveVisitor {  
  2.     publicvoid visitCollection(Collection collection)  
  3.     { ... same as above ... }  
  4.     publicvoid visitString(String string)  
  5.     { ... same as above ... }  
  6.     publicvoid visitFloat(Float float)  
  7.     { ... same as above ... }  
  8.     publicvoid default(Object o)  
  9.     {  
  10.         System.out.println(o.toString());  
  11.     }  
  12.     publicvoid visit(Object o) {  
  13.         // Class.getName() returns package information as well.  
  14.         // This strips off the package information giving us  
  15.         // just the class name  
  16.         String methodName = o.getClass().getName();  
  17.         methodName = "visit"+  
  18.                     methodName.substring(methodName.lastIndexOf('.')+1);  
  19.         // Now we try to invoke the method visit<methodName>  
  20.         try{  
  21.             // Get the method visitFoo(Foo foo)  
  22.             Method m = getClass().getMethod(methodName,  
  23.                 newClass[] { o.getClass() });  
  24.             // Try to invoke visitFoo(Foo foo)  
  25.             m.invoke(this,newObject[] { o });  
  26.         }catch(NoSuchMethodException e) {  
  27.             // No method, so do the default implementation  
  28.             default(o);  
  29.         }  
  30.     }  
  31. }  
现在你无需使用Visitable包装类(包装了原始类型String、Float)。你可以直接访问visit(),它会调用正确的方法。visit()的一个优点是它会分派它认为合适的方法。这不一定使用反射,可以使用完全不同的一种机制。

在新的PrintVisitor类中,有对应于Collections、String和Float的操作方法;对于不能处理的类型,可以通过catch语句捕捉。对于不能处理的类型,可以通过扩展visit()方法来尝试处理它们的所有超类。首先,增加一个新的方法getMethod(Class c),返回值是一个可被触发的方法。它会搜索Class c的所有父类和接口,以找到一个匹配方法。

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. protectedMethod getMethod(Class c) {  
  2.     Class newc = c;  
  3.     Method m = null;  
  4.     // Try the superclasses  
  5.     while(m == null&& newc != Object.class) {  
  6.         String method = newc.getName();  
  7.         method = "visit"+ method.substring(method.lastIndexOf('.') + 1);  
  8.         try{  
  9.             m = getClass().getMethod(method, newClass[] {newc});  
  10.         }catch(NoSuchMethodException e) {  
  11.             newc = newc.getSuperclass();  
  12.         }  
  13.     }  
  14.     // Try the interfaces.  If necessary, you  
  15.     // can sort them first to define 'visitable' interface wins  
  16.     // in case an object implements more than one.  
  17.     if(newc == Object.class) {  
  18.         Class[] interfaces = c.getInterfaces();  
  19.         for(inti = 0; i < interfaces.length; i++) {  
  20.             String method = interfaces[i].getName();  
  21.             method = "visit"+ method.substring(method.lastIndexOf('.') + 1);  
  22.             try{  
  23.                 m = getClass().getMethod(method, newClass[] {interfaces[i]});  
  24.             }catch(NoSuchMethodException e) {}  
  25.         }  
  26.     }  
  27.     if(m == null) {  
  28.         try{  
  29.             m = thisclass.getMethod("visitObject",newClass[] {Object.class});  
  30.         }catch(Exception e) {  
  31.             // Can't happen  
  32.         }  
  33.     }  
  34.     returnm;  
  35. }  
这看上去很复杂,实际上并不。大致来说,首先根据传入的class名称搜索可用方法;如果没找到,就尝试从父类搜索;如果还没找到,就从接口中尝试。最后,(仍没找到)可以使用visitObject()作为默认方法。

由于大家对传统的访问者模式比较熟悉,这里沿用了之前方法命名的惯例。但是,有些人可能注意到,把所有的方法都命名为“visit”并通过参数类型不同来区分,这样更高效。然而,如果你这么做,你必须把visit(Object o)方法的名称改为其他,比如dispatch(Object o)。否则,(当没有对应处理方法时),你无法退回到默认的处理方法,并且当你调用visit(Object o)方法时,为了确保正确的方法调用,你必须将参数强制转化为Object。

为了利用getMethod()方法,现在需要修改一下visit()方法。

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. publicvoid visit(Object object) {  
  2.     try{  
  3.         Method method = getMethod(getClass(), object.getClass());  
  4.         method.invoke(this,newObject[] {object});  
  5.     }catch(Exception e) { }  
  6. }  
现在,visitor类更加强大了——可以传入任意的对象并且有对应的处理方法。另外,有一个默认处理方法,visitObject(Object o),的好处就是就可以捕捉到任何没有明确说明的类型。再稍微修改下,你甚至可以添加一个visitNull()方法。

我仍保留Visitable接口是有原因的。传统访问者模式的另一个好处是它可以通过Visitable对象控制对象结构的遍历顺序。举例来说,假如有一个实现了Visitable接口的类TreeNode,它在accept()方法中遍历自己的左右节点。

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. publicvoid accept(Visitor visitor) {  
  2.     visitor.visitTreeNode(this);  
  3.     visitor.visitTreeNode(leftsubtree);  
  4.     visitor.visitTreeNode(rightsubtree);  
  5. }  

这样,只要修改下Visitor类,就可以通过Visitable类控制遍历:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. publicvoid visit(Object object) throwsException  
  2. {  
  3.     Method method = getMethod(getClass(), object.getClass());  
  4.     method.invoke(this,newObject[] {object});  
  5.     if(object instanceofVisitable)  
  6.     {  
  7.         callAccept((Visitable) object);  
  8.     }  
  9. }  
  10. publicvoid callAccept(Visitable visitable) {  
  11.     visitable.accept(this);  
  12. }  
果你实现了Visitable对象的结构,你可以保持callAccept()不变,就可以使用Visitable控制的对象遍历。如果你想在visitor中遍历对象结构,你只需重写allAccept()方法,让它什么都不做。

当使用几个不同的visitor去操作同一个对象集合时,访问者模式的力量就会展现出来。比如,当前有一个解释器、中序遍历器、后续遍历器、XML编写器以及SQL编写器,它们可以处理同一个对象集合。我可以轻松地为这个集合再写一个先序遍历器或者一个SOAP编写器。另外,它们可以很好地兼容它们不识别的类型,或者我愿意的话可以让它们抛出异常。

总结

使用Java反射,可以使访问者模式提供一种更加强大的方式操作对象结构,可以按照需求灵活地增加新的Visitable类型。我希望在你的编程之旅中可以使用访问者模式。

Jeremy Blosser有5年的Java编程经验,他在很多软件公司工作过。他现在在一家创业型公司Software Instruments供职。你可以访问Jeremy的网站http://www.blosser.org

了解更多

 
原文链接:  javaworld  翻译:  ImportNew.com  文 学敏
译文链接:  http://www.importnew.com/12536.html
转载请保留原文出处、译者和译文链接。 ]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值