树形结构在软件中随处可见,例如操做系统中的目录结构、应用软件中的菜单、办公系统中的公司组织结构等等,如何运用面向对象的方式来处理这种树形结构是组合模式须要解决的问题,组合模式经过一种巧妙的设计方案使得用户能够一致性地处理整个树形结构或者树形结构的一部分,也能够一致性地处理树形结构中的叶子节点(不包含子节点的节点)和容器节点(包含子节点的节点)。下面将学习这种用于处理树形结构的组合模式。html
Sunny软件公司欲开发一个杀毒(AntiVirus)软件,该软件既能够对某个文件夹(Folder)杀毒,也能够对某个指定的文件(File)进行杀毒。该杀毒软件还能够根据各种文件的特色,为不一样类型的文件提供不一样的杀毒方式,例如图像文件(ImageFile)和文本文件(TextFile)的杀毒方式就有所差别。现须要提供该杀毒软件的总体框架设计方案。java |
在介绍Sunny公司开发人员提出的初始解决方案以前,咱们先来分析一下操做系统中的文件目录结构,例如在Windows操做系统中,存在如图11-1所示目录结构:算法
图11-1 Windows目录结构docker
图11-1能够简化为如图11-2所示树形目录结构:编程
图11-2 树形目录结构示意图设计模式
咱们能够看出,在图11-2中包含文件(灰色节点)和文件夹(白色节点)两类不一样的元素,其中在文件夹中能够包含文件,还能够继续包含子文件夹,可是在文件中不能再包含子文件或者子文件夹。在此,咱们能够称文件夹为容器(Container),而不一样类型的各类文件是其成员,也称为叶子(Leaf),一个文件夹也能够做为另外一个更大的文件夹的成员。若是咱们如今要对某一个文件夹进行操做,如查找文件,那么须要对指定的文件夹进行遍历,若是存在子文件夹则打开其子文件夹继续遍历,若是是文件则判断以后返回查找结果。安全
Sunny软件公司的开发人员经过分析,决定使用面向对象的方式来实现对文件和文件夹的操做,定义了以下图像文件类ImageFile、文本文件类TextFile和文件夹类Folder:框架
//为了突出核心框架代码,咱们对杀毒过程的实现进行了大量简化 import java.util.*; //图像文件类 class ImageFile { private String name; public ImageFile(String name) { this.name = name; } public void killVirus() { //简化代码,模拟杀毒 System.out.println("----对图像文件'" + name + "'进行杀毒"); } } //文本文件类 class TextFile { private String name; public TextFile(String name) { this.name = name; } public void killVirus() { //简化代码,模拟杀毒 System.out.println("----对文本文件'" + name + "'进行杀毒"); } } //文件夹类 class Folder { private String name; //定义集合folderList,用于存储Folder类型的成员 private ArrayList<Folder> folderList = new ArrayList<Folder>(); //定义集合imageList,用于存储ImageFile类型的成员 private ArrayList<ImageFile> imageList = new ArrayList<ImageFile>(); //定义集合textList,用于存储TextFile类型的成员 private ArrayList<TextFile> textList = new ArrayList<TextFile>(); public Folder(String name) { this.name = name; } //增长新的Folder类型的成员 public void addFolder(Folder f) { folderList.add(f); } //增长新的ImageFile类型的成员 public void addImageFile(ImageFile image) { imageList.add(image); } //增长新的TextFile类型的成员 public void addTextFile(TextFile text) { textList.add(text); } //需提供三个不一样的方法removeFolder()、removeImageFile()和removeTextFile()来删除成员,代码省略 //需提供三个不一样的方法getChildFolder(int i)、getChildImageFile(int i)和getChildTextFile(int i)来获取成员,代码省略 public void killVirus() { System.out.println("****对文件夹'" + name + "'进行杀毒"); //模拟杀毒 //若是是Folder类型的成员,递归调用Folder的killVirus()方法 for(Object obj : folderList) { ((Folder)obj).killVirus(); } //若是是ImageFile类型的成员,调用ImageFile的killVirus()方法 for(Object obj : imageList) { ((ImageFile)obj).killVirus(); } //若是是TextFile类型的成员,调用TextFile的killVirus()方法 for(Object obj : textList) { ((TextFile)obj).killVirus(); } } }
编写以下客户端测试代码进行测试:ide
class Client { public static void main(String args[]) { Folder folder1,folder2,folder3; folder1 = new Folder("Sunny的资料"); folder2 = new Folder("图像文件"); folder3 = new Folder("文本文件"); ImageFile image1,image2; image1 = new ImageFile("小龙女.jpg"); image2 = new ImageFile("张无忌.gif"); TextFile text1,text2; text1 = new TextFile("九阴真经.txt"); text2 = new TextFile("葵花宝典.doc"); folder2.addImageFile(image1); folder2.addImageFile(image2); folder3.addTextFile(text1); folder3.addTextFile(text2); folder1.addFolder(folder2); folder1.addFolder(folder3); folder1.killVirus(); } }
编译并运行程序,输出结果以下:学习
****对文件夹'Sunny的资料'进行杀毒 ****对文件夹'图像文件'进行杀毒 ----对图像文件'小龙女.jpg'进行杀毒 ----对图像文件'张无忌.gif'进行杀毒 ****对文件夹'文本文件'进行杀毒 ----对文本文件'九阴真经.txt'进行杀毒 ----对文本文件'葵花宝典.doc'进行杀毒 |
Sunny公司开发人员“成功”实现了杀毒软件的框架设计,但经过仔细分析,发现该设计方案存在以下问题:
(1) 文件夹类Folder的设计和实现都很是复杂,须要定义多个集合存储不一样类型的成员,并且须要针对不一样的成员提供增长、删除和获取等管理和访问成员的方法,存在大量的冗余代码,系统维护较为困难;
(2) 因为系统没有提供抽象层,客户端代码必须有区别地对待充当容器的文件夹Folder和充当叶子的ImageFile和TextFile,没法统一对它们进行处理;
(3) 系统的灵活性和可扩展性差,若是须要增长新的类型的叶子和容器都须要对原有代码进行修改,例如若是须要在系统中增长一种新类型的视频文件VideoFile,则必须修改Folder类的源代码,不然没法在文件夹中添加视频文件。
面对以上问题,Sunny软件公司的开发人员该如何来解决?这就须要用到本章将要介绍的组合模式,组合模式为处理树形结构提供了一种较为完美的解决方案,它描述了如何将容器和叶子进行递归组合,使得用户在使用时无须对它们进行区分,能够一致地对待容器和叶子。
对于树形结构,当容器对象(如文件夹)的某一个方法被调用时,将遍历整个树形结构,寻找也包含这个方法的成员对象(能够是容器对象,也能够是叶子对象)并调用执行,牵一而动百,其中使用了递归调用的机制来对整个结构进行处理。因为容器对象和叶子对象在功能上的区别,在使用这些对象的代码中必须有区别地对待容器对象和叶子对象,而实际上大多数状况下咱们但愿一致地处理它们,由于对于这些对象的区别对待将会使得程序很是复杂。组合模式为解决此类问题而诞生,它可让叶子对象和容器对象的使用具备一致性。
组合模式定义以下:
组合模式(Composite Pattern):组合多个对象造成树形结构以表示具备“总体—部分”关系的层次结构。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具备一致性,组合模式又能够称为“总体—部分”(Part-Whole)模式,它是一种对象结构型模式。 |
在组合模式中引入了抽象构件类Component,它是全部容器类和叶子类的公共父类,客户端针对Component进行编程。组合模式结构如图11-3所示:
图11-3 组合模式结构图
在组合模式结构图中包含以下几个角色:
● Component(抽象构件):它能够是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中能够包含全部子类共有行为的声明和实现。在抽象构件中定义了访问及管理它的子构件的方法,如增长子构件、删除子构件、获取子构件等。
● Leaf(叶子构件):它在组合结构中表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,能够经过异常等方式进行处理。
● Composite(容器构件):它在组合结构中表示容器节点对象,容器节点包含子节点,其子节点能够是叶子节点,也能够是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中能够递归调用其子节点的业务方法。
组合模式的关键是定义了一个抽象构件类,它既能够表明叶子,又能够表明容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子仍是容器,能够对其进行统一处理。同时容器对象与抽象构件类之间还创建一个聚合关联关系,在容器对象中既能够包含叶子,也能够包含容器,以此实现递归组合,造成一个树形结构。
若是不使用组合模式,客户端代码将过多地依赖于容器对象复杂的内部实现结构,容器对象内部实现结构的变化将引发客户代码的频繁变化,带来了代码维护复杂、可扩展性差等弊端。组合模式的引入将在必定程度上解决这些问题。
下面经过简单的示例代码来分析组合模式的各个角色的用途和实现。对于组合模式中的抽象构件角色,其典型代码以下所示:
abstract class Component { public abstract void add(Component c); //增长成员 public abstract void remove(Component c); //删除成员 public abstract Component getChild(int i); //获取成员 public abstract void operation(); //业务方法 }
通常将抽象构件类设计为接口或抽象类,将全部子类共有方法的声明和实现放在抽象构件类中。对于客户端而言,将针对抽象构件编程,而无须关心其具体子类是容器构件仍是叶子构件。
若是继承抽象构件的是叶子构件,则其典型代码以下所示:
class Leaf extends Component { public void add(Component c) { //异常处理或错误提示 } public void remove(Component c) { //异常处理或错误提示 } public Component getChild(int i) { //异常处理或错误提示 return null; } public void operation() { //叶子构件具体业务方法的实现 } }
做为抽象构件类的子类,在叶子构件中须要实如今抽象构件类中声明的全部方法,包括业务方法以及管理和访问子构件的方法,可是叶子构件不能再包含子构件,所以在叶子构件中实现子构件管理和访问方法时须要提供异常处理或错误提示。固然,这无疑会给叶子构件的实现带来麻烦。
若是继承抽象构件的是容器构件,则其典型代码以下所示:
class Composite extends Component { private ArrayList<Component> list = new ArrayList<Component>(); public void add(Component c) { list.add(c); } public void remove(Component c) { list.remove(c); } public Component getChild(int i) { return (Component)list.get(i); } public void operation() { //容器构件具体业务方法的实现 //递归调用成员构件的业务方法 for(Object obj:list) { ((Component)obj).operation(); } } }
在容器构件中实现了在抽象构件中声明的全部方法,既包括业务方法,也包括用于访问和管理成员子构件的方法,如add()、remove()和getChild()等方法。须要注意的是在实现具体业务方法时,因为容器构件充当的是容器角色,包含成员构件,所以它将调用其成员构件的业务方法。在组合模式结构中,因为容器构件中仍然能够包含容器构件,所以在对容器构件进行处理时须要使用递归算法,即在容器构件的operation()方法中递归调用其成员构件的operation()方法。
|
import java.util.*; //抽象文件类:抽象构件 abstract class AbstractFile { public abstract void add(AbstractFile file); public abstract void remove(AbstractFile file); public abstract AbstractFile getChild(int i); public abstract void killVirus(); } //图像文件类:叶子构件 class ImageFile extends AbstractFile { private String name; public ImageFile(String name) { this.name = name; } public void add(AbstractFile file) { System.out.println("对不起,不支持该方法!"); } public void remove(AbstractFile file) { System.out.println("对不起,不支持该方法!"); } public AbstractFile getChild(int i) { System.out.println("对不起,不支持该方法!"); return null; } public void killVirus() { //模拟杀毒 System.out.println("----对图像文件'" + name + "'进行杀毒"); } } //文本文件类:叶子构件 class TextFile extends AbstractFile { private String name; public TextFile(String name) { this.name = name; } public void add(AbstractFile file) { System.out.println("对不起,不支持该方法!"); } public void remove(AbstractFile file) { System.out.println("对不起,不支持该方法!"); } public AbstractFile getChild(int i) { System.out.println("对不起,不支持该方法!"); return null; } public void killVirus() { //模拟杀毒 System.out.println("----对文本文件'" + name + "'进行杀毒"); } } //视频文件类:叶子构件 class VideoFile extends AbstractFile { private String name; public VideoFile(String name) { this.name = name; } public void add(AbstractFile file) { System.out.println("对不起,不支持该方法!"); } public void remove(AbstractFile file) { System.out.println("对不起,不支持该方法!"); } public AbstractFile getChild(int i) { System.out.println("对不起,不支持该方法!"); return null; } public void killVirus() { //模拟杀毒 System.out.println("----对视频文件'" + name + "'进行杀毒"); } } //文件夹类:容器构件 class Folder extends AbstractFile { //定义集合fileList,用于存储AbstractFile类型的成员 private ArrayList<AbstractFile> fileList=new ArrayList<AbstractFile>(); private String name; public Folder(String name) { this.name = name; } public void add(AbstractFile file) { fileList.add(file); } public void remove(AbstractFile file) { fileList.remove(file); } public AbstractFile getChild(int i) { return (AbstractFile)fileList.get(i); } public void killVirus() { System.out.println("****对文件夹'" + name + "'进行杀毒"); //模拟杀毒 //递归调用成员构件的killVirus()方法 for(Object obj : fileList) { ((AbstractFile)obj).killVirus(); } } }
编写以下客户端测试代码:
class Client { public static void main(String args[]) { //针对抽象构件编程 AbstractFile file1,file2,file3,file4,file5,folder1,folder2,folder3,folder4; folder1 = new Folder("Sunny的资料"); folder2 = new Folder("图像文件"); folder3 = new Folder("文本文件"); folder4 = new Folder("视频文件"); file1 = new ImageFile("小龙女.jpg"); file2 = new ImageFile("张无忌.gif"); file3 = new TextFile("九阴真经.txt"); file4 = new TextFile("葵花宝典.doc"); file5 = new VideoFile("笑傲江湖.rmvb"); folder2.add(file1); folder2.add(file2); folder3.add(file3); folder3.add(file4); folder4.add(file5); folder1.add(folder2); folder1.add(folder3); folder1.add(folder4); //从“Sunny的资料”节点开始进行杀毒操做 folder1.killVirus(); } }
编译并运行程序,输出结果以下:
****对文件夹'Sunny的资料'进行杀毒 ****对文件夹'图像文件'进行杀毒 ----对图像文件'小龙女.jpg'进行杀毒 ----对图像文件'张无忌.gif'进行杀毒 ****对文件夹'文本文件'进行杀毒 ----对文本文件'九阴真经.txt'进行杀毒 ----对文本文件'葵花宝典.doc'进行杀毒 ****对文件夹'视频文件'进行杀毒 ----对视频文件'笑傲江湖.rmvb'进行杀毒 |
因为在本实例中使用了组合模式,在抽象构件类中声明了全部方法,包括用于管理和访问子构件的方法,如add()方法和remove()方法等,所以在ImageFile等叶子构件类中实现这些方法时必须进行相应的异常处理或错误提示。在容器构件类Folder的killVirus()方法中将递归调用其成员对象的killVirus()方法,从而实现对整个树形结构的遍历。
若是须要更换操做节点,例如只需对文件夹“文本文件”进行杀毒,客户端代码只需修改一行便可,将代码:
folder1.killVirus(); |
改成:
folder3.killVirus(); |
输出结果以下:
****对文件夹'文本文件'进行杀毒 ----对文本文件'九阴真经.txt'进行杀毒 ----对文本文件'葵花宝典.doc'进行杀毒 |
在具体实现时,咱们能够建立图形化界面让用户选择所需操做的根节点,无须修改源代码,符合“开闭原则”,客户端无须关心节点的层次结构,能够对所选节点进行统一处理,提升系统的灵活性。
经过引入组合模式,Sunny公司设计的杀毒软件具备良好的可扩展性,在增长新的文件类型时,无须修改现有类库代码,只需增长一个新的文件类做为AbstractFile类的子类便可,可是因为在AbstractFile中声明了大量用于管理和访问成员构件的方法,例如add()、remove()等方法,咱们不得不在新增的文件类中实现这些方法,提供对应的错误提示和异常处理。为了简化代码,咱们有如下两个解决方案:
解决方案一:将叶子构件的add()、remove()等方法的实现代码移至AbstractFile类中,由AbstractFile提供统一的默认实现,代码以下所示:
//提供默认实现的抽象构件类 abstract class AbstractFile { public void add(AbstractFile file) { System.out.println("对不起,不支持该方法!"); } public void remove(AbstractFile file) { System.out.println("对不起,不支持该方法!"); } public AbstractFile getChild(int i) { System.out.println("对不起,不支持该方法!"); return null; } public abstract void killVirus(); }
若是客户端代码针对抽象类AbstractFile编程,在调用文件对象的这些方法时将出现错误提示。若是不但愿出现任何错误提示,咱们能够在客户端定义文件对象时不使用抽象层,而直接使用具体叶子构件自己,客户端代码片断以下所示:
class Client { public static void main(String args[]) { //不能透明处理叶子构件 ImageFile file1,file2; TextFile file3,file4; VideoFile file5; AbstractFile folder1,folder2,folder3,folder4; //其余代码省略 } }
这样就产生了一种不透明的使用方式,即在客户端不能所有针对抽象构件类编程,须要使用具体叶子构件类型来定义叶子对象。
解决方案二:除此以外,还有一种解决方法是在抽象构件AbstractFile中不声明任何用于访问和管理成员构件的方法,代码以下所示:
abstract class AbstractFile { public abstract void killVirus(); }
此时,因为在AbstractFile中没有声明add()、remove()等访问和管理成员的方法,其叶子构件子类无须提供实现;并且不管客户端如何定义叶子构件对象都没法调用到这些方法,不须要作任何错误和异常处理,容器构件再根据须要增长访问和管理成员的方法,但这时候也存在一个问题:客户端不得不使用容器类自己来声明容器构件对象,不然没法访问其中新增的add()、remove()等方法,若是客户端一致性地对待叶子和容器,将会致使容器构件的新增对客户端不可见,客户端代码对于容器构件没法再使用抽象构件来定义,客户端代码片断以下所示:
class Client { public static void main(String args[]) { AbstractFile file1,file2,file3,file4,file5; Folder folder1,folder2,folder3,folder4; //不能透明处理容器构件 //其余代码省略 } }
在使用组合模式时,根据抽象构件类的定义形式,咱们可将组合模式分为透明组合模式和安全组合模式两种形式:
(1) 透明组合模式
透明组合模式中,抽象构件Component中声明了全部用于管理成员对象的方法,包括add()、remove()以及getChild()等方法,这样作的好处是确保全部的构件类都有相同的接口。在客户端看来,叶子对象与容器对象所提供的方法是一致的,客户端能够相同地对待全部的对象。透明组合模式也是组合模式的标准形式,虽然上面的解决方案一在客户端能够有不透明的实现方法,可是因为在抽象构件中包含add()、remove()等方法,所以它仍是透明组合模式,透明组合模式的完整结构如图11-6所示:
图11-6 透明组合模式结构图
透明组合模式的缺点是不够安全,由于叶子对象和容器对象在本质上是有区别的。叶子对象不可能有下一个层次的对象,即不可能包含成员对象,所以为其提供add()、remove()以及getChild()等方法是没有意义的,这在编译阶段不会出错,但在运行阶段若是调用这些方法可能会出错(若是没有提供相应的错误处理代码)。
(2) 安全组合模式
安全组合模式中,在抽象构件Component中没有声明任何用于管理成员对象的方法,而是在Composite类中声明并实现这些方法。这种作法是安全的,由于根本不向叶子对象提供这些管理成员对象的方法,对于叶子对象,客户端不可能调用到这些方法,这就是解决方案二所采用的实现方式。安全组合模式的结构如图11-7所示:
图11-7 安全组合模式结构图
安全组合模式的缺点是不够透明,由于叶子构件和容器构件具备不一样的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,所以客户端不能彻底针对抽象编程,必须有区别地对待叶子构件和容器构件。在实际应用中,安全组合模式的使用频率也很是高,在Java AWT中使用的组合模式就是安全组合模式。
在学习和使用组合模式时,Sunny软件公司开发人员发现树形结构其实随处可见,例如Sunny公司的组织结构就是“一棵标准的树”,如图11-8所示:
图11-8 Sunny公司组织结构图
在Sunny软件公司的内部办公系统Sunny OA系统中,有一个与公司组织结构对应的树形菜单,行政人员能够给各级单位下发通知,这些单位能够是总公司的一个部门,也能够是一个分公司,还能够是分公司的一个部门。用户只须要选择一个根节点便可实现通知的下发操做,而无须关心具体的实现细节。这不正是组合模式的“特长”吗?因而Sunny公司开发人员绘制了如图11-9所示结构图:
图11-9 Sunny公司组织结构组合模式示意图
在图11-9中,“单位”充当了抽象构件角色,“公司”充当了容器构件角色,“研发部”、“财务部”和“人力资源部”充当了叶子构件角色。
|
组合模式使用面向对象的思想来实现树形结构的构建与处理,描述了如何将容器对象和叶子对象进行递归组合,实现简单,灵活性好。因为在软件开发中存在大量的树形结构,所以组合模式是一种使用频率较高的结构型设计模式,Java SE中的AWT和Swing包的设计就基于组合模式,在这些界面包中为用户提供了大量的容器构件(如Container)和成员构件(如Checkbox、Button和TextComponent等),其结构如图11-10所示:
图11-10 AWT组合模式结构示意图
在图11-10中,Component类是抽象构件,Checkbox、Button和TextComponent是叶子构件,而Container是容器构件,在AWT中包含的叶子构件还有不少,由于篇幅限制没有在图中一一列出。在一个容器构件中能够包含叶子构件,也能够继续包含容器构件,这些叶子构件和容器构件一块儿组成了复杂的GUI界面。
除此之外,在XML解析、组织结构树处理、文件系统设计等领域,组合模式都获得了普遍应用。
1. 主要优势
组合模式的主要优势以下:
(1) 组合模式能够清楚地定义分层次的复杂对象,表示对象的所有或部分层次,它让客户端忽略了层次的差别,方便对整个层次结构进行控制。
(2) 客户端能够一致地使用一个组合结构或其中单个对象,没必要关心处理的是单个对象仍是整个组合结构,简化了客户端代码。
(3) 在组合模式中增长新的容器构件和叶子构件都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
(4) 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,经过叶子对象和容器对象的递归组合,能够造成复杂的树形结构,但对树形结构的控制却很是简单。
2. 主要缺点
组合模式的主要缺点以下:
在增长新构件时很难对容器中的构件类型进行限制。有时候咱们但愿一个容器中只能有某些特定类型的对象,例如在某个文件夹中只能包含文本文件,使用组合模式时,不能依赖类型系统来施加这些约束,由于它们都来自于相同的抽象层,在这种状况下,必须经过在运行时进行类型检查来实现,这个实现过程较为复杂。
3. 适用场景
在如下状况下能够考虑使用组合模式:
(1) 在具备总体和部分的层次结构中,但愿经过一种方式忽略总体与部分的差别,客户端能够一致地对待它们。
(2) 在一个使用面向对象语言开发的系统中须要处理一个树形结构。
(3) 在一个系统中可以分离出叶子对象和容器对象,并且它们的类型不固定,须要增长一些新的类型。
|
原文地址:https://www.cnblogs.com/lfxiao/p/6816026.html