定义
Compose objects into tree structures to represent part-whole hierarchies.Composite lets clients treat individual objects and compositions of objects uniformly.
组合模式又称整体-部分模式,是一种结构型模式,它将对象组成树状结构用来表示对象之间整体和部分的层级结构关系,让客户端可以用一致的方式访问个别对象和组合对象。
背景
电脑上的目录结构像一棵树,如果我们需要删除某些大的文件和目录,那么需要一层一层的查找:首先,打开根目录进行查找,如果没找到,接着打开里面的目录;然后,接着找,如果还是没找到,再打开里面的目录;
如此一层层的打开,直到遍历完所有的目录和文件为止。如果用程序来显示某个目录下的文件和子目录大小,那么伪代码如下:
public static void main(String[] args) {
//获取目录下的文件列表
List<File> files = new LinkedList<>();
for (File file : files) {
System.out.println("名称:"+file.getName()+",大小:"+file.getSize());
}
//多层遍历并汇总文件大小
for (Directory directory : directories) {
Integer totalSize=0;
//多层遍历直至最后一层
for(Directory child :directory.getChildren()){
//汇总文件大小
List<File> subFiles = new LinkedList<>();
for (File subFile : subFiles) {
totalSize+=subFile.getSize();
}
}
System.out.println("名称:"+directory.getName()+",大小:"+totalSize);
}
}
实现逻辑可分为两步:第一,查找目录下的文件并获取其大小;第二,一层层遍历子目录并汇总子目录中文件大小。
问题
从上面的代码中我们可以看出,文件和目录被定义成了不同种类的对象。这样做的问题是文件和目录的操作不一致,增大了客户端使用对象的复杂度。
其实,客户端只想获取某个资源的大小,这个资源不管是构成整体的部分(文件),还是由部分构成的整体(目录),这些客户端都不太关心。
所以,如果能隐藏文件和目录对客户端的差异或者说让客户端能以一致的方式获取文件和目录的大小,那么自然就能降低客户端的复杂度。
方案
解决上面的问题比较合适的方式是组合模式:首先,它为文件和目录定义了共同的接口,这样就保证客户端对两者的操作是一致的;
其次,它把文件和目录当作树中的节点,并将它们组织成一棵有层级结构的树,这样客户端只需和树交互就可以操作树中任何节点;
最后,在树的内部只有一种类型的对象即节点并且是自关联的,这样就可以进行递归遍历。
结构
构件角色(Component):是树的构件叶子与树枝的接口,它统一了构件的操作,使客户端可以一致的使用构件;
叶子角色(Leaf):在树中它表示一个没有子节点的构件,是操作的实际实现类。
树枝角色(Composite):这是一个有子子节点的构件,它负责存储子节点:叶子和树枝,并定义了管理和访问子节点的方法。
客户角色(Client):它通过Component接口,以一致的方式和叶子以及树枝构件交互。
/**构件*/
public interface Component {
public void operation();
}
/**叶子*/
public class Leaf implements Component{
@Override
public void operation() {
System.out.println("我是叶子节点");
}
}
/**树枝*/
public class Composite implements Component{
protected List<Component> components;
public boolean addChild(Component component) {
return this.components.add(component);
}
public boolean removeChild(Component component) {
return this.components.remove(component);
}
public Component getChild(int index) {
return this.components.get(index);
}
public void operation() {
System.out.println("我是树枝节点");
}
}
/**客户端*/
public class Client {
public static void main(String[] args) {
//根节点
Composite root = new Composite();
//树枝节点
Composite branchA = new Composite();
Composite branchB = new Composite();
//叶子节点
Leaf leafA = new Leaf();
Leaf leafB = new Leaf();
branchA.addChild(leafA);
branchB.addChild(leafB);
root.addChild(branchA);
root.addChild(branchB);
Component component = root.getChild(0);
component.operation();
}
}
应用
接下来,我们使用组合模式重构一下"查看目录大小的程序",使客户端可以一致地使用"不同的对象"。
首先,创建一个资源接口,可以用它来表示文件和目录。
/**资源接口*/
public interface IResource {
/**文件和目录对象的公共接口*/
public Integer getSize();
}
然后,创建具体的文件类并实现抽象的资源接口。
/**文件类*/
public class File implements IResource{
@Override
public Integer getSize() {
return 1024;
}
}
现在,创建具体的目录类同时也实现抽象的资源接口,并且它需要定义特有的方法add、remove、get方便对子节点进行操作。
/**目录类*/
public class Directory implements IResource{
//关联抽象类型的节点
protected List<IResource> resources;
public Directory(List<IResource> resources){
this.resources = resources;
}
public boolean add(IResource resource) {
return this.resources.add(resource);
}
public boolean remove(IResource resource) {
return this.resources.remove(resource);
}
public IResource get(int index) {
return this.resources.get(index);
}
@Override
public Integer getSize() {
Integer total= 0;
//递归汇总文件大小
for (IResource resource : resources) {
total += resource.getSize();
}
return total;
}
}
最后,我们在看看客户端如何使用组合模式查看文件和文件夹大小。
public class Client {
public static void main(String[] args) {
Directory root = new Directory();
File fileA = new File();
root.add(fileA);
Directory subDirectory = new Directory();
File fileB = new File();
subDirectory.add(fileB);
root.add(subDirectory);
//这样无论获取目录还是文件大小
//两者的操作是一致的
//获取根目录大小
Integer rootSize = root.getSize();
//获取文件大小
IResource file = root.get(0);
file.getSize();
}
}