设计模式之组合模式
组合模式--导读
我们知道电脑中的文件,既有各种各样的文件,同时还有各种文件夹来放置这些文件,文件夹中同时还存在着文件夹,就像下面提供的那幅图一样:
从图中我们可以看出,电脑中的文件结构是一个树形结构,各种个样的文件就是相当于树的叶子,而各种文件夹就相当于树的分叉点,也可以叫做容器节点,当我们需要对电脑进行文件查找时,需要一层一层的进行遍历,对于叶子节点他里面并不能存放文件所以我们不需要也不能查找里面的内容,但是对于容器节点就要对其中的内容进行遍历了,这就需要我们对叶子节点和容器节点进行区别对待了。但是如果我不是进行查找文件呢,我是进行文件的查看,那么不同的具体文件有不同的播放方式,那么我又要区分不同文件之间的区别了,这样无疑会导致编码更加复杂化。于是我们想到了组合模式。
组合模式--定义
组合模式(Composite Pattern),又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
这种模式创建了一个包含自己对象组的类。该类提供了修改相同对象组的方式。
组合模式--结构
组合模式主要包含如下几个角色:
1.Component :组合中的对象声明接口,在适当的情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理Component子部件。
2.Leaf:叶子对象。叶子结点没有子结点。
3.Composite:容器对象,定义有枝节点行为,用来存储子部件,在Component接口中实现与子部件有关操作,如增加(add)和删除(remove)等。
从模式结构中我们看出了叶子节点和容器对象都实现Component接口,这也是能够将叶子对象和容器对象一致对待的关键所在。
组合模式--代码实现
下面我用杀毒软件对文件系统杀毒为例来进行代码实现:
ComponentFile.java
package Composite_Pattern; /** * 定义所有文件的抽象类,所有的文件都要继承该类 * @author xyxy001 * */ public abstract class ComponentFile { public String name; public ComponentFile(String name){ this.name=name; } public abstract void addFile(ComponentFile file); public abstract void deleteFile(ComponentFile file); public abstract ComponentFile getFile(String name); public abstract void killVirus(); }
Folder.java
package Composite_Pattern; import java.util.ArrayList; import java.util.List; //模拟文件夹 public class Folder extends ComponentFile { //定义一个用于存放文件的集合 private List<ComponentFile> fileList=new ArrayList<ComponentFile>(); public Folder(String name){ super(name); } //向文件夹中添加文件 public void addFile(ComponentFile file) { fileList.add(file); } @Override public void deleteFile(ComponentFile file) { fileList.remove(file); } //递归调用来查文件 public ComponentFile getFile(String name) { ComponentFile wantedFile=null; for(ComponentFile temp:fileList){ if((temp.name).equals(name)) { wantedFile=temp; }else if(wantedFile==null){ wantedFile=temp.getFile(name); } } if(wantedFile==null){ System.out.println("在"+this.name+"没有找到该文件"); return null; }else{ return wantedFile; } } @Override public void killVirus() { System.out.println("正在对文件夹:***"+name+"进行病毒查杀"); //递归调用子类的查杀病毒的方法 for(ComponentFile temp:fileList) temp.killVirus(); } }
ImageFile.java
package Composite_Pattern; //模拟图片文档 public class ImageFile extends ComponentFile { public ImageFile(String name) { super(name); // TODO Auto-generated constructor stub } @Override public void addFile(ComponentFile file) { System.out.println("该文件为图片文件,不支持该方法"); } @Override public void deleteFile(ComponentFile file) { System.out.println("该文件为图片文件,不支持该方法"); } @Override public ComponentFile getFile(String name) { System.out.println("该文件为图片文件,不支持该方法"); return null; } @Override public void killVirus() { System.out.println("正在为图片文件:"+name+"进行病毒查杀"); } }
MusicFile.java
package Composite_Pattern; //模拟音乐文档 public class MusicFile extends ComponentFile { public MusicFile(String name) { super(name); // TODO Auto-generated constructor stub } @Override public void addFile(ComponentFile file) { System.out.println("该文件为音乐文件,不支持该方法"); } @Override public void deleteFile(ComponentFile file) { System.out.println("该文件为音乐文件,不支持该方法"); } @Override public ComponentFile getFile(String name) { System.out.println("该文件为音乐文件,不支持该方法"); return null; } @Override public void killVirus() { System.out.println("正在为音乐文件:"+name+"进行病毒查杀"); } }
TextFile.java
package Composite_Pattern; //模拟文本文档 public class TextFile extends ComponentFile { public TextFile(String name) { super(name); // TODO Auto-generated constructor stub } @Override public void addFile(ComponentFile file) { System.out.println("该文件为文本文件,不支持该方法"); } @Override public void deleteFile(ComponentFile file) { System.out.println("该文件为文本文件,不支持该方法"); } @Override public ComponentFile getFile(String name) { System.out.println("该文件为文本文件,不支持该方法"); return null; } @Override public void killVirus() { System.out.println("正在为文本文件:"+name+"进行病毒查杀"); } }
VideoFile.java
package Composite_Pattern; //模拟视频文档 public class VideoFile extends ComponentFile { public VideoFile(String name) { super(name); // TODO Auto-generated constructor stub } @Override public void addFile(ComponentFile file) { System.out.println("该文件为视屏文件,不支持该方法"); } @Override public void deleteFile(ComponentFile file) { // TODO Auto-generated method stub System.out.println("该文件为视屏文件,不支持该方法"); } @Override public ComponentFile getFile(String name) { System.out.println("该文件为视屏文件,不支持该方法"); return null; } @Override public void killVirus() { System.out.println("正在为视屏文件:"+name+"进行病毒查杀"); } }
Client.java
package Composite_Pattern; public class Client { public static void main(String[] args) { /** * 我们先建立一个这样的文件系统 * 娱乐 * * 音乐 视屏 新建文件夹 狂神.txt * ... ..... ..... * */ Folder folder1=new Folder("娱乐"); Folder folder2=new Folder("音乐"); Folder folder3=new Folder("视屏"); Folder folder4=new Folder("新建文件夹"); MusicFile m1=new MusicFile("绿色.mp3"); MusicFile m2=new MusicFile("出山.mp3"); MusicFile m3=new MusicFile("可不可以.mp3"); MusicFile m4=new MusicFile("年少有为.mp3"); folder2.addFile(m1); folder2.addFile(m2); folder2.addFile(m3); folder2.addFile(m4); folder3.addFile(new VideoFile("战狼.mp4")); folder3.addFile(new VideoFile("流浪地球.mp4")); folder4.addFile(new ImageFile("苍老师.jpg")); folder4.addFile(new TextFile("斗破苍穹.txt")); folder1.addFile(folder4); folder1.addFile(folder2); folder1.addFile(folder3); folder1.addFile(new TextFile("狂神.txt")); //娱乐文件进行杀毒 //folder1.killVirus(); ComponentFile file1=folder1.getFile("狂神.txt"); if(file1!=null) System.out.println("想要文件的名字"+file1.name); } }
运行效果如下:
由于我们所有的文件都是继承自ComonentFile抽象类,当客户端换过节点进行病毒查杀是,仅需直接换用节点即可,而不用考虑不同文件的特殊性,可以对其进行一致的 处理,当系统需要增加一些额外的文件时,不需要对源码进行更改只需要增加一个继承类便可,符合开闭原则
组合模式--优缺点
优点
1、可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,使得增加新构件也更容易。
2、客户端调用简单,客户端可以一致的使用组合结构或其中单个对象。
3、定义了包含叶子对象和容器对象的类层次结构,叶子对象可以被组合成更复杂的容器对象,而这个容器对象又可以被组合,这样不断递归下去,可以形成复杂的树形结构。
4、更容易在组合体内加入对象构件,客户端不必因为加入了新的对象构件而更改原有代码。
缺点
1、使设计变得更加抽象,对象的业务规则如果很复杂,则实现组合模式具有很大挑战性,而且不是所有的方法都与叶子对象子类都有关联
组合模式--使用场景
1、需要表示一个对象整体或部分层次,在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,可以一致地对待它们。
2、让客户能够忽略不同对象层次的变化,客户端可以针对抽象构件编程,无须关心对象层次结构的细节。
组合模式--拓展
说到这里我就不得不说一下透明的组合模式和安全的组合模式了下面,便来介绍这两种之间的区别:
通过引入组合模式,杀毒软件具有良好的可扩展性,在增加新的文件类型时,无须修改现有类库代码,只需增加一个新的文件类作为ComonentFile类的子类即可,但是由于在ComonentFile中声明了大量用于管理和访问成员构件的方法,例如addFile()、delete()等方法,我们不得不在新增的文件类中实现这些方法,提供对应的错误提示和异常处理。为了简化代码,我们有以下两个解决方案:
解决方案一:将叶子构件的addFile()、delete()等方法的实现代码移至ComonentFile类中,由ComonentFile提供统一的默认实现,但是如果这样做的话问题就来了,如果客户端代码针对抽象类ComonentFile编程,在调用文件对象的这些方法时将出现错误提示。如果不希望出现任何错误提示,我们可以在客户端定义文件对象时不使用抽象层,而直接使用具体叶子构件本身就像我提供的例子一样。 这样就产生了一种不透明的使用方式,即在客户端不能全部针对抽象构件类编程,需要使用具体叶子构件类型来定义叶子对象。
package Composite_Pattern; /** * 定义所有文件的抽象类,所有的文件都要继承该类 * @author xyxy001 * */ public abstract class ComponentFile { public String name; public ComponentFile(String name){ this.name=name; } public void addFile(ComponentFile file){
System.out.println("对不起,不支持该方法!");
} public void deleteFile(ComponentFile file){
System.out.println("对不起,不支持该方法!");
} public ComponentFile getFile(String name){
System.out.println("对不起,不支持该方法!");
} public abstract void killVirus(); }
解决方案二:除此之外,还有一种解决方法是在抽象构件ComonentFile中不声明任何用于访问和管理成员构件的方法代码如下:
package Composite_Pattern; public abstract class ComponentFile { public abstract void killVirus(); }
此时,由于在ComonentFile中没有声明addFile()、delete()等访问和管理成员的方法,其叶子构件子类无须提供实现;而且无论客户端如何定义叶子构件对象都无法调用到这些方法,不需要做任何错误和异常处理,容器构件再根据需要增加访问和管理成员的方法,但这时候也存在一个问题:客户端不得不使用容器类本身来声明容器构件对象,否则无法访问其中新增的add()、remove()等方法,如果客户端一致性地对待叶子和容器,将会导致容器构件的新增对客户端不可见,客户端代码对于容器构件无法再使用抽象构件来定义。
在使用组合模式时,根据抽象构件类的定义形式,我们可将组合模式分为透明组合模式和安全组合模式两种形式:
(1) 透明组合模式
透明组合模式中,抽象构件Component中声明了所有用于管理成员对象的方法,包括add()、delete()以及getFile()等方法,这样做的好处是确保所有的构件类都有相同的接口。在客户端看来,叶子对象与容器对象所提供的方法是一致的,客户端可以相同地对待所有的对象。透明组合模式也是组合模式的标准形式,虽然上面的解决方案一在客户端可以有不透明的实现方法,但是由于在抽象构件中包含addFile()、delete()等方法,因此它还是透明组合模式。
透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的。叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供addFile()、delete()以及getFile()等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)。
(2) 安全组合模式
安全组合模式中,在抽象构件Component中没有声明任何用于管理成员对象的方法,而是在Composite类中声明并实现这些方法。这种做法是安全的,因为根本不向叶子对象提供这些管理成员对象的方法,对于叶子对象,客户端不可能调用到这些方法,这就是解决方案二所采用的实现方式。
安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。在实际应用中,安全组合模式的使用频率也非常高,在java AWT中使用的组合模式就是安全组合模式。