场景引入:树形结构在软件中随处可见,例如操作系统中的目录结构、应用软件中的菜单、办公系统中的公司组织结构等等,如何运用面向对象的方式来处理这种树形结构是组合模式需要解决的问题,组合模式通过一种巧妙的设计方案使得用户可以一致性地处理整个树形结构或者树形结构的一部分,它描述了如何将容器对象和叶子对象进行递归组合,使得用户在使用时无须对其进行区分,可以一致地对待容器对象和叶子对象,这就是组成模式的模式动机
定义:组合多个对象形成树形结构以表示具有部分-整体关系的层次结构。组合模型让客户端可以统一对待单个对象和组合对象
由上图可知,组合模式包含以下3个角色
(1)Component(抽象构件):它可以是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。
(2)Leaf(叶子构件):它在组合结构中表示叶子节点对象,它实现了在抽象构件中定义的行为
(3)Composite(容器构件):它在组合结构中表示容器节点对象,容器节点包含子节点,其子节点可以是容器或叶子节点
组合模式应用实例
1. 实例说明
某软件公司欲开发一个杀毒(AntiVirus)软件,该软件既可以对某个文件夹(Folder)杀毒,也可以对某个指定的文件(File)进行杀毒。该杀毒软件还可以根据各类文件的特点,为不同类型的文件提供不同的杀毒方式,例如图像文件(ImageFile)和文本文件(TextFile)的杀毒方式就有所差异。现需要提供该杀毒软件的整体框架设计方案。
2. 实例类图
3. 示例代码
1 package designpatterns.composite; 2 3 public abstract class AbstractFile { 4 public abstract void add(AbstractFile file); 5 public abstract void remove(AbstractFile file); 6 public abstract AbstractFile getChild(int i); 7 public abstract void killVirus(); 8 }
1 package designpatterns.composite; 2 3 public class ImageFile extends AbstractFile { 4 private String name; 5 6 public ImageFile(String name) { 7 super(); 8 this.name = name; 9 } 10 11 @Override 12 public void add(AbstractFile file) { 13 System.out.println("对不起,不支持该方法!"); 14 } 15 16 @Override 17 public void remove(AbstractFile file) { 18 System.out.println("对不起,不支持该方法!"); 19 20 } 21 22 @Override 23 public AbstractFile getChild(int i) { 24 System.out.println("对不起,不支持该方法!"); 25 return null; 26 } 27 28 @Override 29 public void killVirus() { 30 System.out.println("----对图像文件'" + name + "'进行杀毒"); 31 } 32 }
1 package designpatterns.composite; 2 3 public class TextFile extends AbstractFile { 4 private String name; 5 6 public TextFile(String name) { 7 super(); 8 this.name = name; 9 } 10 11 @Override 12 public void add(AbstractFile file) { 13 System.out.println("对不起,不支持该方法!"); 14 } 15 16 @Override 17 public void remove(AbstractFile file) { 18 System.out.println("对不起,不支持该方法!"); 19 } 20 21 @Override 22 public AbstractFile getChild(int i) { 23 System.out.println("对不起,不支持该方法!"); 24 return null; 25 } 26 27 @Override 28 public void killVirus() { 29 System.out.println("----对文本文件'" + name + "'进行杀毒"); 30 } 31 32 }
1 package designpatterns.composite; 2 3 public class VideoFile extends AbstractFile { 4 private String name; 5 6 public VideoFile(String name) { 7 super(); 8 this.name = name; 9 } 10 11 @Override 12 public void add(AbstractFile file) { 13 System.out.println("对不起,不支持该方法!"); 14 } 15 16 @Override 17 public void remove(AbstractFile file) { 18 System.out.println("对不起,不支持该方法!"); 19 } 20 21 @Override 22 public AbstractFile getChild(int i) { 23 System.out.println("对不起,不支持该方法!"); 24 return null; 25 } 26 27 @Override 28 public void killVirus() { 29 System.out.println("----对视频文件'" + name + "'进行杀毒"); 30 } 31 32 }
1 package designpatterns.composite; 2 3 import java.util.ArrayList; 4 5 public class Folder extends AbstractFile { 6 //定义集合fileList,用于存储AbstractFile类型的成员 7 private ArrayList<AbstractFile> fileList = new ArrayList<AbstractFile>(); 8 private String name; 9 10 public Folder(String name) { 11 super(); 12 this.name = name; 13 } 14 15 @Override 16 public void add(AbstractFile file) { 17 fileList.add(file); 18 } 19 20 @Override 21 public void remove(AbstractFile file) { 22 fileList.remove(file); 23 } 24 25 @Override 26 public AbstractFile getChild(int i) { 27 return fileList.get(i); 28 } 29 30 @Override 31 public void killVirus() { 32 System.out.println("****对文件夹'" + name +"'进行杀毒"); 33 34 //递归调用成员构件的killVirus()方法 35 for(Object obj : fileList){ 36 ((AbstractFile)obj).killVirus(); 37 } 38 } 39 }
1 package designpatterns.composite; 2 3 public class Client { 4 5 public static void main(String[] args) { 6 //针对抽象构件编程 7 AbstractFile file1,file2,file3,file4,file5,folder1,folder2,folder3,folder4; 8 9 folder1 = new Folder("Sunny的资料"); 10 folder2 = new Folder("图像文件"); 11 folder3 = new Folder("文本文件"); 12 folder4 = new Folder("图像文件"); 13 14 file1 = new ImageFile("小龙女.jpg"); 15 file2 = new ImageFile("黄蓉.jpg"); 16 file3 = new TextFile("九阴真经.txt"); 17 file4 = new TextFile("御女真经.doc"); 18 file5 = new VideoFile("神雕侠侣.mp4"); 19 20 folder2.add(file1); 21 folder2.add(file2); 22 folder3.add(file3); 23 folder3.add(file4); 24 folder4.add(file5); 25 26 folder1.add(folder2); 27 folder1.add(folder3); 28 folder1.add(folder4); 29 30 //从“Sunny的资料”节点开始进行杀毒 31 folder1.killVirus(); 32 } 33 }
4. 结果及分析
如果需要更换节点,只需在客户端修改一行代码即可:
folder2.killVirus();
结果:
在具体实现时可以创建图形化界面来让用户选择所需操作的根节点,无须修改源代码,符合开闭原则,客户端无须关心节点的层次结构,可以对所选节点进行统一处理,提高系统的灵活性
透明组合模式和安全组合模式
组合模式根据抽象构件类的定义形式可以分为透明组合模式和安全组合模式
1. 透明组合模式
在透明模式中,抽象构件Component中声明了所有用于管理成员对象的方法,包括add()、remove()以及getChild()等方法,这样做的好处是确保所有的构件类都有相同的接口。
透明模式的缺点是不够安全,因为叶子对象和容器对象本质上是有区别的。叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此提供add()、remove()以及getChild()等方法是没有意义的,这在编译阶段不会出错,但是在运行阶段如果调用这些方法可能会出错
2. 安全组合模式
在安全组合模式中,抽象构件Component中没有声明任何用于管理成员对象的方法,而是在Composite类中声明并实现这些方法。这种做法是安全的,因为根本不向叶子对象提供这些管理成员对象的方法,对于叶子对象,客户端不可能调用得到这些方法
安全模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件中定义,因此客户端不能完全针对抽象编程,必须有区别的对待叶子构件和容器构件。在实际应用中,安全模式的使用频率也非常高
组合模式的优/缺点与适用环境
1. 组合模式优点
(1)可以清楚的定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制
(2)客户端可以一致的使用一个组合对象而不必关心处理的是单个对象还是整个组合结构
(3)增加新的容器和叶子构件都很方便,无须对现有的库类进行修改,符合开闭原则
(4)为树形结构的面相对象实现提供了一个灵活的解决方案
2. 组合模式的缺点
(1)在增加新构件的时候很难对容器中的构件类型进行限制
3. 组合模式适用环境
(1)在具有整体和部分的层次结构中希望通过一种方式来忽略整体与部分的差异
(2)需要处理一个树形结构
(3)在一个系统中能够分离出叶子对象和容器对象,而且他们的类型不固定,需要增加一些新的类型