一.什么是组合模式?

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

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

二.举个例子

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

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

层级结构也就是树形结构,我们很容易想到定义一个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. Redis的简介

    Redis 简介 Redis 是一个高性能的key-value数据库.支持复杂的数据结构,支持持久化,支持主从集群,支持高可用,支持较大的value存储... Redis是一个nosql,非关系型数据 ...

  2. cin中函数的作用

    cin是istream类的对象,它是从标准输入设备(键盘)获取数据,程序中的变量通过流提取符">>"从流中提取数据.流提取符">>"从流 ...

  3. CSS中的各种width(宽度)

    一 window对象的innerWidth.outerWidth innerWidth是可用区域的宽度(内容区 + 滚动条) outerWidth是浏览器窗口的宽度(可用区域的宽度+审查元素区域的宽度 ...

  4. mysql数据库的最基本的命令

    #查看mysql有哪些数据库:show databases; 创建一个数据库名称为DataBaseName,字符编码为utf8支持中文create database DataBaseName char ...

  5. mysqlbateis generator 当遇到tinyint 生成转化bool 解决方法

    当遇到tyint 生成转化bool  类型问题很恶心,记录一下解决方法 一. TinyInt转换规则 JAVA数据类型 和 MYSQL的数据类型转换,要注意tinyInt 类型,且存储长度为1的情况. ...

  6. 抽象 abstract 和 接口 interface。 java 的 堆 和 栈。 参数传递(基本类型和普通对象的区别)

    package com.test; import com.test.Pro; //protected 修饰的成员只能在本包中和 继承该类的方法中使用 public abstract class Tes ...

  7. HTTP 500.21 处理程序“PageHandlerFactory-Integrated”在其模块列表中有一个错误模块“ManagedPipelineHandler”

    问题:处理程序“PageHandlerFactory-Integrated”在其模块列表中有一个错误模块“ManagedPipelineHandler” IIS7下部署网站出现错误,原因是先安装的II ...

  8. 2018.09.24 bzoj1486: [HNOI2009]最小圈(01分数规划+spfa判负环)

    传送门 答案只保留了6位小数WA了两次233. 这就是一个简单的01分数规划. 直接二分答案,根据图中有没有负环存在进行调整. 注意二分边界. 另外dfs版spfa判负环真心快很多. 代码: #inc ...

  9. 2018.08.22 hyc的xor/mex(线段树/01trie)

    hyc的xor/mex 描述 NOIP2017就要来了,备战太累,不如做做hyc的新题? 找回自信吧! 一句话题意:n个数,m个操作 操作具体来讲分两步 1.读入x,把n个数全部xor上x 2.询问当 ...

  10. Spring Cloud基础教程视频教程

    视频课程包含: Spring Cloud基础视频教程24G 目录 获取方式: 关注公众微信号:博涵大数据 或者扫描下面的二维码关注获取. 关注后在公众平台上回复"SpringCloud基础& ...