概念
组合模式是一种结构型设计模式,它提供了一种方式来将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
在组合模式中,有一个根对象(Component),它包含了一系列的子对象(SubObject)。根对象负责管理其所有子对象的状态,并且定义了访问这些子对象的方法。当需要添加或删除子对象时,根对象会相应地进行调整。
使用组合模式的好处在于,它可以将复杂的对象结构分解为简单的组成部分,并且可以轻松地添加、删除或替换这些组成部分。此外,由于子对象是作为根对象的成员存在的,因此可以方便地对它们进行操作和管理。
组合模式为处理树形结构提供了一种较为完美的解决方案,它描述了如何将容器和叶子进行递归组合,使得用户在使用时无须对它们进行区分,可以一致地对待容器和叶子。
● Component(抽象构件):它可以是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问及管理它的子构件的方法,如增加子构件、删除子构件、获取子构件等。
● Leaf(叶子构件):它在组合结构中表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过异常等方式进行处理。
● Composite(容器构件):它在组合结构中表示容器节点对象,容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。
组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器,以此实现递归组合,形成一个树形结构。
示例
示例一
我们用公司的组织机构来举一个例子,公司的各层机构作为容器,人员作为叶子节点
1、定义抽象构件
/**
* 抽象组织机构:抽象构件
* */
abstract class Component {
public abstract void add(Component c) throws Exception; //增加成员
public abstract void remove(Component c) throws Exception; //删除成员
public abstract Component getChild(int i); //获取成员
public abstract void getName(String orgnaName); //业务方法 获取用户名
}
2、定义容器构件
/**
* 组织机构容器:继承抽象构件
* */
public class Orgna extends Component{
List<Component> componentList = new ArrayList<>();
private String orgnaName;
public Orgna(String orgnaName) {
this.orgnaName = orgnaName;
}
@Override
public void add(Component c) throws Exception {
componentList.add(c);
}
@Override
public void remove(Component c) throws Exception {
componentList.remove(c);
}
@Override
public Component getChild(int i) {
return componentList.get(i);
}
@Override
public void getName(String orgnaName) {
for (Component obj : componentList) {
obj.getName(this.orgnaName);
}
}
}
3、定义叶子节点
/**
* 员工作为叶子节点
* */
public class staffLeaf extends Component{
private String name;
public staffLeaf(String name){
this.name = name;
}
@Override
public void add(Component c) throws Exception {
throw new Exception();
}
@Override
public void remove(Component c) throws Exception {
throw new Exception();
}
@Override
public Component getChild(int i) {
return null;
}
@Override
public void getName(String orgnaName) {
System.out.println( orgnaName + " 职位的员工:" + name);
}
}
4、客户端
Component orgna,orgna1,orgna2,orgna3,staffLeaf1,staffLeaf2,staffLeaf3,staffLeaf4,staffLeaf5;
orgna = new Orgna("公司");
orgna1 = new Orgna("董事会");
orgna2 = new Orgna("经理");
orgna3 = new Orgna("普通员工");
staffLeaf1 = new staffLeaf("张三");
staffLeaf2 = new staffLeaf("李四");
staffLeaf3 = new staffLeaf("王五");
staffLeaf4 = new staffLeaf("王六");
staffLeaf5 = new staffLeaf("王七");
orgna.add(orgna1);
orgna.add(orgna2);
orgna.add(orgna3);
orgna1.add(staffLeaf1);
orgna2.add(staffLeaf2);
orgna3.add(staffLeaf3);
orgna3.add(staffLeaf4);
orgna3.add(staffLeaf5);
orgna.getName("公司");
输出结果
董事会 职位的员工:张三
经理 职位的员工:李四
普通员工 职位的员工:王五
普通员工 职位的员工:王六
普通员工 职位的员工:王七
示例二
同样的机构,我们这次不区分叶子节点还是容器节点
/**
* 抽象组织机构:抽象构件
* */
abstract class ComponentNew {
public abstract void add(ComponentNew c) throws Exception; //增加成员
public abstract void remove(ComponentNew c) throws Exception; //删除成员
public abstract ComponentNew getChild(int i); //获取成员
public abstract void getName(); //业务方法 获取用户名
}
/**
* 组织机构容器:继承抽象构件
* */
public class OrgnaNew extends ComponentNew {
List<ComponentNew> componentList = new ArrayList<>();
private String orgnaName;
private Integer orgnaLevel;
public OrgnaNew(String orgnaName,Integer orgnaLevel) {
this.orgnaName = orgnaName;
this.orgnaLevel = orgnaLevel;
}
@Override
public void add(ComponentNew c) throws Exception {
componentList.add(c);
}
@Override
public void remove(ComponentNew c) throws Exception {
componentList.remove(c);
}
@Override
public ComponentNew getChild(int i) {
return componentList.get(i);
}
@Override
public void getName() {
System.out.println("机构等级:" + orgnaLevel + ";机构名称:" + orgnaName);
for (ComponentNew obj : componentList) {
obj.getName();
}
}
}
客户端
ComponentNew orgna,orgna1,orgna2,orgna3,orgna1_1,orgna2_1,orgna3_1,orgna3_2;
orgna = new OrgnaNew("1000",1);
orgna1 = new OrgnaNew("1100",2);
orgna2 = new OrgnaNew("1200",2);
orgna3 = new OrgnaNew("1300",2);
orgna1_1 = new OrgnaNew("1101",3);
orgna2_1 = new OrgnaNew("1201",3);
orgna3_1 = new OrgnaNew("1301",3);
orgna3_2 = new OrgnaNew("1302",3);
orgna.add(orgna1);
orgna.add(orgna2);
orgna.add(orgna3);
orgna1.add(orgna1_1);
orgna2.add(orgna2_1);
orgna3.add(orgna3_1);
orgna3.add(orgna3_2);
orgna.getName();
输出结果
机构等级:1;机构名称:1000
机构等级:2;机构名称:1100
机构等级:3;机构名称:1101
机构等级:2;机构名称:1200
机构等级:3;机构名称:1201
机构等级:2;机构名称:1300
机构等级:3;机构名称:1301
机构等级:3;机构名称:1302
示例三
当我们扫描文件时,文件夹可以作为容器节点,具体的文件比如图片、文本可以作为叶子节点,使用组合模式从跟目录递归遍历这些文件。
//抽象文件类:抽象构件
abstract class AbstractFile {
public abstract void add(AbstractFile file);
public abstract void remove(AbstractFile file);
public abstract AbstractFile getChild(int i);
public abstract void scanFile();
}
//图像文件类:叶子构件
class ImageFile extends AbstractFile {
private String name;
public ImageFile(String name) {
this.name = name;
}
@Override
public void add(AbstractFile file) {
throw new Exception();
}
@Override
public void remove(AbstractFile file) {
throw new Exception();
}
@Override
public AbstractFile getChild(int i) {
throw new Exception();
return null;
}
@Override
public void scanFile() {
//模拟扫描
System.out.println("-->对图像文件'" + name + "'进行扫描");
}
}
//文本文件类:叶子构件
class TextFile extends AbstractFile {
private String name;
public TextFile(String name) {
this.name = name;
}
@Override
public void add(AbstractFile file) {
throw new Exception();
}
@Override
public void remove(AbstractFile file) {
throw new Exception();
}
@Override
public AbstractFile getChild(int i) {
throw new Exception();
return null;
}
@Override
public void scanFile() {
//模拟扫描
System.out.println("-->对文本文件'" + name + "'进行扫描");
}
}
//视频文件类:叶子构件
class VideoFile extends AbstractFile {
private String name;
public VideoFile(String name) {
this.name = name;
}
@Override
public void add(AbstractFile file) {
throw new Exception();
}
@Override
public void remove(AbstractFile file) {
throw new Exception();
}
@Override
public AbstractFile getChild(int i) {
throw new Exception();
return null;
}
@Override
public void scanFile() {
//模拟扫描
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;
}
@Override
public void add(AbstractFile file) {
fileList.add(file);
}
@Override
public void remove(AbstractFile file) {
fileList.remove(file);
}
@Override
public AbstractFile getChild(int i) {
return (AbstractFile)fileList.get(i);
}
@Override
public void scanFile() {
System.out.println("-------对文件夹'" + name + "'进行扫描"); //模拟扫描
//递归调用成员构件的scanFile()方法
for(Object obj : fileList) {
((AbstractFile)obj).scanFile();
}
}
}
客户端
//针对抽象构件编程
AbstractFile file1,file2,file3,file4,file5,folder1,folder2,folder3,folder4;
folder1 = new Folder("根目录文件夹");
folder2 = new Folder("图像文件");
folder3 = new Folder("文本文件");
folder4 = new Folder("视频文件");
file1 = new ImageFile("林俊杰.jpg");
file2 = new ImageFile("张杰.gif");
file3 = new TextFile("斗罗大陆.txt");
file4 = new TextFile("java开发手册.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);
//从“根目录文件夹”节点开始进行扫描文件
folder1.scanFile();
输出结果
-------对文件夹'根目录文件夹'进行扫描
-------对文件夹'图像文件'进行扫描
-->对图像文件'林俊杰.jpg'进行扫描
-->对图像文件'张杰.gif'进行扫描
-------对文件夹'文本文件'进行扫描
-->对文本文件'斗罗大陆.txt'进行扫描
-->对文本文件'java开发手册.doc'进行扫描
-------对文件夹'视频文件'进行扫描
-->对视频文件'从你的全世界路过.rmvb'进行扫描
Process finished with exit code 0
透明组合模式与安全组合模式
通过以上两个例子我们可以看出,无论是子节点还是容器节点,都有add,remove等方法,而子节点是不需要这些方法的,如果子节点调用了这些方法要么报错要么抛出异常,因此我们要区分一下透明组合模式与安全组合模式。
一、透明组合模式
上述两个示例为透明组合模式
二、安全组合模式
安全组合模式有两种处理方案
方案1
在子节点中保留操作方法,抽象组合类则预先实现这些方法
//提供默认实现的抽象构件类
abstract class AbstractFile {
public void add(AbstractFile file) {
throw new Exception();
}
public void remove(AbstractFile file) {
throw new Exception();
}
public AbstractFile getChild(int i) {
throw new Exception();
return null;
}
public abstract void scanFile();
}
另外,客户端在使用时,调用子节点时可以直接定义子节点的类,这样可以避免开发人员在使用时误认为是容器节点
ImageFile file1,file2;
TextFile file3,file4;
VideoFile file5;
AbstractFile folder1,folder2,folder3,folder4;
方案2
我们可以在抽象组合类中只保留操作方法
abstract class AbstractFile {
public abstract void scanFile();
}
容器节点中单独编写add、remove等方法,子节点则只实现scanFile方法即可,这样在客户端使用时,可以分别定义抽象组合类和容器节点
AbstractFile file1,file2,file3,file4,file5;
Folder folder1,folder2,folder3,folder4; //不能透明处理容器构件
这样的话,开发人员在使用时,叶子节点就无法调用到多余的方法。
总结
优点:
1、简化客户端代码:使用组合模式,客户端可以以统一的方式处理单个对象和组合对象,无需区分它们的差异,从而简化了客户端的代码。
2、增强扩展性:组合模式使得在对象树中添加新对象变得简单,可以通过继承组合模式的抽象类来实现。这样可以方便地扩展对象的层次结构,而无需修改现有代码。
3、统一操作:组合模式使得可以对组合对象和叶子对象执行相同的操作,从而统一了操作接口。客户端无需关心是对单个对象还是组合对象进行操作,简化了代码的逻辑。
4、简化管理对象集合:组合模式提供了一种树形结构来管理对象集合,使得对集合中的对象进行增加、移除和遍历等操作变得更加简单和直观。
缺点:
1、可能会导致系统过于一般化:使用组合模式时,对象的层次结构可能变得过于抽象和一般化,可能会导致某些特定操作变得困难或复杂。
2、难以限制组合对象的类型:组合模式通常会将组合对象和叶子对象抽象成一个类,这意味着在对象树中可以任意组合各种类型的对象。这可能导致某些操作在特定类型的对象上是不适用的,需要额外的处理来限制对象的类型。
适用场景:
1、树形结构:当对象集合具有层次结构,例如文件系统的文件和文件夹,菜单和子菜单等,可以使用组合模式进行管理。
2、部分-整体关系:当希望以统一的方式处理单个对象和组合对象时,可以使用组合模式。例如,对于组织机构中的公司、部门和员工,可以使用组合模式进行管理。
3、递归结构:当对象的结构可以递归定义为相似的对象组合时,可以使用组合模式。这样可以简化代码,提高扩展性,并使得操作可以在整个对象树上执行。
4、统一操作:当希望对组合对象和叶子对象进行一致的操作时,可以使用组合模式。例如,对于一个文件夹中的所有文件,可以通过组合模式实现一次性的遍历、搜索或执行操作。
总之,组合模式适用于具有层次结构的对象集合,并且需要以统一的方式处理单个对象和组合对象的场景。它可以简化代码、增强扩展性,并使得操作可以在整个对象树上执行。但需要注意,过度使用组合模式可能导致系统过于一般化和复杂化。