定义:(GoF《设计模式》):将对象组合成树形结构以表示“部分整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
从定义中可以看出,组合模式用来表示部分与整体的层次结构(类似于树结构),而且也可以使用户对单个对象(叶子节点)以及组合对象(非叶子节点)的使用具有一致性,一致性的意思就是说,这些对象都拥有相同的接口。
很多书中包括文章都喜欢使用公司、子公司以及部门的例子,这其实就是一个典型的树结构。其实生活当中的树结构还有很多,比如书的目录、文件系统、网站的菜单等等,有很多很多。
我们先来看看组合模式的类图,引自百度百科。
类图当中有三个类,一个是Component(节点的统一接口),它的目的是为了统一节点的操作。接下来的两个实现类,一个则是非叶子节点(Composite),它可以有子节点。另外一个则是叶子节点(Leaf),它不能含有子节点。
我们随便挑一个树结构的例子,比如文件系统,我们来分析一下,在文件系统中,如果使用组合模式,各个部分的类都应该是什么样子的。
首先,文件系统中,叶子节点是文件,非叶子节点是文件夹,所以Leaf和Composite实现类就是文件和文件夹。对于Component接口,其实也很简单,就是提取文件和文件夹的共性就可以了。
很显然,二者的共性有很多,比如都可以进行复制、剪切、删除、重命名等操作。但是不同的是,对于文件和文件夹的这些操作是有细微的区别的,最明显的就是删除操作,如果是文件,那么我们只需要删除当前文件即可,而如果是文件夹,则需要删除文件夹下的所有文件以及文件夹,然后再删除该文件夹。
那么定义当中的一致性就体现在,我们的客户端不需要知道当前操作的是文件还是文件夹,它只知道它要进行删除操作,而我们去针对文件类别的不同去进行相应的处理。
下面我们来模拟一下组合模式,采用文件系统。
首先,我们先给出一个接口,它相当于Component接口,定义了文件与文件夹的公共行为。
package com.composite; //文件系统中的节点接口 public interface IFile { //下面两个方法,相当于类图中operation方法 void delete(); String getName(); /* 以上为公共行为,以下为文件夹才有的行为 */ //创建新文件,相当于add方法 void createNewFile(String name); //相当于remove方法 void deleteFile(String name); //相当于GetChild方法 IFile getIFile(int index); }
类图中的operation方法是一个宏观定义,它代表的意思是叶子节点和非叶子节点的公共行为,并不是说只有一个operation方法,本次LZ给出两个共有行为作为代表,即删除操作和获取文件名称的操作。
下面我们来看下非叶子节点,即文件夹的实现类。
package com.composite; import java.util.ArrayList; import java.util.List; //文件夹 public class Folder implements IFile{ private String name; private IFile folder; private List<IFile> files; public Folder(String name) { this(name, null); } public Folder(String name,IFile folder) { super(); this.name = name; this.folder = folder; files = new ArrayList<IFile>(); } public String getName() { return name; } //与File的删除方法不同,先删除下面的文件以及文件夹后再删除自己 public void delete() { List<IFile> copy = new ArrayList<IFile>(files); System.out.println("------------删除子文件-------------"); for (IFile file : copy) { file.delete(); } System.out.println("----------删除子文件结束-------------"); if (folder != null) { folder.deleteFile(name); } System.out.println("---删除[" + name + "]---"); } public void createNewFile(String name) { if (name.contains(".")) { files.add(new File(name,this)); }else { files.add(new Folder(name,this)); } } public void deleteFile(String name) { for (IFile file : files) { if (file.getName().equals(name)) { files.remove(file); break; } } } public IFile getIFile(int index) { return files.get(index); } }
我们看到这里面最主要的地方在于它有一个List<IFile>属性,这个属性是树结构的关键点,当我们删除一个文件夹时,即delete方法,我们会首先删除该文件夹下面的所有文件以及文件夹,这与我们平时使用的windows操作系统的文件操作是一致的。
下面三个方法,createNewFile、deleteFile和getIFile,分别对应于类图当中的add、remove以及getChild方法,只不过为了更加形象,此处修改了方法名称。
下面我们看叶子节点的实现,即文件类。
package com.composite; //文件 public class File implements IFile{ private String name; private IFile folder; public File(String name,IFile folder) { super(); this.name = name; this.folder = folder; } public String getName() { return name; } public void delete() { folder.deleteFile(name); System.out.println("---删除[" + name + "]---"); } //文件不支持创建新文件 public void createNewFile(String name) { throw new UnsupportedOperationException(); } //文件不支持删除文件 public void deleteFile(String name) { throw new UnsupportedOperationException(); } //文件不支持获取下面的文件列表 public IFile getIFile(int index) { throw new UnsupportedOperationException(); } }
文件类中的delete方法与文件夹中的不同,一个文件的删除操作,只需要删除它自己即可。我们还会注意到,下面的三个方法,LZ全部抛出了不支持的操作的异常,这也是与我们传统意义上的文件操作是一致的,一个文件当然不能在该文件下进行创新新文件、删除文件以及获取某个文件的操作。
当然,你也可以直接将三个方法放空,或者返回null值,不过LZ觉得这样的方式不易于以后进行调试,所以LZ个人不推荐。
下面我们来简单的模拟下我们的文件系统,我们创建一个简单的文件系统,然后在上面进行删除操作。
package com.composite; public class Main { public static void main(String[] args) { IFile root = new Folder("我的电脑"); root.createNewFile("C盘"); root.createNewFile("D盘"); root.createNewFile("E盘"); IFile D = root.getIFile(1); D.createNewFile("project"); D.createNewFile("电影"); IFile project = D.getIFile(0); project.createNewFile("test1.java"); project.createNewFile("test2.java"); project.createNewFile("test3.java"); IFile movie = D.getIFile(1); movie.createNewFile("致青春.avi"); movie.createNewFile("速度与激情6.avi"); /* 以上为当前文件系统的情况,下面我们尝试删除文件和文件夹 */ display(null, root); System.out.println(); project.delete(); movie.getIFile(1).delete(); System.out.println(); display(null, root); } //打印文件系统 public static void display(String prefix,IFile iFile){ if (prefix == null) { prefix = ""; } System.out.println(prefix + iFile.getName()); if(iFile instanceof Folder){ for (int i = 0; ; i++) { try { if (iFile.getIFile(i) != null) { display(prefix + "--", iFile.getIFile(i)); } } catch (Exception e) { break; } } } } }
我们首先模拟了一个简单的文件系统,有C/D/E盘,然后又在D盘下建立了两个文件夹以及一些文件,接下来我们使用统一的操作接口去操作文件和文件夹,进行删除操作。
在删除的前后,LZ分别打印了一遍当前的文件系统,结果如下。
可以看到,我们成功删除了[project]文件夹和[速度与激情6.avi]文件,在删除[project]文件夹时,首先删除了其文件夹下面的三个java文件。
所以结合组合模式的定义,在上面的例子中,我们做了下面两件事,正好是组合模式定义中提到的。
第一、就是我们使用组合模式,描述了一个文件系统的树结构。
第二、就是在组合模式下,我们给客户端提供了统一的删除操作,当然,我们还可以提供统一的复制,剪切,查看文件属性等等操作,只不过作为例子,我们只列出了删除操作。
上面我们针对标准的组合模式,给出了一个例子,下面请各位思考一下,上面的例子当中,是否有不妥的地方。
答案是肯定的,上面的例子当中,我们的叶子节点类(File)中,有三个不支持的方法,而之所以出现这样的情况,是因为我们在IFile接口中,提供的是宽接口,这样做的目的是为了对客户端保持透明,然而相应的却带来了不安全性。
所以有时候我们为了安全性,会相应的牺牲透明性,把IFile接口中叶子节点不支持的三个行为全部删掉,由此可见,在组合模式中,安全性和透明性是互相矛盾的,这是由于叶子节点和非叶子节点行为的不一致以及需要提供一个一致的行为接口所造成的,是不可调和的矛盾。
针对这种情况,我们只能做出相应的取舍,如果我们使用非透明且相对安全的方式去实现上面的例子,那么我们的客户端调用时,会经常出现下面这样的代码。
IFile movie = D.getIFile(1); if (movie instanceof Folder) { Folder folder = (Folder) movie; //下面使用folder进行文件夹独有的操作 }
出现上面代码的原因很明显,这是由于我们IFile接口不再提供Folder的行为所造成的。所以使用非透明的组合模式,会相应的增加客户端操作的复杂性。
LZ个人认为大部分情况下,我们应当优先考虑透明的策略,即本文给出的方式。