原文地址 树形结构的处理——组合模式(一)
树形结构在软件中随处可见,例如操作系统中的目录结构、应用软件中的菜单、办公系统中的公司组织结构,配置文件生成的结构等等,如何运用面向对象的方式来处理这种树形结构是组合模式需要解决的问题,组合模式通过一种巧妙的设计方案使得用户可以一致性地处理整个树形结构或者树形结构的一部分,也可以一致性地处理树形结构中的叶子节点(不包含子节点的节点)和容器节点(包含子节点的节点)。下面将学习这种用于处理树形结构的组合模式。
Sunny软件公司欲开发一个杀毒(AntiVirus)软件,该软件既可以对某个文件夹(Folder)杀毒,也可以对某个指定的文件(File)进行杀毒。该杀毒软件还可以根据各类文件的特点,为不同类型的文件提供不同的杀毒方式,例如图像文件(ImageFile)和文本文件(TextFile)的杀毒方式就有所差异。现需要提供该杀毒软件的整体框架设计方案。
在介绍Sunny公司开发人员提出的初始解决方案之前,我们先来分析一下操作系统中的文件目录结构,例如在Windows操作系统中,存在如图11-1所示目录结构:
图11-1可以简化为如图11-2所示树形目录结构:
我们可以看出,在图11-2中包含**文件(灰色节点)和文件夹(白色节点)两类不同的元素,其中在文件夹中可以包含文件,还可以继续包含子文件夹,但是在文件中不能再包含子文件或者子文件夹。在此,我们可以称文件夹为容器(Container),而不同类型的各种文件是其成员,也称为叶子(Leaf),一个文件夹也可以作为另一个更大的文件夹的成员。**如果我们现在要对某一个文件夹进行操作,如查找文件,那么需要对指定的文件夹进行遍历,如果存在子文件夹则打开其子文件夹继续遍历,如果是文件则判断之后返回查找结果。
Sunny软件公司的开发人员通过分析,决定使用面向对象的方式来实现对文件和文件夹的操作,定义了如下图像文件类ImageFile、文本文件类TextFile和文件夹类Folder,
类图如下,类图网站
代码如下
//为了突出核心框架代码,我们对杀毒过程的实现进行了大量简化
import java.util.*;
//图像文件类
class ImageFile {
private String name;
public ImageFile(String name) {
this.name = name;
}
public void killVirus() {
//简化代码,模拟杀毒
System.out.println("----对图像文件'" + name + "'进行杀毒");
}
}
//文本文件类
class TextFile {
private String name;
public TextFile(String name) {
this.name = name;
}
public void killVirus() {
//简化代码,模拟杀毒
System.out.println("----对文本文件'" + name + "'进行杀毒");
}
}
//文件夹类
class Folder {
private String name;
//定义集合folderList,用于存储Folder类型的成员
private ArrayList<Folder> folderList = new ArrayList<Folder>();
//定义集合imageList,用于存储ImageFile类型的成员
private ArrayList<ImageFile> imageList = new ArrayList<ImageFile>();
//定义集合textList,用于存储TextFile类型的成员
private ArrayList<TextFile> textList = new ArrayList<TextFile>();
public Folder(String name) {
this.name = name;
}
//增加新的Folder类型的成员
public void addFolder(Folder f) {
folderList.add(f);
}
//增加新的ImageFile类型的成员
public void addImageFile(ImageFile image) {
imageList.add(image);
}
//增加新的TextFile类型的成员
public void addTextFile(TextFile text) {
textList.add(text);
}
//需提供三个不同的方法removeFolder()、removeImageFile()和removeTextFile()来删除成员,代码省略
//需提供三个不同的方法getChildFolder(int i)、getChildImageFile(int i)和getChildTextFile(int i)来获取成员,代码省略
public void killVirus() {
System.out.println("****对文件夹'" + name + "'进行杀毒"); //模拟杀毒
//如果是Folder类型的成员,递归调用Folder的killVirus()方法
for(Object obj : folderList) {
((Folder)obj).killVirus();
}
//如果是ImageFile类型的成员,调用ImageFile的killVirus()方法
for(Object obj : imageList) {
((ImageFile)obj).killVirus();
}
//如果是TextFile类型的成员,调用TextFile的killVirus()方法
for(Object obj : textList) {
((TextFile)obj).killVirus();
}
}
}
编写如下客户端测试代码进行测试:
class Client {
public static void main(String args[]) {
Folder folder1,folder2,folder3;
folder1 = new Folder("Sunny的资料");
folder2 = new Folder("图像文件");
folder3 = new Folder("文本文件");
ImageFile image1,image2;
image1 = new ImageFile("小龙女.jpg");
image2 = new ImageFile("张无忌.gif");
TextFile text1,text2;
text1 = new TextFile("九阴真经.txt");
text2 = new TextFile("葵花宝典.doc");
folder2.addImageFile(image1);
folder2.addImageFile(image2);
folder3.addTextFile(text1);
folder3.addTextFile(text2);
folder1.addFolder(folder2);
folder1.addFolder(folder3);
folder1.killVirus();
}
}
编译并运行程序,输出结果如下:
在学习组合模式之前,这个是很中规中矩的设计,对其分析有如下发现
(1) Folder里面定义的变量是文件夹实现类,所以函数也是针对具体文件夹实现类,因为树结构中,增加节点和删除节点是必须操作,修改节点值比较可能需要,针对具体类型的话,每增加一种新类型的具体文件夹实现类,都要到Folder中增加对应的增删方法和修改方法。在具体类多起来之后,这些代码会看起来比较冗余,虽然可以用分布类的方式将他们挨个分到不同的文件让结构清晰;
(2) 即使addImageFile 和 addTextFile可以改为同名重载函数, 由于系统没有提供抽象层(即所有这些节点的公共部分抽象出来的父类),客户端代码起码在成员定义的时候要有区别地对待充当容器的文件夹Folder和充当叶子的ImageFile和TextFile,无法统一对它们进行处理,如果需求分析发现大部分情况他需要的都是统一操作,例如添加到树之前的数据过滤操作, 这时因为没有抽象层,所以在过滤时需要用分支语句来分别调用对应节点的过滤函数,而不是统一调用抽象层的虚函数。另外增加新的类型的时候,客户端也要跟着一起进行新类型的成员定义和常规方法操作。
面对以上问题,Sunny软件公司的开发人员该如何来解决?这就需要用到本章将要介绍的组合模式,组合模式为处理树形结构提供了一种较为完美的解决方案,它描述了如何将容器和叶子进行递归组合,使得用户在使用时无须对它们进行区分,可以一致地对待容器和叶子。