一.什么是组合模式?

组合模式提供了一种层级结构,并允许我们忽略对象与对象集合之间的差别

调用者并不知道手里的东西是一个对象还是一组对象,不过没关系,在组合模式中,调用者本来就不需要知道这些

二.举个例子

假设我们要去描述文件系统,文件系统里有文件和文件夹,文件夹里又有文件夹和文件。。。

没错,这是一个层级结构,就像菜单一样,菜单里有菜单项和子菜单,子菜单里有菜单项和子子菜单。。

层级结构也就是树形结构,我们很容易想到定义一个Node类,包含一组指向孩子的指针,以此构造一颗完整的树

那么我们的类图将是这样的:

注意:File仅支持类图中列出的操作,Folder类支持继承来的所有操作

类的基本设计就是这样,利用这样的类结构就可以描述文件系统了,下面来做代码实现:

定义Directory基类:

package CompositePattern;

import java.util.ArrayList;

/**
* 定义目录类
* @author ayqy
*/
public abstract class Directory {
String name;
String description;
ArrayList<Directory> files; /**
* 添加指定文件/文件夹到该目录下
* @param dir 将要添加的文件/文件夹
* @return 添加成功/失败
*/
public boolean add(Directory dir){
throw new UnsupportedOperationException();//默认抛出操作异常
} /**
* 删除该目录下的指定文件/文件夹
* @param dir 将要删除的文件/文件夹
* @return 删除成功/失败
*/
public boolean remove(Directory dir){
throw new UnsupportedOperationException();//默认抛出操作异常
} /**
* 清空该目录下所有文件和文件夹
* @return 清空成功/失败
*/
public boolean clear(){
throw new UnsupportedOperationException();//默认抛出操作异常
} public ArrayList<Directory> getFiles() {
throw new UnsupportedOperationException();//默认抛出操作异常
} /**
* 打印输出
*/
public abstract void print(); public String getName() {
return name;
} public String getDescription() {
return description;
} public String toString(){
return name + description;
}
}

P.S.注意我们在基类中对Folder特有的方法的处理方式(抛出异常),当然也可以用更和谐的方式来做,各有各的好处与缺陷,在后文详述采用抛出异常这样粗暴的方式的原因

注意,我们在基类中定义了一个抽象的print方法,想用通过调用print方法来输出整个文件树,组合模式允许我们以很轻松很优雅的方式实现这个麻烦的过程

下面来实现File类:

package CompositePattern;

/**
* 实现文件类
* @author ayqy
*/
public class File extends Directory{ public File(String name, String desc) {
this.name = name;
this.description = desc;
} @Override
public void print() {
System.out.print(this.toString());//输出文件自身信息
}
}

File类非常简单,由于基类中对File不支持的操作都做了默认实现(抛出异常),所以File变得相当苗条

接下来是Folder类:

package CompositePattern;

import java.util.ArrayList;

/**
* 实现文件夹类
* @author ayqy
*/
public class Folder extends Directory{ public Folder(String name, String desc){
this.name = name;
this.description = desc;
this.files = new ArrayList<Directory>();
} @Override
public void print() {
//打印该Folder自身信息
System.out.print(this.toString() + "(");
//打印该目录下所有文件及子文件
for(Directory dir : files){
dir.print();
System.out.print(", ");
}
//打印文件夹遍历结束标志
System.out.print(")");
} @Override
public boolean add(Directory dir){
if(files.add(dir))
return true;
else
return false;
} @Override
public boolean remove(Directory dir){
if(files.remove(dir))
return true;
else
return false;
} @Override
public boolean clear(){
files.clear(); return true;
} @Override
public ArrayList<Directory> getFiles() {
return files;
}
}

Folder类对所有支持的操作提供了自己的实现,并在print方法里做了点文章,用一个非常简单的循环实现了对当前节点所有子孙节点的打印输出(这容易让人联想到什么?没错,是装饰者模式),看起来似乎有些不可思议,不过这正是使用组合模式的好处之一(给递归提供了天然的土壤)

三.效果示例

上面实现了描述文件系统所需的类,不妨测试一下,看看效果:

测试类代码如下:

package CompositePattern;

/**
* 实现一个测试类
* @author ayqy
*/
public class Test {
public static void main(String[] args){
/*构造文件树*/
/*
C
a.txt
b.txt
system
sys.dat
windows
win32
settings
log.txt
win32.config
*/
Directory dir = new Folder("C", "");
dir.add(new File("a.txt", ""));
dir.add(new File("b.txt", ""));
Directory subDir = new Folder("system", "");
subDir.add(new File("sys.dat", ""));
dir.add(subDir);
Directory subDir2 = new Folder("windows", "");
Directory subDir3 = new Folder("win32", "");
subDir3.add(new Folder("settings", ""));
subDir3.add(new File("log.txt", ""));
subDir2.add(subDir3);
subDir2.add(new File("win32.config", ""));
dir.add(subDir2); dir.print();//打印输出文件树
}
}

运行结果如下:

C(a.txt, b.txt, system(sys.dat, ), windows(win32(settings(), log.txt, ), win32.config, ), )

