实例场景:我们平时操作文件夹的复制,无论是文件中包含文件或者文件中的子文件我们都会一并将其复制到相应的目录下面,在这个场景中我们使用了组合模式。
组合模式:属于结构性模式,它描述了对象间的组合关系。对象间常常通过树结构来组织(包含)起来,以实现整体-部分的层次结构。整体上可以看做是一个组合对象。抛却各种复杂的术语。
组合模式的特点是:对象通过实现(继承)统一的接口(抽象类),调用者对单一对象和组合对象的操作具有一致性。
场景分析:文件夹系统就是一个树形结构,它里面包含了整体与部分的层次结构,文件和文件夹提供给用户的操作是一样的,比如说复制文件或文件夹,于是,可以通过一个统一的接口将文件和文件夹统一起来,对用户提供一致的操作,屏蔽不同的复制实现过程。我们在复制文件夹的时候,操作系统实现了对文件夹内的所有文件和文件夹的复制,即实现了组合对象的整体复制,而不是一个空的文件夹;这和我们复制单个文件的体验是一致的。这便是组合模式的妙处。
组合模式的角色:
抽象构件角色(Component):为组合的对象声明接口,在某些情况下实现从此接口派生出的所有类共有的默认行为;定义一个接口可以访问及管理它的多个子部件
叶部件(Leaf):在组合中表示叶节点对象,叶节点没有子节点;定义组合中接口对象的行为
组合类(Composite):定义有子节点(子部件)的部件的行为;存储子节点;在Component接口中实现与子部件相关的操作。
客户端(Client):通过Component接口控制组合部件的对象。
场景代码实现:
package cn.com.composite;
//抽象构件角色,为所有文件及文件夹定义统一的接口
public interface Component {
public void copy();//定义一个文件复制的方法
}
package cn.com.composite;
//文件,相当与叶子节点(叶部件)
public class MyFile implements Component {
private String name;
public MyFile(String name){
this.name=name;
}
@Override
public void copy() {
System.out.println("复制文件:"+name);
}
}
package cn.com.composite;
import java.util.ArrayList;
import java.util.List;
//文件夹,相当与组合类(Composite)
public class Folder implements Component {
private String name;
public Folder(String name){
this.name=name;
}
//里面装Component表示可以操作Component
List<Component> list=new ArrayList<Component>();
//增加构件角色,增加文件或者文件夹
public void add(Component component){
list.add(component);
}
//删除文件或文件夹
public void remove(Component componet){
list.remove(componet);
}
//返回所有文件及文件夹
public List<Component> getAll(){
return this.list;
}
//对文件及文件夹进行复制操作
@Override
public void copy() {
System.out.println("复制文件夹:"+name);
//遍历文件及文件夹,如果是文件就会复制,如果是文件夹
//就会遍历文件夹中的文件进行复制,隐藏了一个递归操作
for(Component component:list){
component.copy();
}
}
}
package cn.com.composite;
public class Client {
public static void main(String[] args) {
Component file1=new MyFile("你是我的眼.mp3");
Component file2=new MyFile("伤不起.mp3");
Folder music=new Folder("我的音乐");
music.add(file1);
music.add(file2);
Component file3=new MyFile("我的资料.doc");
Component file4=new MyFile("我的图片.jpg");
Folder folder=new Folder("我的文件");
folder.add(file3);
folder.add(file4);
folder.add(music);//添加一个文件夹
folder.copy();
}
}
结果输出:复制文件夹:我的文件 复制文件:我的资料.doc 复制文件:我的图片.jpg 复制文件夹:我的音乐 复制文件:你是我的眼.mp3 复制文件:伤不起.mp3
由以上的代码和运行结果可知:
通过实现组合模式,用户对文件夹的操作与对普通文件的操作并无差异。用户完全不用关心这是文件夹还是文件,也不用关心文件夹内部的具体结构,就可以完成相关操作。同样的道理,我们可以表达如下:
通过实现组合模式,调用者对组合对象的操作与对单一对象的操作具有一致性。调用者不用关心这是组合对象还是文件,也不用关心组合对象内部的具体结构,就可以调用相关方法,实现功能。
类比JUnit中,当我们定义TestCase时与suite,运行的时候发现无论运行多个测试案例还是单个测试案例,他们运行没有什么差异,因为在这个里面就用到了我们的组合模式,在Suite中遍历TestCase,执行其方法
仔细分析copy()方法的代码,我们会发现,如果从面向过程的角度思考,组合模式通过递归原理实现了树结构(组合对象)的深度优先遍历。
代码中还有一种实现方式:就是把那些方法都定义在Component接口中,在Leaf中对其进行空实现,那么我们在Client端就都可以换成是Component引用了。
代码如下:
package cn.com.composite;
import java.util.List;
//抽象构件角色,为所有文件及文件夹定义统一的接口
public interface Component {
public void copy();//定义一个文件复制的方法
public void add(Component component);
public void remove(Component componet);
public List<Component> getAll();
}
package cn.com.composite;
import java.util.List;
//文件,相当与叶子节点(叶部件)
public class MyFile implements Component {
private String name;
public MyFile(String name){
this.name=name;
}
@Override
public void copy() {
System.out.println("复制文件:"+name);
}
@Override
public void add(Component component) {
}
@Override
public List<Component> getAll() {
return null;
}
@Override
public void remove(Component componet) {
}
}
//Client都可以换成Component引用
Component file1=new MyFile("你是我的眼.mp3");
Component file2=new MyFile("伤不起.mp3");
Component music=new Folder("我的音乐");
music.add(file1);
music.add(file2);
Component file3=new MyFile("我的资料.doc");
Component file4=new MyFile("我的图片.jpg");
Component folder=new Folder("我的文件");
folder.add(file3);
folder.add(file4);
folder.add(music);//添加一个文件夹
folder.copy();
使用组合模式时考虑的几个问题:
- Composite模式采用树形结构来实现普遍存在的对象容器,从而将“一对多”的关系转换为“一对一”的关系,使得客户代码可以一致地处理对象和对象容器,无需关系处理的是单个对象还是组合的对象容器。
- 将客户代码与复杂的对象容器解耦是合成模式的核心思想,解耦之后,客户代码将与纯粹的抽象接口—----非对象容器的内部实现结构发生依赖关系。
- 有时候系统需要遍历一个树枝结构的子构件很多次,这时候可以考虑把遍历子构件的结构暂时存储在父构件里面作为缓存。
- Composite模式中,是将Add和Remove等和对象容器相关的方法定义在“表示抽象的Componont类”中,还是定义在“表示对象容器的Composite类”中,是一个关乎“透明性”和“安全性”的两难问题,需要仔细权衡。