本文从一个给定的实现了组合(Composite)模式的例子开始说明怎么在这个数据结构上实现业务逻辑代码依次介绍了非面向对象的方式在组合结构中加入方法使用访问者(Visitor)模式以及用改进后的访问者(Visitor)模式来实现相同的业务逻辑代码并且对于每种实现分别给出了优缺点 读者定位于具有Java程序开发和设计模式经验的开发人员 读者通过本文可以学到如何在组合(Composite)模式中实现各种不同的业务方法及其优缺点 组合(Composite)模式 组合模式是结构型模式中的一种GOF的《设计模式》一书中对使用组合模式的意图描述如下将对象组合成树形结构以表示部分整体的层次结构Composite使得用户对单个对象和组合对象的使用具有一致性 组合模式应用广泛根据GOF中对组合模式的定义Composite模式一般由Component接口Leaf类和Composite类组成现在需要对一个软件产品管理系统的实体建模某公司开发了一系列软件集(SoftwareSet)包含了多种品牌(Brand)的软件产品就象IBM提供了LotusWebsPhere等品牌每个品牌下面又有各种产品(Product)如IBM的Lotus下面有Domino Server/Client产品等建模后的类图如下(代码可以参见随本文带的附件中包comtestentity下所有的源文件) 如图所示 ()接口SoftwareComponent就是对应于组合模式中的Component接口它定义了所有类共有接口的缺省行为 ()AbsSoftwareComposite类对应于Composite类并且是抽象类所有可以包含子节点的类都扩展这个类这个类的主要功能是用来存储子部件实现了接口中的方法部分可以重用的代码写在此类中 ()SoftwareSet类继承于AbsSoftwareComposite类对应于软件集软件集下直接可以包含品牌(Brand)也可以直接包含不属于任何品牌的产品(Product) ()Brand类继承于AbsSoftwareComposite类对应于品牌包含了品牌名属性并且用来存储Product类的实例 ()Product类就是对应的Leaf类表示叶子节点叶子节点没有子节点 用不同的方法实现业务逻辑 数据结构建立好之后需要在这个数据结构上添加方法实现业务逻辑比如现在的这个例子中有这样的需求给定一些用户选择好的产品需要计算出这些选中后软件的总价格下面开始介绍如何使用各种不同的方法来实现这个业务逻辑 非面向对象的编程方式 这种方式下编程思路最简单遍历SoftwareSet实例中的所有节点如果遍历到的当前对象是Product的话就累加否则继续遍历下一层直到全部遍历完毕代码片断如下 /** *取得某个SoftwareComponent对象下面所有Product的价格 *@parambrand *@return */ publicdoublegetTotalPrice(SoftwareComponentsoftwareComponent){ SoftwareComponenttemp=softwareComponent; doubletotalPrice=; //如果传入的实例是SoftwareSet的类型 if(tempinstanceofSoftwareSet){ Iteratorit=((SoftwareSet)softwareComponent)getChilds() &erator(); while(ithasNext()){//遍历 temp=(SoftwareComponent)itnext(); //如果子对象是Product类型的直接累加 if(tempinstanceofProduct){ Productproduct=(Product)temp; totalPrice+=productgetPrice(); }elseif(tempinstanceofBrand){ //如果子对象是Brand类型的则遍历Brand下面所有的产品并累加 Brandbrand=(Brand)temp; totalPrice+=getBrandPrice(brand); } } }elseif(tempinstanceofBrand){ //如果传入的实例是SoftwareSet的类型则遍历Brand下面所有的产品并累加 totalPrice+=getBrandPrice((Brand)temp); }elseif(tempinstanceofProduct){ //如果子对象是Product类型的直接返回价格 return((Product)temp)getPrice(); } returntotalPrice; } /** *取得某个Brand对象下面所有Product的价格 *@parambrand *@return */ privatedoublegetBrandPrice(Brandbrand){ IteratorbrandIt=brandgetChilds(erator(); doubletotalPrice=; while(brandIthasNext()){ Productproduct=(Product)brandItnext(); totalPrice+=productgetPrice(); } returntotalPrice; }
这段代码的好处是实现业务逻辑的时候无需对前面已经定好的数据结构做改动并且效率比较高缺点是代码凌乱而且频繁使用了instanceof判断类型和强制类型转换代码的可读性不强如果层次多了代码就更加混乱 面向对象的编程方式(将计算价格的方法加入数据结构中) 下面我们采用面向对象的方式可以这么做在接口SoftWareComponent中加入一个方法名叫getTotalPrice方法的声明如下 /** *返回该节点中所有子节点对象的价格之和 *@return */ publicdoublegetTotalPrice(); 由于类Brand和SoftwareSet都继承了AbsSoftwareComposite我们只需在类AbsSoftwareComposite中实现该方法getTotalPrice方法即可如下publicdoublegetTotalPrice(){ Iteratorit=erator(); doubleprice=; while(ithasNext()){ SoftwareComponentsoftwareComponent=(SoftwareComponent)itnext(); //自动递归调用各个对象的getTotalPrice方法并累加 price+=softwareComponentgetTotalPrice(); } returnprice; } 在Product类中实现如下publicdoublegetTotalPrice(){ returnprice; } 在外面需要取得某个对象的总价格的时候只需这样写(在本文的例子comtestbusinessSoftwareManager中可以找到这段代码)//getMockData()方法返回数据 SoftwareComponentdata=getMockData(); //只需直接调用data对象的getTotalPrice方法就可以返回该对象下所有product对象的价格 doubleprice=datagetTotalPrice(); //找到某个对象后直接调用其getTotalPrice方法也可以返回总价格 price=datafindSoftwareComponentByID(id)getTotalPrice();
现在把业务逻辑的实现都放在了数据结构中(组合模式的结构中)好处很明显每个类只管理自己相关的业务代码的实现跟前面举的面向过程方式的实现方式相比没有了instanceof和强制类型转换但是不好的地方是如果需要增加新的业务方法的话就很麻烦必须在接口SoftWareComponent中首先声明该方法然后在各个子类中实现并且重新编译 使用访问者模式 使用访问者模式就能解决上面提到的问题如果要经常增加或者删除业务功能方法的话需要频繁地对程序进行重新实现和编译根据面向对象设计原则之一的SRP(单一职责原则)原则如果一个类承担了多于一个的职责那么引起该类变化的原因就会有多个就会导致脆弱的设计在发生变化时原有的设计可能会遭到意想不到的破坏下面我们引入了一个叫做Visitor的接口该接口中定义了针对各个子类的访问方法如下所示 publicinterfaceVisitor{ publicvoidvisitBrand(Brandbrand); publicvoidvisitSoftwareSet(SoftwareSetsoftwareSet); publicvoidvisitProduct(Productproduct); } visitBrand方法是访问Brand对象节点的时候用的剩下的方法依次类推并在接口SoftwareComponent中增加一个方法publicvoidaccept(Visitorvisitor); 在SoftwareSet中实现接口中的accept方法首先直接调用Visitor接口中的visitSoftwareSet方法传入的参数是本身对象然后递归调用子对象的accept方法publicvoidaccept(Visitorvisitor){ visitorvisitSoftwareSet(this); Iteratorit=erator(); while(ithasNext()){ SoftwareComponentcomponent=(SoftwareComponent)itnext(); componentaccept(visitor); } } 在Brand中实现接口中的accept方法首先直接调用Visitor接口中的visitBrand方法传入的参数是本身对象然后递归调用子对象的accept方法publicvoidaccept(Visitorvisitor){ visitorvisitBrand(this); Iteratorit=erator(); while(ithasNext()){ SoftwareComponentcomponent=(SoftwareComponent)itnext(); componentaccept(visitor); } } 其实在上面的两个类的实现中可以将遍历子节点并调用其accept方法的代码写到父类AbsSoftwareComposite中的某个方法中然后直接调用父类中的这个方法即可这里为了解释方便分别写在了两个子类中在Product中实现接口中的accept方法直接调用Visitor接口的visitProduct方法即可 publicvoidaccept(Visitorvisitor){ visitorvisitProduct(this); } 下面需要实现Visitor接口类名是CaculateTotalPriceVisitor实现了计算总价格的业务逻辑实现代码如下所示publicclassCaculateTotalPriceVisitorimplementsVisitor{ privatedoubletotalPrice; publicvoidvisitBrand(Brandbrand){ } publicvoidvisitSoftwareSet(SoftwareSetsoftwareSet){ } publicvoidvisitProduct(Productproduct){ //每次在组合的结构中碰到Product对象节点的时候就会调用此方法 totalPrice+=productgetPrice(); } publicdoublegetTotalPrice(){ returntotalPrice; } } 上面那段代码中首先在类内定义一个总价格的属性由于Brand和SoftwareSet都没有价格因此在实现中只需在visitProduct方法中累加totalPrice即可在外面如果需要计算总价格的话这样写(在本文的例子comtestbusinessSoftwareManager中可以找到这段代码)//建立一个新的Visitor对象 CaculateTotalPriceVisitorvisitor=newCaculateTotalPriceVisitor(); //将该visitor对象传到结构中 dataaccept(visitor); //调用visitor对象的getTotalPrice()方法就返回了总价格 doubleprice=visitorgetTotalPrice();
下面是它的时序图在类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如下所示 publicinterfaceReflectionVisitor{ /** *定义了一个访问节点的方法 *@paramsoftwareComposite */ publicvoidvisitSoftwareComposite(ObjectsoftwareComposite); }
在现在的接口的方法里能接受任意的对象(参数是Object) 下面实现接口ReflectionVisitor名叫ReflectionVisitorImpl代码如下所示 publicclassReflectionVisitorImplimplementsReflectionVisitor{ publicvoidvisitSoftwareComposite(ObjectsoftwareComposite){ //判断是否是null if(softwareComposite==null){ thrownewNullPointerException(Thevisitnodeshouldnotbenull!); } //组装class数组即调用动态方法的时候参数的类型 Class[]classes=newClass[]{softwareCompositegetClass()}; //组装与class数组相对应的值 Object[]objects=newObject[]{softwareComposite}; try{ //查找visit方法 Methodm=getClass()getMethod(visitclasses); //调用该方法 minvoke(thisobjects); }catch(NoSuchMethodExceptione){ //没有找到相应的方法 Systemout println(Youdidnotimplementthevisitmethodforclass: +softwareCompositegetClass()); }catch(Exceptione){ //发生了别的异常 Systemoutprintln(Catchedexcepctioninvisitmethod); eprintStackTrace(); } } }
这段代码首先判断传入的对象是否是空指针然后创建class数组和object数组然后用getMethod方法取得方法名是visit方法的参数是对象softwareComposite对应的类的方法最后调用该方法调用该方法的时候可能会发生NoSuchMethodException异常发生这个异常就表明它的子类或者当前类中没有与参数中传入相对应的visit方法 下面再来写新版本Visitor类扩展刚写好的那个ReflectionVisitorImpl类名叫CaculateTotalPriceReflectionVisitor如下所示 publicclassCaculateTotalPriceReflectionVisitorextendsReflectionVisitorImpl{ privatedoubletotalPrice; publicvoidvisit(Productproduct){ totalPrice+=productgetPrice(); } publicvoidvisit(SoftwareSetsoftwareSet){ Systemoutprintln(Nopriceforsoftwareset); } publicdoublegetTotalPrice(){ returntotalPrice; } } 代码中声明了两个visit方法(因为在类ReflectionVisitorImpl中查找名为visit参数与传进去的对象匹配的的方法)一个是给Product的另外一个是给SoftwareSet的在这里SoftwareSet中并没有价格只需当前的对象是类Product的实例的时候将价格累加即可如果在组合模式的结构中增加了新的类只需要在ReflectionVisitorImpl的扩展类中声明一个visit方法该方法的参数是新增加的类对于文中的例子只需增加下面的一个方法publicvoidvisit(ProductSetproductSet){ //实现的代码 }
在组合结构的接口SoftwareComponent中改一下accept方法参数是修改后的Visitor接口如下所示 public void accept(ReflectionVisitor visitor); 由于在类SoftwareSetBrand和ProductSet中实现上面accept方法的代码都一样因此把代码抽象到上层共有的抽象类AbsSoftwareComposite中如下所示 publicvoidaccept(ReflectionVisitorvisitor){ visitorvisitSoftwareComposite(this); Iteratorit=erator(); while(ithasNext()){ SoftwareComponentcomponent=(SoftwareComponent)itnext(); //递归调用子对象的accept方法 componentaccept(visitor); } } 现在如果想在外面要调用的话代码如下所示(在本文的例子comtestbusinessSoftwareManager中可以找到这段代码)//建立一个新的Visitor对象 CaculateTotalPriceReflectionVisitorreflectionVisitor =newCaculateTotalPriceReflectionVisitor(); //将该visitor对象传到结构中 dataaccept(reflectionVisitor); //调用visitor对象的getTotalPrice()方法就返回了总价格 doubleprice=reflectionVisitorgetTotalPrice();
另外由于没有实现Brand类的visit方法在组合结构遍历到Brand的节点的时候会抛出NoSuchMethodException异常就是没有关于该节点方法的实现在当前的程序中会打印出一句话 You did not implement the visit method for class:class comtestentityBrand 如果运行程序时发生了别的异常请参见相应的Java API文档 在现在的改进后的访问者模式中如果在组合的结构中新增或删除节点并不会对已经实现了的Visitor产生任何影响如果新增了业务方法只需扩展类ReflectionVisitorImpl就可以了因此很好地解决了访问者模式的问题 改进访问者模式实现与既有代码对接 到现在为止改进后的访问者模式好像已经很好地解决了所有出现的问题但是考虑到有下面的这种情况现在需要写一个JSP的标签库(TagLib)这个标签库还必须具有Visitor的功能(就是需要有遍历节点的功能)可以将节点的内容根据需要打印到HTML页面中由于标签本身需要继承相应的类(如TagSupport)如果继续使用上面提供的方法将无法实现因为Java不允许多重继承不过我们可以将原有ReflectionVisitorImpl的代码再改进一下以解决这种情况新的Visitor的实现类叫NewReflectionVisitorImpl代码如下所示 publicclassNewReflectionVisitorImplimplementsReflectionVisitor{ //实现visit方法的类 privateObjecttargetObject; //构造方法传入实现了visit方法的类 publicNewReflectionVisitorImpl(ObjecttargetObject){ if(targetObject==null) thrownewNullPointerException( Thetargetobjectshouldnotbenull!); thistargetObject=targetObject; } publicvoidvisitSoftwareComposite(ObjectsoftwareComposite){ //……与上个例子相同 try{ //从目标的对象中查找visit方法 Methodm=targetObjectgetClass()getMethod(visitclasses); //调用该方法 minvoke(targetObjectobjects); }catch(NoSuchMethodExceptione){ //……与上个例子相同 }catch(Exceptione){ //……与上个例子相同 } } } 该类的实现与上面的实现差不多多了一个构造函数在该构造函数的参数中传入实现了visit方法的类并且维护了指向该类的一个引用另外最重要的地方是下面的两行代码//从目标的对象中查找visit方法 Methodm=targetObjectgetClass()getMethod(visitclasses); //调用该方法 minvoke(targetObjectobjects);
本来的代码中从本身的类及其子类中查找visit方法而现在是从维护的目标类中查找visit方法 现在需要写Tag类这个类扩展了TagSupport类如下所示(为说明的方便随本文的例子提供了一个模拟的TagSupport类) publicclassMyTagextendsTagSupport{ SoftwareComponentsoftwareComponent=null; privatedoubletotalPrice=; publicintdoEngTag(){ //创建一个visitor对象并且将本身传入visitor对象中 ReflectionVisitorvisitor=newNewReflectionVisitorImpl(this); //遍历结构 softwareComponentaccept(visitor); //打印出价格 outprintln(totalPrice); return; } //实现了针对Product的visit方法 publicvoidvisit(Productproduct){ totalPrice+=productgetPrice(); } publicvoidvisit(Brandbrand){ outprintln(brandgetId()+brandgetDescription()); } //别的代码请参见随本文带的源程序 …… } 如果想测试上面写的那段代码(在本文的例子comtestbusinessSoftwareManager中可以找到这段代码))如下所示//getMockData()方法返回数据 SoftwareComponentdata=getMockData(); MyTagmyTag=newMyTag(); myTagsetSoftwareComponent(data); //计算总价格并打印出来 myTagdoEngTag();
可以看到通过Java的反射机制很好地解决了多重继承的问题使该访问者模式能够更好地应用于你的应用中另外可以看到那些visit方法所在的类已经不是实现了接口ReflectionVisitor可以说是访问者模式在Java语言的支持下的一种特殊实现 如果担心引入类反射机制后带来的效率问题你可以将Method对象通过某种方式缓沖起来这样不会每次从传入的对象中找visit方法可以部分地提高效率 结论 在给定的组合模式的数据结构中实现业务逻辑的方法非常多文中试着介绍了几种实现业务逻辑的方法并给出了相应的实现方式下的优缺点读者可以综合考虑应用的需求来决定相应的实现方法 |