【IT168 技术文档】本文从一个给定的实现了组合(Composite)模式的例子开始,说明怎么在这个数据结构上实现业务逻辑代码。依次介绍了非面向对象的方式、在组合结构中加入方法、使用访问者(Visitor)模式以及用改进后的访问者(Visitor)模式来实现相同的业务逻辑代码,并且对于每种实现分别给出了优缺点。
读者定位于具有Java程序开发和设计模式经验的开发人员。
读者通过本文可以学到如何在组合(Composite)模式中实现各种不同的业务方法及其优缺点。
组合(Composite)模式
组合模式是结构型模式中的一种。GOF的《设计模式》一书中对使用组合模式的意图描述如下:将对象组合成树形结构以表示"部分-整体"的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。
组合模式应用广泛。根据GOF中对组合模式的定义,Composite模式一般由Component接口、Leaf类和Composite类组成。现在需要对一个软件产品管理系统的实体建模:某公司开发了一系列软件集(SoftwareSet),包含了多种品牌(Brand)的软件产品,就象IBM提供了Lotus、WebsPhere等品牌。每个品牌下面又有各种产品(Product),如IBM的Lotus下面有Domino Server/Client产品等。
(1)接口SoftwareComponent就是对应于组合模式中的Component接口,它定义了所有类共有接口的缺省行为
(2)AbsSoftwareComposite类对应于Composite类,并且是抽象类,所有可以包含子节点的类都扩展这个类。这个类的主要功能是用来存储子部件,实现了接口中的方法,部分可以重用的代码写在此类中
(3)SoftwareSet类继承于AbsSoftwareComposite类,对应于软件集,软件集下直接可以包含品牌(Brand),也可以直接包含不属于任何品牌的产品(Product)
(4)Brand类继承于AbsSoftwareComposite类,对应于品牌,包含了品牌名属性,并且用来存储Product类的实例
(5)Product类就是对应的Leaf类,表示叶子节点,叶子节点没有子节点
用不同的方法实现业务逻辑
数据结构建立好之后,需要在这个数据结构上添加方法实现业务逻辑。比如现在的这个例子中,有这样的需求:给定一些用户选择好的产品,需要计算出这些选中后软件的总价格。下面开始介绍如何使用各种不同的方法来实现这个业务逻辑。
非面向对象的编程方式
这种方式下,编程思路最简单:遍历SoftwareSet实例中的所有节点,如果遍历到的当前对象是Product的话就累加,否则继续遍历下一层直到全部遍历完毕。代码片断如下:
/** * 取得某个SoftwareComponent对象下面所有Product的价格 * @param brand * @return */ public double getTotalPrice(SoftwareComponent softwareComponent) { SoftwareComponent temp = softwareComponent; double totalPrice = 0; //如果传入的实例是SoftwareSet的类型 if (temp instanceof SoftwareSet) { Iterator it = ((SoftwareSet) softwareComponent).getChilds() .iterator(); while (it.hasNext()) {//遍历 temp = (SoftwareComponent) it.next(); //如果子对象是Product类型的,直接累加 if (temp instanceof Product) { Product product = (Product) temp; totalPrice += product.getPrice(); } else if (temp instanceof Brand) { //如果子对象是Brand类型的,则遍历Brand下面所有的产品并累加 Brand brand = (Brand) temp; totalPrice += getBrandPrice(brand); } } } else if (temp instanceof Brand) { //如果传入的实例是SoftwareSet的类型,则遍历Brand下面所有的产品并累加 totalPrice += getBrandPrice((Brand) temp); } else if (temp instanceof Product) { //如果子对象是Product类型的,直接返回价格 return ((Product) temp).getPrice(); } return totalPrice; } /** * 取得某个Brand对象下面所有Product的价格 * @param brand * @return */ private double getBrandPrice(Brand brand) { Iterator brandIt = brand.getChilds().iterator(); double totalPrice = 0; while (brandIt.hasNext()) { Product product = (Product) brandIt.next(); totalPrice += product.getPrice(); } return totalPrice; }
这段代码的好处是实现业务逻辑的时候无需对前面已经定好的数据结构做改动,并且效率比较高;缺点是代码凌乱而且频繁使用了instanceof判断类型和强制类型转换,代码的可读性不强,如果层次多了代码就更加混乱。
面向对象的编程方式(将计算价格的方法加入数据结构中)
下面我们采用面向对象的方式,可以这么做:在接口SoftWareComponent中加入一个方法,名叫getTotalPrice,方法的声明如下:
/** * 返回该节点中所有子节点对象的价格之和 * @return */ public double getTotalPrice();
由于类Brand和SoftwareSet都继承了AbsSoftwareComposite,我们只需在类AbsSoftwareComposite中实现该方法getTotalPrice方法即可,如下:
public double getTotalPrice() { Iterator it = childs.iterator(); double price = 0; while (it.hasNext()) { SoftwareComponent softwareComponent = (SoftwareComponent) it.next(); //自动递归调用各个对象的getTotalPrice方法并累加 price += softwareComponent.getTotalPrice(); } return price; } 在Product类中实现如下: public double getTotalPrice(){ return price; }
在外面需要取得某个对象的总价格的时候只需这样写(在本文的例子com.test.business.SoftwareManager中可以找到这段代码):
// getMockData()方法返回数据 SoftwareComponent data = getMockData(); //只需直接调用data对象的getTotalPrice 方法就可以返回该对象下所有product对象的价格 double price = data. getTotalPrice(); //找到某个对象后直接调用其getTotalPrice方法也可以返回总价格 price = data. findSoftwareComponentByID("id").getTotalPrice();
现在把业务逻辑的实现都放在了数据结构中(组合模式的结构中),好处很明显,每个类只管理自己相关的业务代码的实现,跟前面举的面向过程方式的实现方式相比,没有了instanceof和强制类型转换。但是不好的地方是如果需要增加新的业务方法的话就很麻烦,必须在接口SoftWareComponent中首先声明该方法,然后在各个子类中实现并且重新编译。
使用访问者模式
使用访问者模式就能解决上面提到的问题:如果要经常增加或者删除业务功能方法的话,需要频繁地对程序进行重新实现和编译。根据面向对象设计原则之一的SRP(单一职责原则)原则,如果一个类承担了多于一个的职责,那么引起该类变化的原因就会有多个,就会导致脆弱的设计,在发生变化时,原有的设计可能会遭到意想不到的破坏。下面我们引入了一个叫做Visitor的接口,该接口中定义了针对各个子类的访问方法,如下所示:
public interface Visitor { public void visitBrand(Brand brand); public void visitSoftwareSet(SoftwareSet softwareSet); public void visitProduct(Product product); }
visitBrand方法是访问Brand对象节点的时候用的,剩下的方法依次类推。并在接口SoftwareComponent中增加一个方法:
public void accept(Visitor visitor);
在SoftwareSet中实现接口中的accept方法,首先直接调用Visitor接口中的visitSoftwareSet方法,传入的参数是本身对象,然后递归调用子对象的accept方法:
public void accept(Visitor visitor) { visitor.visitSoftwareSet(this); Iterator it = childs.iterator(); while (it.hasNext()) { SoftwareComponent component = (SoftwareComponent)it.next(); component.accept(visitor); } }
在Brand中实现接口中的accept方法,首先直接调用Visitor接口中的visitBrand方法,传入的参数是本身对象,然后递归调用子对象的accept方法:
public void accept(Visitor visitor) { visitor.visitBrand(this); Iterator it = childs.iterator(); while (it.hasNext()) { SoftwareComponent component = (SoftwareComponent)it.next(); component.accept(visitor); } }
其实在上面的两个类的实现中可以将遍历子节点并调用其accept方法的代码写到父类AbsSoftwareComposite中的某个方法中,然后直接调用父类中的这个方法即可。这里为了解释方便分别写在了两个子类中。
在Product中实现接口中的accept方法,直接调用Visitor接口的visitProduct方法即可:
public void accept(Visitor visitor) { visitor.visitProduct(this); }
下面需要实现Visitor接口,类名是CaculateTotalPriceVisitor,实现了计算总价格的业务逻辑,实现代码如下所示:
public class CaculateTotalPriceVisitor implements Visitor { private double totalPrice; public void visitBrand(Brand brand) { } public void visitSoftwareSet(SoftwareSet softwareSet) { } public void visitProduct(Product product) { //每次在组合的结构中碰到Product对象节点的时候,就会调用此方法 totalPrice += product.getPrice(); } public double getTotalPrice() { return totalPrice; } }
上面那段代码中,首先在类内定义一个总价格的属性,由于Brand和SoftwareSet都没有价格,因此在实现中,只需在visitProduct方法中累加totalPrice即可。在外面如果需要计算总价格的话这样写(在本文的例子com.test.business.SoftwareManager中可以找到这段代码):
//建立一个新的Visitor对象 CaculateTotalPriceVisitor visitor = new CaculateTotalPriceVisitor(); //将该visitor对象传到结构中 data.accept(visitor); //调用visitor对象的getTotalPrice()方法就返回了总价格 double price = visitor.getTotalPrice();
下面是它的时序图:在类SoftwareManager中的main方法中,调用软件集对象(data)的accept方法,并将生成的visitor对象传给它。accept方法开始递归调用各个子对象的accept方法。如果当前的对象是SoftwareSet的实例,则调用visitor对象visitSoftwareSet方法,在visitor对象中对该节点的数据进行一些处理,然后返回;依次类推,遍历到Brand对象和Product对象也与此类似。当前的逻辑是计算软件产品的总价格,因此当遍历到Product对象的时候,取出产品的价格并且累加,最后当结构遍历完毕后,调用visitor对象的getTotalPrice方法返回给定软件集对象的(data)的总的价格。如果需要加入一个新的计算逻辑,只实现Visitor接口,并且将该类的实例传给data对象的accept方法就可以实现不同的逻辑方法了。
我们可以看到通过访问者模式很好地解决了如何加入新的业务代码而无需重新改动、编译既有代码。但是该模式也不是没有缺点:如果在组合模式中结构加入新的子类的话会导致接口Visitor也跟着改动,导致所有Visitor的子类都需要实现新增的方法。因此这种访问者模式适合于结构不经常变动的情况。
改进访问者模式
前面我们说到了如何使用Visitor模式及使用该模式后的优缺点,下面举具体的例子说明。假设现在客户提出了一个产品集(ProductSet)的概念:随着公司软件版本的增多,需要将同一个版本的产品(Product)都放到产品集(ProductSet)中,而一个品牌包含有多个产品集。因为现在组合结构中增加了一个节点,所以在Visitor接口中也必须随之增加一个叫做visitProductSet的方法,并且会导致原有系统中所有已经实现了Visitor接口的类都需要重新实现并编译。用Java的反射机制可以解决这个问题。
使用Java的Method Reflection机制实现访问者模式
首先我们需要改变一下Visitor接口,接口名叫做ReflectionVisitor,如下所示:
public interface ReflectionVisitor { /** * 定义了一个访问节点的方法 * @param softwareComposite */ public void visitSoftwareComposite(Object softwareComposite); }
在现在的接口的方法里,能接受任意的对象(参数是Object)。
下面实现接口ReflectionVisitor,名叫ReflectionVisitorImpl,代码如下所示:
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(); } } }
这段代码首先判断传入的对象是否是空指针,然后创建class数组和object数组,然后用getMethod方法取得方法名是"visit"、方法的参数是"对象softwareComposite对应的类"的方法,最后调用该方法。调用该方法的时候可能会发生NoSuchMethodException异常,发生这个异常就表明它的子类或者当前类中没有与参数中传入相对应的visit方法。
下面再来写新版本Visitor类,扩展刚写好的那个ReflectionVisitorImpl类,名叫CaculateTotalPriceReflectionVisitor,如下所示:
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; } }
代码中声明了两个visit方法(因为在类ReflectionVisitorImpl中,查找名为visit、参数与传进去的对象匹配的的方法),一个是给Product的,另外一个是给SoftwareSet的。在这里SoftwareSet中并没有价格,只需当前的对象是类Product的实例的时候将价格累加即可。如果在组合模式的结构中增加了新的类,只需要在ReflectionVisitorImpl的扩展类中声明一个visit方法,该方法的参数是新增加的类,对于文中的例子,只需增加下面的一个方法:
public void visit(ProductSet productSet) { //实现的代码 }
在组合结构的接口SoftwareComponent中改一下accept方法,参数是修改后的Visitor接口,如下所示:
public void accept(ReflectionVisitor visitor);
由于在类SoftwareSet、Brand和ProductSet中实现上面accept方法的代码都一样,因此把代码抽象到上层共有的抽象类AbsSoftwareComposite中,如下所示:
public void accept(ReflectionVisitor visitor) { visitor.visitSoftwareComposite(this); Iterator it = childs.iterator(); while (it.hasNext()) { SoftwareComponent component = (SoftwareComponent) it.next(); //递归调用子对象的accept方法 component.accept(visitor); } }
现在如果想在外面要调用的话,代码如下所示(在本文的例子com.test.business.SoftwareManager中可以找到这段代码):
//建立一个新的Visitor对象 CaculateTotalPriceReflectionVisitor reflectionVisitor = new CaculateTotalPriceReflectionVisitor(); //将该visitor对象传到结构中 data.accept(reflectionVisitor); //调用visitor对象的getTotalPrice()方法就返回了总价格 double price = reflectionVisitor.getTotalPrice();
另外由于没有实现Brand类的visit方法,在组合结构遍历到Brand的节点的时候会抛出NoSuchMethodException异常,就是没有关于该节点方法的实现,在当前的程序中会打印出一句话:
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,代码如下所示。
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方法 Method m = targetObject.getClass().getMethod("visit", classes); // 调用该方法 m.invoke(targetObject, objects);
本来的代码中从本身的类及其子类中查找visit方法,而现在是从维护的目标类中查找visit方法。
现在需要写Tag类,这个类扩展了TagSupport类,如下所示(为说明的方便,随本文的例子提供了一个模拟的TagSupport类):
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()); } //别的代码请参见随本文带的源程序 …… }
如果想测试上面写的那段代码,(在本文的例子com.test.business.SoftwareManager中可以找到这段代码))如下所示:
//getMockData()方法返回数据 SoftwareComponent data = getMockData(); MyTag myTag = new MyTag(); myTag.setSoftwareComponent(data); //计算总价格,并打印出来 myTag.doEngTag();
可以看到通过Java的反射机制很好地解决了多重继承的问题,使该访问者模式能够更好地应用于你的应用中。另外可以看到,那些visit方法所在的类已经不是实现了接口ReflectionVisitor,可以说是访问者模式在Java语言的支持下的一种特殊实现。
如果担心引入类反射机制后带来的效率问题,你可以将Method对象通过某种方式缓冲起来,这样不会每次从传入的对象中找visit方法,可以部分地提高效率。
结论
在给定的组合模式的数据结构中,实现业务逻辑的方法非常多,文中试着介绍了几种实现业务逻辑的方法,并给出了相应的实现方式下的优缺点。读者可以综合考虑应用的需求来决定相应的实现方法。