编程爱好者联盟 2016-11-29
今天就不国际惯例了,直接进入正文吧,昨天我们学习了单例模式,今天我们来学习一个常见的设计模式,叫做组合模式。
组合模式是个什么鬼呢?听这个名字可能觉得高大上,但是对于作为码农的我们肯定是比较熟悉下面的内容的:
有人会说,博主你不要逗好不好,这不就是一个再普通不过的JAVA Project吗?这有啥好看的?我天天都能看到,对于此,博主想说,你说的对,这个就是作为程序猿的我们天天见到的东西,但是这个东西跟我们今天所说的组合模式有什么关系呢?暂且保密,我们先来看一下对于组合模式的定义:
(GOF《设计模式》):将对象组合成树形结构以表示“部分整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
看到这里,大家就应该明白上面的图片的意思了,没错,就是树形结构,我们的一个普通的JAVA项目中首先会有一个src资源包(source folder),在这个资源包下面我们可以再创建多个package,在package中既可以创建新的package,也可以创建java文件,从而组成一个树形结构。
如果说上面的例子感触不深的话,还有一个更明显的例子就是我们windows系统中的文件目录,我的电脑中有C/D/E/F四个分区,每个分区下又可以有多个文件或者文件夹,文件夹中又可以有文件和文件夹,这也是一个明显的树形结构。
在这种树形结构中,单个对象可以组合成复杂对象,复杂对象又可以组合成更加复杂的对象,理论上是可以无限组合下去的,如果你想要客户端可以忽略这种复杂的层次结构,可以使用统一的方式去操作层次结构中的对象,那么使用组合模式将是你不可多得的选择。
其次,根据组合模式的定义,我们知道,组合模式可以将对象组合成树形结构,用以表示“部分整体”的层次结构,也就是说,在你想表示“部分整体”的层次结构或者说要让客户端忽略复杂的层次结构,使用统一的方式去操作复杂结构中的对象,那么你可以选择使用组合模式。
那么,组合模式定义中所说的使单个对象和组合对象具有一致性又是什么意思呢?其实就是具有统一的接口,只要两者实现同样的接口,那么行为肯定是一致的。
说了半天,我们来看一下组合模式的类图:
通过类图我们可以看到,在组合模式中存在三个类:
1.Conponent类:这个是节点的统一接口,也就是上面所说的单个对象和组合对象具有一致性的实现基础;
2.Leaf类:这个是叶子节点,它不能再包含子节点,也就是上面定义中所说的单个对象;
3.Composite类:这个是非叶子节点,它是可以包含子节点的,可以说是组合对象;
理论部分的内容基本说完了,下面我们来举个具体的例子来说明组合模式,还是文章刚开始的时候给大家看的那个图片,我们知道在src下可以创建新的package,在package下又可以创建package和java文件,而且删除package下的java文件时只是简单的删除了java文件,但是在删除package时,会在删除掉package下的java文件和package后同时删除掉package。
那么怎么体现出定义中所说的单个对象和组合对象具有一致性呢?这里的一致性就在于在客户端不管你删除的是java文件还是package,客户端只管删除,而我们需要针对文件的不同进行相应的处理;
下面我们使用组合模式写出上面的代码,首先需要有一个一致性接口类,也就是Component接口,定义了java文件和package的一致性内容:
public interface IFile { /** * package拥有的方法 */ //新建java文件 public void addFile(String fileName); //删除java文件 public void removeFile(String fileName); //根据索引查询java文件 public IFile getFile(int index); /** * java文件和package共有的方法 */ //删除方法 public void delete(); public String getName(); }
对于一致性接口需要解释一下:
首先,我们知道,对于package我们可以在其下面新增java文件或者package,也可以删除这个文件夹下的java文件但是一个单纯的java文件是不能再删除java文件或者再新建java文件(内部类不算,而且内部类经过编译以后class文件也是单独存在的),那么为什还要在这个接口中写add/remove/get方法呢?
这就牵扯到两个新的概念透明方式和安全方式:
上面我们使用的就是透明方式,这么做的结果就是只要是IFile接口的实现类,都具备了add/remove方法,这样的话叶子节点和枝节点对于外界就没有区别,他们具备完全一致的接口,但是这样做的缺点也是很明确的,叶子节点实现这样的add、remove方法完全没有意义;
如果要使用安全方式,也就是在上面的接口中不声明add/remove方法,那么叶子节点就不需要去实现它们,只有在枝节点的类中声明add/remove方法,这样做虽然可以避免上面透明方式的问题,但是由于不够透明,所以叶子节点和枝节点将不具有相同的接口,客户端每次获取到一个节点都需要判断这个节点是叶子节点还是枝节点,还是比较麻烦的;
好了,解释完之后我们继续看枝节点的实现类:
package com.pattern.combain; import java.util.ArrayList; import java.util.List; public class PackageFile implements IFile{ private String name; private IFile pack; private List<IFile> files; public PackageFile(String name){ this(name,null); } public PackageFile(String name,IFile pack){ super(); this.name = name; this.pack = pack; files = new ArrayList<IFile>(); } @Override public void addFile(String fileName) { if(fileName.indexOf("java")!=-1){ files.add(new JavaFile(fileName,this)); }else{ files.add(new PackageFile(fileName, this)); } } @Override public void removeFile(String fileName) { for(IFile file:files){ if(fileName!=null&&fileName.equals(file.getName())){ files.remove(file); break; } } } @Override public IFile getFile(int index) { return files.get(index); } @Override public void delete() { //1.删除package下的package和java文件 List<IFile> list = new ArrayList<IFile>(files); for(IFile file:list){ file.delete(); } //2.删除当前package if(this.pack!=null){ pack.removeFile(name); } } @Override public String getName(){ return this.name; } public List<IFile> getFiles() { return files; } }
然后是叶子节点的实现类:
package com.pattern.combain; public class JavaFile implements IFile{ private String name; private IFile pack; public JavaFile(String name,IFile pack){ super(); this.name = name; this.pack = pack; } @Override public void addFile(String fileName) { throw new UnsupportedOperationException("java文件不支持创建新的文件"); } @Override public void removeFile(String fileName) { throw new UnsupportedOperationException("java文件不支持移除文件"); } @Override public IFile getFile(int index) { throw new UnsupportedOperationException("java文件不支持根据索引查询java文件"); } @Override public void delete() { pack.removeFile(name); } @Override public String getName(){ return this.name; } }
测试类的内容如下所示:
package com.pattern.combain; public class Test { public static void main(String[] args) { IFile src = new PackageFile("src"); src.addFile("com.pattern.combain"); IFile combainPack = src.getFile(0); combainPack.addFile("IFile.java"); combainPack.addFile("JavaFile.java"); combainPack.addFile("PackageFile.java"); combainPack.addFile("Test.java"); display(null,src); System.out.println("----------------------------"); combainPack.getFile(1).delete(); display(null, src); System.out.println("---------------------------"); combainPack.delete(); display(null, src); } private static void display(String prefix,IFile file) { if (prefix == null) { prefix = ""; } System.out.println(prefix+file.getName()); if(file instanceof PackageFile){ PackageFile pack = (PackageFile)file; for(int i = 0;i<pack.getFiles().size(); i++){ if(file.getFile(i)!=null){ display(prefix+"--",file.getFile(i)); } } } } }
在上述的测试类中,添加了一个展示文件目录的方法,然后通过创建一个src文件夹,然后在src文件夹下又创建了一个文件夹,最后展示文件目录结构,后面分别删除一个java文件和一个package,测试结果如下所示:
最后我们来简单的总结一下组合模式,组合模式将对象组合成树形结构来展示“部分整体”的层次结构,并且使用户在处理单个对象和组合对象时具有一致的行为,另外,我们一般优先采用透明的方式来使用组合模式,虽然牺牲了安全性,但是对于单个对象和组合对象具有一致性,当然,也要按照实际情况来说,不能一味的使用透明策略,有时也是需要使用安全策略的。
OK,组合模式暂时就到这里了,我们下期再见;