组合模式
图中的文件结构可以称之为树形结构,在数据结构中我们可以通过调用某个方法来遍历整棵树,当我们找到某个叶子节点后,就可以对叶子节点进行相关的操作。我们可以将这颗树理解成一个大的容器,容器里面包含很多的成员对象,这些成员对象即可是容器对象也可以是叶子对象。但是由于容器对象和叶子对象在功能上面的区别,使得我们在使用的过程中必须要区分容器对象和叶子对象,但是这样就会给客户带来不必要的麻烦,作为客户而已,它始终希望能够一致的对待容器对象和叶子对象。
组合模式的设计动机:组合模式定义了如何将容器对象和叶子对象进行递归组合,使得客户在使用的过程中无须进行区分,可以对他们进行一致的处理。
一、模式定义
组合模式组合多个对象形成树形结构以表示“整体-部分”的结构层次。
组合模式对单个对象(叶子对象)和组合对象(组合对象)具有一致性,它将对象组织到树结构中,可以用来描述整体与部分的关系。同时它也模糊了简单元素(叶子对象)和复杂元素(容器对象)的概念,使得客户能够像处理简单元素一样来处理复杂元素,从而使客户程序能够与复杂元素的内部结构解耦。
上面的图种的文件系统,文件系统由文件和目录组成,目录下面也可以包含文件或者目录,计算机的文件系统是用递归结构来进行组织的,对于这样的数据结构是非常适用使用组合模式的。
在使用组合模式中需要注意一点也是组合模式最关键的地方:叶子对象和组合对象实现相同的接口。这就是组合模式能够将叶子节点和对象节点进行一致处理的原因。
二、模式结构
组合模式主要包含如下几个角色:
1. Component : 组合中的对象声明接口,在适当的情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理Component子部件。
2. Leaf: 叶子对象。叶子结点没有子结点。
3. Composite: 容器对象,定义有枝节点行为,用来存储子部件,在Component接口中实现与子部件有关操作,如增加(add)和删除(remove)等。
从模式结构中我们看出了叶子节点和容器对象都实现Component接口,这也是能够将叶子对象和容器对象一致对待的关键所在。
注意:
1. 这里的 Component 即可以是接口也可以是抽象类,都是通过子类去实现抽象的方法,在这里不做区分。
2. Component 是否有 add() 方法和 remove() 方法是有区分的,如果没有以上两种方法,那么叫做 安全方式 。如果有以上两种方法,叫 透明方式。
下面两道例题可以用来做区分。
三、 模式实现
3.1 水果盘问题 (安全方式)
在水果盘(Plate)中有一些水果,如 苹果(Apple) 、香蕉(Banana) 、 梨(Pear) ,当然 大水果盘中还可以有小水果盘 ,现需要对盘中的水果进行遍历( 吃 ),当然如果对一 个 水 果 盘执行 “吃 ”方法,实际上就是吃其中的水果。使用组合模式模拟该场景。
类图如下:
Component:组合对象的声明接口
public abstract class MyElement {
public abstract void eat();
}
Leaf:叶子节点,包括苹果、香蕉、梨
public class Apple extends MyElement {
public void eat(){
System.out.println("eat apple !");
}
}
public class Banana extends MyElement {
public void eat(){
System.out.println("eat Banana !");
}
}
public class Pear extends MyElement {
public void eat(){
System.out.println("eat Pear !");
}
}
Composite:容器对象
public class Plate extends MyElement {
private ArrayList<MyElement> list = new ArrayList();
public void add(MyElement element) {
list.add(element);
}
public void delete(MyElement element) {
list.remove(element);
}
public void eat() {
for(MyElement element: list) {
element.eat();
}
System.out.println("--------------------");//用于分隔
}
}
演示:客户端代码
public class Client{
public static void main(String[] args) {
MyElement obj1, obj2, obj3, obj4, obj5;
Plate plate1, plate2, plate3;
obj1 = new Apple();
obj2 = new Pear();
plate1 = new Plate();
plate1.add(obj1);//盘子1放苹果和梨
plate1.add(obj2);
obj3 = new Banana();
obj4 = new Banana();
plate2 = new Plate();
plate2.add(obj3);//盘子2放两个香蕉
plate2.add(obj4);
obj5 = new Apple();
plate3 = new Plate();
plate3.add(plate1);
plate3.add(plate2);
plate3.add(obj5);//盘子3放盘子1、盘子2和苹果
plate3.eat();
}
}
结果展示:
3.2 杀毒软件 (透明方式)
使用组合模式设计一个杀毒软件(AntiVirus)的框架,该软件既可以对某个文件夹(Folder)杀毒,也可以对某个指定的文件(File)进行杀毒,文件种类包括文本文件TextFile、图片文件ImageFile、视频文件VideoFile。绘制类图并编程模拟实现。
类图如下:
Component:组合对象的声明接口
public interface AbstractFile {
void add(AbstractFile file);
void remove(AbstractFile file);
void killVirus();
}
Leaf:叶子节点,包括图像文件、文本文件以及视频文件
class ImageFile implements AbstractFile { //图像文件类:叶子构件
private String name;
public ImageFile(String name) {
this.name = name;
}
public void add(AbstractFile file) {
System.out.println("对不起,不支持该方法!");
}
public void remove(AbstractFile file) {
System.out.println("对不起,不支持该方法!");
}
public void killVirus() {
//模拟杀毒
System.out.println("----对图像文件'" + name + "'进行杀毒");
}
}
class TextFile implements AbstractFile { //文本文件类:叶子构件
private String name;
public TextFile(String name) {
this.name = name;
}
public void add(AbstractFile file) {
System.out.println("对不起,不支持该方法!");
}
public void remove(AbstractFile file) {
System.out.println("对不起,不支持该方法!");
}
public void killVirus() {
//模拟杀毒
System.out.println("----对文本文件'" + name + "'进行杀毒");
}
}
class VideoFile implements AbstractFile { //视频文件类:叶子构件
private String name;
public VideoFile(String name) {
this.name = name;
}
public void add(AbstractFile file) {
System.out.println("对不起,不支持该方法!");
}
public void remove(AbstractFile file) {
System.out.println("对不起,不支持该方法!");
}
public void killVirus() {
//模拟杀毒
System.out.println("----对视频文件'" + name + "'进行杀毒");
}
}
Composite:容器对象
class Folder implements AbstractFile { //文件夹类:容器构件
//定义集合fileList,用于存储AbstractFile类型的成员
private ArrayList<AbstractFile> list=new ArrayList();
private String name;
public Folder(String name) {
this.name = name;
}
public void add(AbstractFile file) {
list.add(file);
}
public void remove(AbstractFile file) {
list.remove(file);
}
public void killVirus() {
System.out.println("****对文件夹'" + name + "'进行杀毒"); //模拟杀毒
//递归调用成员构件的killVirus()方法
for(AbstractFile file : list) {
file.killVirus();
}
}
}
演示:客户端代码
class Client { //客户端类
public static void main(String args[]) {
//针对抽象构件编程
AbstractFile file1, file2, file3, file4, file5;
AbstractFile folder1,folder2, folder3;
folder1 = new Folder("文本文件");
folder2 = new Folder("图像文件");
folder3 = new Folder("视频文件");
file1 = new TextFile("记事本.txt");
file2 = new ImageFile("转圈圈.gif");
file3 = new ImageFile("跑步.png");
file4 = new VideoFile("电影1.rmvb");
file5 = new VideoFile("电影2.rmvb");
folder1.add(file1);
folder2.add(file2);
folder2.add(file3);
folder3.add(file4);
folder3.add(file5);
folder1.add(folder2);
folder1.add(folder3);
folder1.killVirus();
}
}
结果展示:
四、 模式优缺点
优点:
- 可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,使得增加新构件也更容易。
- 客户端调用简单,客户端可以一致的使用组合结构或其中单个对象。
- 定义了包含叶子对象和容器对象的类层次结构,叶子对象可以被组合成更复杂的容器对象,而这个容器对象又可以被组合,这样不断递归下去,可以形成复杂的树形结构。
- 更容易在组合体内加入对象构件,客户端不必因为加入了新的对象构件而更改原有代码。
缺点:
使设计变得更加抽象,对象的业务规则如果很复杂,则实现组合模式具有很大挑战性,而且不是所有的方法都与叶子对象子类都有关联。
五、 模式适用场景
1、需要表示一个对象整体或部分层次,在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,可以一致地对待它们。
2、让客户能够忽略不同对象层次的变化,客户端可以针对抽象构件编程,无须关心对象层次结构的细节。
六、 模式总结
-
组合模式用于将多个对象组合成树形结构以表示“整体-部分”的结构层次。组合模式对单个对象(叶子对象)和组合对象(容器对象)的使用具有一致性。
-
组合对象的关键在于它定义了一个抽象构建类,它既可表示叶子对象,也可表示容器对象,客户仅仅需要针对这个抽象构建进行编程,无须知道他是叶子对象还是容器对象,都是一致对待。
-
组合模式虽然能够非常好地处理层次结构,也使得客户端程序变得简单,但是它也使得设计变得更加抽象,而且也很难对容器中的构件类型进行限制,这会导致在增加新的构件时会产生一些问题。