和我们预期的结果基本相同,但美中不足的是:存在多余的逗号分隔符

要想消除多余的逗号,我们就要显示循环在走最后一趟时不输出逗号,其余时候都输出一个逗号

很容易想到用一个显式的迭代器来实现(hasNext不正好用来判断是不是最后一趟吗?别忘了ArrayList是支持迭代器的),我们修改下print方法:

public void print() {
//打印该Folder自身信息
System.out.print(this.toString() + "(");
//打印该目录下所有文件及子文件
Iterator<Directory> iter = getFiles().iterator();
while(iter.hasNext()){
Directory dir = iter.next();
dir.print();
if(iter.hasNext()){
System.out.print(",");
}
}
//打印文件夹遍历结束标志
System.out.print(")");
}

成功消除了碍眼的多余逗号

四.多一点改变

如何打印输出所有关联程序为NotePad.exe的文件信息

那么现在先要给File添加一个新的属性linkedExe,表示与该文件关联的可执行程序,而文件夹则不支持这个属性(在这里我们规定文件夹不支持linkedExe属性,不考虑与文件夹相关联的程序是资源管理器还是别的什么)

为了实现新的需求,我们不得不做一些改动了,为了获得类型上的一致性,我们必须把linkedExe属性添加到基类Directory中(这样做或许会遭到诟病,但有些时候我们不得不牺牲一些好处来换取另一些好处。。)

矩形框中的内容是我们添加的新东西,这些东西都是File支持但Folder不支持的

做了这样的变动之后,我们就可以打印输出所有关联程序为NotePad.exe的文件信息了。当然,还要修改Folder类的print方法:

public void print() {
//打印该目录下所有关联程序为NotePad.exe的文件
for(Directory dir : files){
try{
if("NotePad.exe".equalsIgnoreCase(dir.getLinkedExe())){
dir.print();
}
}catch(UnsupportedOperationException e){
//吃掉异常,继续遍历(Folder不支持getLinkedExe操作)
}
}
}

发现什么了吗?似乎组合模式的缺点越来越明显了

组合模式要求忽略一个对象与一组对象之间的差异,一视同仁的对待它们

没错,照这样做我们确实获得了透明性(print方法中我们并不知道当前正在处理的是一个File还是一个Folder)

但我们甚至“滥用”异常处理机制来掩盖对象集合与单一对象的差别,以追求“一视同仁”

而这样做到底值不值得,需要视具体情况而定(我们总是在牺牲一些东西,以换取另一些更有用的东西,至于这种牺牲是否值得,当然需要权衡)

五.迭代器与组合模式

说好的迭代器呢?我怎么没有看到?它在哪里?

迭代器就藏在组合模式中,我们的print方法内部不就一直在用迭代器吗?(不是隐式迭代器就是显示迭代器。。)

上面的例子中用的迭代器被称为内部迭代器,也就是说,迭代器潜藏在组合模式的构成类中,所以不容易发现

当然,如果你喜欢的话也可以构造一个外部迭代器,就像这样:

在DirectoryIterator中,我们需要手动维护一个栈结构来记录当前的位置(内部迭代器是由系统栈提供的支持),以实现hasNext与next方法

其实还存在一个问题,File类显然不支持iterator方法,但它已经从父类继承过来了,我们应该如何处理?

  • 返回null,那么调用者必须使用if语句进行判断
  • 抛出异常,那么调用者必须做异常处理
  • (推荐做法)返回一个空迭代器(NullIterator),空迭代器如何实现?hasNext直接返回false就好了。。这样做对调用者没有任何影响

六.总结

组合模式提供的树形层次结构使得我们能够一视同仁地对待单一对象与对象集合(获得了操作上的方便),但这样的好处是以牺牲类的单一责任原则换来的,而且组合模式是用继承来实现的,缺少弹性。

所以在使用组合模式的时候应当慎重考虑,想想这样的牺牲是否值得,如果不值得的话,考虑是不是可以用其它设计模式代替。。

七.一点题外话(关于是否抛出异常)

有些时候我们可以选择返回null,返回false,返回错误码等等而不是抛出异常,这些方式或许更和谐一些,但抛出异常在有些时候是对事实最贴切的表达

举个例子,假设我们的File类有一个hasLinkedExe属性,表示是否存在与之关联的应用程序,而Folder不支持hasLinkedExe属性,同时该属性又是从父类继承得到的,我们无法删除它。

此时我们可以选择返回false或者抛出异常:

  • 返回false:表示Folder没有与之关联的应用程序
  • 抛出异常:表示Folder不支持该操作

显然,抛出异常的含义才是我们真正想要表达的

-------

说了这么多,我们费了好大劲去用抛出异常的方式,好像只是为了表达的更贴切一些?

不不不,绝对不要这样想,这一点点意义上的差异可能会导致严重的问题,比如:

假设我们要输出所有尚未关联应用程序的文件(即“未知文件”)

如果我们当初采用了返回false的方式来表示文件夹不支持此操作,那么我们将会得到错误的结果(输出了所有未知文件和所有文件夹。。)

