一、树形结构
树形结构在软件中随处可见,例如操作系统中的目录结构、应用软件中的菜单、办公系统中的公司组织结构等等。
可以看出,在上图中包含两种不同的元素:文件(蓝色节点)和文件夹(白色节点),其中在文件夹中即可以包含文件,也可以包含子文件夹,但是在文件中就不能再包含子文件或者子文件夹了。在此,我们可以称文件夹为容器(Container),而不同类型的各种文件是其成员,也称为叶子(Leaf),一个文件夹也可以作为另一个更大的文件夹的成员。
二、树形结构的普通实现
1、图像文件类
class ImageFile {
private String name;
public ImageFile(String name) {
this.name = name;
}
public void display() { //展示文件内容
System.out.println("----图像文件'" + name + "展示");
}
}
2、文本文件类
class TextFile {
private String name;
public TextFile(String name) {
this.name = name;
}
public void display() { //展示文件内容
System.out.println("----文本文件'" + name + "'展示");
}
}
3、文件夹类
class Folder {
private String name;
//定义集合folderList,用于存储Folder类型的成员
private ArrayList folderList = new ArrayList();
//定义集合imageList,用于存储ImageFile类型的成员
private ArrayList imageList = new ArrayList();
//定义集合textList,用于存储TextFile类型的成员
private ArrayList textList = new ArrayList();
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 display() {
System.out.println("****文件夹'" + name + "'展示");
//如果是Folder类型的成员,递归调用Folder的display()方法
for(Object obj : folderList) {
((Folder)obj).display();
}
//如果是ImageFile类型的成员,调用ImageFile的display()方法
for(Object obj : imageList) {
((ImageFile)obj).display();
}
//如果是TextFile类型的成员,调用TextFile的display()方法
for(Object obj : textList) {
((TextFile)obj).display();
}
}
}
4、存在的问题
(1) 文件夹类Folder的设计和实现都非常复杂,需要定义多个集合存储不同类型的成员,而且需要针对不同的成员提供增加、删除和获取等管理和访问成员的方法,存在大量的冗余代码,系统维护较为困难;
(2) 由于系统没有提供抽象层,客户端代码必须有区别地对待充当容器的文件夹Folder和充当叶子的ImageFile和TextFile,无法统一对它们进行处理;
(3) 系统的灵活性和可扩展性差,如果需要增加新的类型的叶子和容器都需要对原有代码进行修改,例如如果需要在系统中增加一种新类型的视频文件VideoFile,则必须修改Folder类的源代码,否则无法在文件夹中添加视频文件。
三、组合模式
从上面的代码中可以看出,我们分别定义了图片文件类、文本文件类与文件夹类,这是因为文件与文件夹之间的操作不同,文件没有下级节点,而文件夹可以有下级节点,但是可以这样想:既然文件与目录都是可以作为一个节点的下级节点而存在,那么就可以将二者抽象为一类对象,虽然二者的操作不同,但是可以在实现类的方法中具体定义,比如文件没有新增下级节点的方法,就可以在文件的这个方法中抛出一个异常,不做具体实现,而在文件夹中则具体实现新增操作。而二者都有的功能,就可以各自实现。
1、定义
组合模式(Composite Pattern):组合多个对象形成树形结构以表示具有“整体—部分”关系的层次结构。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性,组合模式又可以称为“整体—部分”(Part-Whole)模式,它是一种对象结构型模式。
2、类图
组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器,以此实现递归组合,形成一个树形结构。
如果不使用组合模式,客户端代码将过多地依赖于容器对象复杂的内部实现结构,容器对象内部实现结构的变化将引起客户代码的频繁变化,带来了代码维护复杂、可扩展性差等弊端。组合模式的引入将在一定程度上解决这些问题。
四、运用组合模式
1、构建Component的抽象类
abstract class Component {
public abstract void add(Component c); //增加成员
public abstract void remove(Component c); //删除成员
public abstract Component getChild(int i); //获取成员
public abstract void operation(); //业务方法--对应上例为display()
}
2、文件(叶子)继承Component
class Leaf extends Component {
public void add(Component c) {
//异常处理或错误提示
}
public void remove(Component c) {
//异常处理或错误提示
}
public Component getChild(int i) {
//异常处理或错误提示
return null;
}
public void operation() {
//叶子的具体业务--display()
}
}
3、文件夹(容器)继承Component
class Composite extends Component {
private ArrayList list = new ArrayList();
public void add(Component c) {
list.add(c);
}
public void remove(Component c) {
list.remove(c);
}
public Component getChild(int i) {
return (Component)list.get(i);
}
public void operation() {
//容器的具体业务--display()
//递归调用成员构件的业务方法
for(Object obj:list) {
((Component)obj).operation();
}
}
}
在容器类中实现了在抽象类中声明的所有方法,既包括业务方法,也包括用于访问和管理其子成员的方法,如add()、remove()和getChild()等方法。在实现具体业务方法时,由于容器类充当的是容器角色,包含成员类,因此它将调用其成员类的业务方法。在组合模式结构中,由于容器类中仍然可以包含容器构件,因此在对容器类进行处理时需要使用递归算法,即在容器类的operation()方法中递归调用其成员类的operation()方法。
五、组合模式的分类
1、透明组合模式
上面这种组合模式也称为透明组合模式,抽象类Component中声明了所有用于管理成员对象的方法,包括add()、remove()以及getChild()等方法,这样做的好处是确保所有的类都有相同的接口。叶子与容器所提供的方法是一致的,客户端可以相同地对待所有的类。透明组合模式也是组合模式的标准形式。
透明组合模式的缺点:不安全,因为叶子和容器在本质上是有区别的。叶子不可能有下一个层次的对象,不可能包含成员,因此为其提供add()、remove()以及getChild()等方法是没有意义的,如果没有提供相应的错误处理代码,虽然在编译阶段不会出错,但在运行阶段调用这些方法就会出错。
2、安全组合模式
在抽象类Component中不声明任何用于管理成员对象的方法(add()、remove()以及getChild()等),只声明叶子和容器共同需要的方法,在容器类中声明只有他需要的方法。这种做法是安全的。
安全组合模式的缺点是不够透明,因为叶子和容器具有不同的方法,且容器中那些用于管理成员对象的方法没有在抽象类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子和容器。
六、组合模式在Android中的应用
View和ViewGroup是一种很标准的组合模式:
七、几种模式的区别