设计模式之组合模式(Composite Pattern)
一.什么是组合模式?
组合模式提供了一种层级结构,并允许我们忽略对象与对象集合之间的差别
调用者并不知道手里的东西是一个对象还是一组对象,不过没关系,在组合模式中,调用者本来就不需要知道这些
二.举个例子
假设我们要去描述文件系统,文件系统里有文件和文件夹,文件夹里又有文件夹和文件。。。
没错,这是一个层级结构,就像菜单一样,菜单里有菜单项和子菜单,子菜单里有菜单项和子子菜单。。
层级结构也就是树形结构,我们很容易想到定义一个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)的更多相关文章
- 乐在其中设计模式(C#) - 组合模式(Composite Pattern)
原文:乐在其中设计模式(C#) - 组合模式(Composite Pattern) [索引页][源码下载] 乐在其中设计模式(C#) - 组合模式(Composite Pattern) 作者:weba ...
- 二十四种设计模式:组合模式(Composite Pattern)
组合模式(Composite Pattern) 介绍将对象组合成树形结构以表示"部分-整体"的层次结构.它使得客户对单个对象和复合对象的使用具有一致性.示例有一个Message实体 ...
- 【设计模式】组合模式 Composite Pattern
树形结构是软件行业很常见的一种结构,几乎随处可见, 比如: HTML 页面中的DOM,产品的分类,通常一些应用或网站的菜单,Windows Form 中的控件继承关系,Android中的View继承 ...
- python 设计模式之组合模式Composite Pattern
#引入一 文件夹对我们来说很熟悉,文件夹里面可以包含文件夹,也可以包含文件. 那么文件夹是个容器,文件夹里面的文件夹也是个容器,文件夹里面的文件是对象. 这是一个树形结构 咱们生活工作中常用的一种结构 ...
- 设计模式-12组合模式(Composite Pattern)
1.模式动机 很多时候会存在"部分-整体"的关系,例如:大学中的部门与学院.总公司中的部门与分公司.学习用品中的书与书包.在软件开发中也是这样,例如,文件系统中的文件与文件夹.窗体 ...
- 设计模式系列之组合模式(Composite Pattern)——树形结构的处理
说明:设计模式系列文章是读刘伟所著<设计模式的艺术之道(软件开发人员内功修炼之道)>一书的阅读笔记.个人感觉这本书讲的不错,有兴趣推荐读一读.详细内容也可以看看此书作者的博客https:/ ...
- 浅谈设计模式--组合模式(Composite Pattern)
组合模式(Composite Pattern) 组合模式,有时候又叫部分-整体结构(part-whole hierarchy),使得用户对单个对象和对一组对象的使用具有一致性.简单来说,就是可以像使用 ...
- 设计模式 - 组合模式(composite pattern) 迭代器(iterator) 具体解释
组合模式(composite pattern) 迭代器(iterator) 具体解释 本文地址: http://blog.csdn.net/caroline_wendy 參考组合模式(composit ...
- java_设计模式_组合模式_Composite Pattern(2016-08-12)
概念: 组合模式(Composite Pattern)将对象组合成树形结构以表示“部分-整体”的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性. 有时候又叫做部分-整体模式,它使我们树 ...
- 设计模式 -- 组合模式 (Composite Pattern)
定义: 对象组合成部分整体结构,单个对象和组合对象具有一致性. 看了下大概结构就是集团总公司和子公司那种层级结构. 角色介绍: Component :抽象根节点:其实相当去总公司,抽象子类共有的方法: ...
随机推荐
- python简单基础代码
1.从键盘输入两个数,并计算A的B次幂:number1=raw_input('input number1:')number2=raw_input('input number2:')print 'num ...
- poj3616(LIS简单变式)
题目链接:http://poj.org/problem?id=3616 思路: 我的第一反应是背包,因为每个interval要么选择要么不选,后来发现状态方程很难写出来.后来想一想发现就是LIS的简单 ...
- Spring AOP开发
--------------------siwuxie095 Spring AOP 开发 1.在 Spring 中进行 ...
- Python学习记录day8
目录 Python学习记录day8 1. 静态方法 2. 类方法 3. 属性方法 4. 类的特殊成员方法 4.1 __doc__表示类的描述信息 4.2 __module__ 和 __class__ ...
- ubuntu 操作系统的目录结构
Ubuntu 系统的目录众多,并且 Ubuntu 系统是不分 C 盘.D 盘等的,但是所有的目录都是在/目录下面的. 一./:根目录,是所有目录的绝对路径的起始点,Ubuntu 中的所有文件和目录都在 ...
- AspectJ的XML方式完成AOP的开发之切入点的表达式
1. 再配置切入点的时候,需要定义表达式,重点的格式如下:execution(public * *(..)),具体展开如下: * 切入点表达式的格式如下: * execution([修饰符] 返回值类 ...
- Spring框架的配置文件分开管理(了解)
1. 例如:在src的目录下又多创建了一个配置文件,现在是两个核心的配置文件,那么加载这两个配置文件的方式有两种! * 主配置文件中包含其他的配置文件: <import resource=&qu ...
- Linux系统 SecureCRT SecureFX 注册破解方法
1.创建脚本文件crack-binary.sh,内容如下: #!/bin/sh cd $1 cat SecureCRT | od -A n -v -t x1 | tr -d ' \n ...
- 如何在win 2008 server和win 7上add web site
在 windows 2008 server 英文版的操作系统上,通过桌面上的 Computer 右键选择 Manage ,打开 Server Manager,选中左侧资源树中的Roles 在上图右侧 ...
- break MISSING_BLOCK_LABEL_160; 看源代码出现的,源代码是反编译的
break MISSING_BLOCK_LABEL_160; FileNotFoundException fnfe; fnfe; out.close(); throw fnfe; in.close() ...