设计模式之组合模式(Composite Pattern)的更多相关文章

  1. 乐在其中设计模式(C#) - 组合模式(Composite Pattern)

    原文:乐在其中设计模式(C#) - 组合模式(Composite Pattern) [索引页][源码下载] 乐在其中设计模式(C#) - 组合模式(Composite Pattern) 作者:weba ...

  2. 二十四种设计模式:组合模式(Composite Pattern)

    组合模式(Composite Pattern) 介绍将对象组合成树形结构以表示"部分-整体"的层次结构.它使得客户对单个对象和复合对象的使用具有一致性.示例有一个Message实体 ...

  3. 【设计模式】组合模式 Composite Pattern

    树形结构是软件行业很常见的一种结构,几乎随处可见,  比如: HTML 页面中的DOM,产品的分类,通常一些应用或网站的菜单,Windows Form 中的控件继承关系,Android中的View继承 ...

  4. python 设计模式之组合模式Composite Pattern

    #引入一 文件夹对我们来说很熟悉,文件夹里面可以包含文件夹,也可以包含文件. 那么文件夹是个容器,文件夹里面的文件夹也是个容器,文件夹里面的文件是对象. 这是一个树形结构 咱们生活工作中常用的一种结构 ...

  5. 设计模式-12组合模式(Composite Pattern)

    1.模式动机 很多时候会存在"部分-整体"的关系,例如:大学中的部门与学院.总公司中的部门与分公司.学习用品中的书与书包.在软件开发中也是这样,例如,文件系统中的文件与文件夹.窗体 ...

  6. 设计模式系列之组合模式(Composite Pattern)——树形结构的处理

    说明:设计模式系列文章是读刘伟所著<设计模式的艺术之道(软件开发人员内功修炼之道)>一书的阅读笔记.个人感觉这本书讲的不错,有兴趣推荐读一读.详细内容也可以看看此书作者的博客https:/ ...

  7. 浅谈设计模式--组合模式(Composite Pattern)

    组合模式(Composite Pattern) 组合模式,有时候又叫部分-整体结构(part-whole hierarchy),使得用户对单个对象和对一组对象的使用具有一致性.简单来说,就是可以像使用 ...

  8. 设计模式 - 组合模式(composite pattern) 迭代器(iterator) 具体解释

    组合模式(composite pattern) 迭代器(iterator) 具体解释 本文地址: http://blog.csdn.net/caroline_wendy 參考组合模式(composit ...

  9. java_设计模式_组合模式_Composite Pattern(2016-08-12)

    概念: 组合模式(Composite Pattern)将对象组合成树形结构以表示“部分-整体”的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性. 有时候又叫做部分-整体模式,它使我们树 ...

  10. 设计模式 -- 组合模式 (Composite Pattern)

    定义: 对象组合成部分整体结构,单个对象和组合对象具有一致性. 看了下大概结构就是集团总公司和子公司那种层级结构. 角色介绍: Component :抽象根节点:其实相当去总公司,抽象子类共有的方法: ...

随机推荐

  1. leetcdoe 175. Combine Two Tables

    给定两个表,一个是人,一个是地址,要求查询所有人,可以没有地址. select a.FirstName, a.LastName, b.City, b.State from Person as a le ...

  2. TZOJ 2754 Watering Hole(最小生成树Kruskal)

    描述 Farmer John has decided to bring water to his N (1 <= N <= 300) pastures which are convenie ...

  3. django做form表单的数据验证

    我们之前写的代码都没有对前端input框输入的数据做验证,我们今天来看下,如果做form表单的数据的验证 在views文件做验证 首先用文字描述一下流程 1.在views文件中导入forms模块 2. ...

  4. Maven 学习笔记(一) 基础环境搭建

    在Java的世界里,项目的管理与构建,有两大常用工具,一个是Maven,另一个是Gradle,当然,还有一个正在淡出的Ant.Maven 和 Gradle 都是非常出色的工具,排除个人喜好,用哪个工具 ...

  5. sql优化常用命令总结

    1.显示执行计划的详细步骤 SET SHOWPLAN_ALL ON; SET SHOWPLAN_ALL OFF; 2. 显示执行语句的IO成本,时间成本 SET STATISTICS IO ON SE ...

  6. jq给动态生成的标签绑定事件的几种方法

    经常遇到给动态生成的标签绑定事件不好用,自己简单测试总结了下,结论如下了: body> <!-- 下面是用纯动态方式生成标签 --> <div id="d2" ...

  7. OpenSSL编程

    简介 OpenSSL是一个功能丰富且自包含的开源安全工具箱.它提供的主要功能有:SSL协议实现(包括SSLv2.SSLv3和TLSv1).大量软算法(对称/非对称/摘要).大数运算.非对称算法密钥生成 ...

  8. loadrunner--常用函数列表【转】

    1.        Intweb_reg_save_param("参数名","LB=左边界","RB=右边界",LAST);/注册函数,在参 ...

  9. 学习GIT 版本控制的好去处 另GDB资料

    廖雪峰的官方网站 http://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000 作者不仅仅是做技 ...

  10. pandas replace函数使用小结

    http://blog.csdn.net/kancy110/article/details/72719340