【IT168技术文档】
改进访问者模式
前面我们说到了如何使用Visitor模式及使用该模式后的优缺点,下面举具体的例子说明。假设现在客户提出了一个产品集(ProductSet)的概念:随着公司软件版本的增多,需要将同一个版本的产品(Product)都放到产品集(ProductSet)中,而一个品牌包含有多个产品集。因为现在组合结构中增加了一个节点,所以在Visitor接口中也必须随之增加一个叫做visitProductSet的方法,并且会导致原有系统中所有已经实现了Visitor接口的类都需要重新实现并编译。用Java的反射机制可以解决这个问题。
使用Java的Method Reflection机制实现访问者模式
首先我们需要改变一下Visitor接口,接口名叫做ReflectionVisitor,如下所示:
在现在的接口的方法里,能接受任意的对象(参数是Object)。public interface ReflectionVisitor ...{ /**//** * 定义了一个访问节点的方法 * @param softwareComposite */ public void visitSoftwareComposite(Object softwareComposite); }
下面实现接口ReflectionVisitor,名叫ReflectionVisitorImpl,代码如下所示:
这段代码首先判断传入的对象是否是空指针,然后创建class数组和object数组,然后用getMethod方法取得方法名是"visit"、方法的参数是"对象softwareComposite对应的类"的方法,最后调用该方法。调用该方法的时候可能会发生NoSuchMethodException异常,发生这个异常就表明它的子类或者当前类中没有与参数中传入相对应的visit方法。public class ReflectionVisitorImpl implements ReflectionVisitor ...{ public void visitSoftwareComposite(Object softwareComposite) ...{ //判断是否是null if (softwareComposite == null) ...{ throw new NullPointerException("The visit node should not be null!"); } //组装class数组,即调用动态方法的时候参数的类型 Class[] classes = new Class[] ...{ softwareComposite.getClass() }; //组装与class数组相对应的值 Object[] objects = new Object[] ...{ softwareComposite }; try ...{ //查找visit方法 Method m = getClass().getMethod("visit", classes); //调用该方法 m.invoke(this, objects); } catch (NoSuchMethodException e) ...{ //没有找到相应的方法 System.out .println("You did not implement the visit method for class:" + softwareComposite.getClass()); } catch (Exception e) ...{ //发生了别的异常 System.out.println("Catched excepction in visit method."); e.printStackTrace(); } } }
下面再来写新版本Visitor类,扩展刚写好的那个ReflectionVisitorImpl类,名叫CaculateTotalPriceReflectionVisitor,如下所示:
代码中声明了两个visit方法(因为在类ReflectionVisitorImpl中,查找名为visit、参数与传进去的对象匹配的的方法),一个是给Product的,另外一个是给SoftwareSet的。在这里SoftwareSet中并没有价格,只需当前的对象是类Product的实例的时候将价格累加即可。如果在组合模式的结构中增加了新的类,只需要在ReflectionVisitorImpl的扩展类中声明一个visit方法,该方法的参数是新增加的类,对于文中的例子,只需增加下面的一个方法:public class CaculateTotalPriceReflectionVisitor extends ReflectionVisitorImpl ...{ private double totalPrice; public void visit(Product product) ...{ totalPrice += product.getPrice(); } public void visit(SoftwareSet softwareSet) ...{ System.out.println("No price for software set."); } public double getTotalPrice() ...{ return totalPrice; } }
在组合结构的接口SoftwareComponent中改一下accept方法,参数是修改后的Visitor接口,如下所示:public void visit(ProductSet productSet) ...{ //实现的代码 }
由于在类SoftwareSet、Brand和ProductSet中实现上面accept方法的代码都一样,因此把代码抽象到上层共有的抽象类AbsSoftwareComposite中,如下所示:public void accept(ReflectionVisitor visitor);
现在如果想在外面要调用的话,代码如下所示(在本文的例子com.test.business.SoftwareManager中可以找到这段代码):public void accept(ReflectionVisitor visitor) ...{ visitor.visitSoftwareComposite(this); Iterator it = childs.iterator(); while (it.hasNext()) ...{ SoftwareComponent component = (SoftwareComponent) it.next(); //递归调用子对象的accept方法 component.accept(visitor); } }
另外由于没有实现Brand类的visit方法,在组合结构遍历到Brand的节点的时候会抛出NoSuchMethodException异常,就是没有关于该节点方法的实现,在当前的程序中会打印出一句话://建立一个新的Visitor对象 CaculateTotalPriceReflectionVisitor reflectionVisitor = new CaculateTotalPriceReflectionVisitor(); //将该visitor对象传到结构中 data.accept(reflectionVisitor); //调用visitor对象的getTotalPrice()方法就返回了总价格 double price = reflectionVisitor.getTotalPrice();
You did not implement the visit method for class:class com.test.entity.Brand
如果运行程序时发生了别的异常,请参见相应的Java API文档。
在现在的改进后的访问者模式中,如果在组合的结构中新增或删除节点并不会对已经实现了的Visitor产生任何影响;如果新增了业务方法,只需扩展类ReflectionVisitorImpl就可以了。因此很好地解决了访问者模式的问题。
改进访问者模式实现与既有代码对接
到现在为止,改进后的访问者模式好像已经很好地解决了所有出现的问题,但是考虑到有下面的这种情况:现在需要写一个JSP的标签库(TagLib),这个标签库还必须具有Visitor的功能(就是需要有遍历节点的功能),可以将节点的内容根据需要打印到HTML页面中。由于标签本身需要继承相应的类(如TagSupport),如果继续使用上面提供的方法将无法实现,因为Java不允许多重继承。不过我们可以将原有ReflectionVisitorImpl的代码再改进一下以解决这种情况,新的Visitor的实现类叫NewReflectionVisitorImpl,代码如下所示。
该类的实现与上面的实现差不多,多了一个构造函数,在该构造函数的参数中传入实现了visit方法的类,并且维护了指向该类的一个引用,另外最重要的地方是下面的两行代码:public class NewReflectionVisitorImpl implements ReflectionVisitor ...{ // 实现visit方法的类 private Object targetObject; //构造方法,传入实现了visit方法的类 public NewReflectionVisitorImpl(Object targetObject) ...{ if (targetObject == null) throw new NullPointerException( "The target object should not be null!"); this.targetObject = targetObject; } public void visitSoftwareComposite(Object softwareComposite) ...{ //……与上个例子相同 try ...{ // 从目标的对象中查找visit方法 Method m = targetObject.getClass().getMethod("visit", classes); // 调用该方法 m.invoke(targetObject, objects); } catch (NoSuchMethodException e) ...{ //……与上个例子相同 } catch (Exception e) ...{ //……与上个例子相同 } } }
本来的代码中从本身的类及其子类中查找visit方法,而现在是从维护的目标类中查找visit方法。// 从目标的对象中查找visit方法 Method m = targetObject.getClass().getMethod("visit", classes); // 调用该方法 m.invoke(targetObject, objects);
现在需要写Tag类,这个类扩展了TagSupport类,如下所示(为说明的方便,随本文的例子提供了一个模拟的TagSupport类):
如果想测试上面写的那段代码,(在本文的例子com.test.business.SoftwareManager中可以找到这段代码))如下所示:public class MyTag extends TagSupport ...{ SoftwareComponent softwareComponent = null; private double totalPrice = 0; public int doEngTag() ...{ //创建一个visitor对象,并且将本身传入visitor对象中 ReflectionVisitor visitor = new NewReflectionVisitorImpl(this); //遍历结构 softwareComponent.accept(visitor); //打印出价格 out.println(totalPrice); return 1; } //实现了针对Product的visit方法 public void visit(Product product) ...{ totalPrice += product.getPrice(); } public void visit(Brand brand) ...{ out.println(brand.getId() + brand.getDescription()); } //别的代码请参见随本文带的源程序 …… }
可以看到通过Java的反射机制很好地解决了多重继承的问题,使该访问者模式能够更好地应用于你的应用中。另外可以看到,那些visit方法所在的类已经不是实现了接口ReflectionVisitor,可以说是访问者模式在Java语言的支持下的一种特殊实现。//getMockData()方法返回数据 SoftwareComponent data = getMockData(); MyTag myTag = new MyTag(); myTag.setSoftwareComponent(data); //计算总价格,并打印出来 myTag.doEngTag();
如果担心引入类反射机制后带来的效率问题,你可以将Method对象通过某种方式缓冲起来,这样不会每次从传入的对象中找visit方法,可以部分地提高效率。
结论
在给定的组合模式的数据结构中,实现业务逻辑的方法非常多,文中试着介绍了几种实现业务逻辑的方法,并给出了相应的实现方式下的优缺点。读者可以综合考虑应用的需求来决定相应的实现方法。