在组合模式中实现访问者(Visitor)模式
本文从一个给定的实现了组合(Composite)模式的例子开始,说明怎么在这个数据结构上实现业务逻辑代码。依次介绍了非面向对象的方式、在组合结构 中加入方法、使用访问者(Visitor)模式以及用改进后的访问者(Visitor)模式来实现相同的业务逻辑代码,并且对于每种实现分别给出了优缺 点。
读者定位于具有Java程序开发和设计模式经验的开发人员。
读者通过本文可以学到如何在组合(Composite)模式中实现各种不同的业务方法及其优缺点。
组合(Composite)模式
组合模式是结构型模式中的一种。GOF的《设计模式》一书中对使用组合模式的意图描述如下:将对象组合成树形结构以表示"部分-整体"的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。
组合模式应用广泛。根据GOF中对组合模式的定义,Composite模式一般由Component接口、Leaf类和Composite类组成。现在 需要对一个软件产品管理系统的实体建模:某公司开发了一系列软件集(SoftwareSet),包含了多种品牌(Brand)的软件产品,就象IBM提供 了Lotus、WebsPhere等品牌。每个品牌下面又有各种产品(Product),如IBM的Lotus下面有 Domino Server/Client产品等。建模后的类图如下(代码可以参见随本文带的附件中,包com.test.entity下所有的源文 件):
如图所示:
(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) {