什么是组合模式
组合模式提供了一种层级结构,并允许我们忽略对象与对象集合之间的差别。
调用者并不知道手里的东西是一个对象还是一组对象,不过没关系,在组合模式中,调用者本来就不需要知道这些。
举个例子
假设我们要去描述文件系统,文件系统里有文件和文件夹,文件夹里又有文件夹和文件。。。
没错,这是一个层级结构,就像菜单一样,菜单里有菜单项和子菜单,子菜单里有菜单项和子子菜单。。
层级结构也就是树形结构,我们很容易想到定义一个Node类,包含一组指向孩子的指针,以此构造一颗完整的树
那么我们的类图将是这样的:
注意:File仅支持类图中列出的操作,Folder类支持继承来的所有操作
类的基本设计就是这样,利用这样的类结构就可以描述文件系统了,下面来做代码实现:
定义Directory基类:
import java.util.ArrayList;
/**
* 定义目录类
*
*/
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类:
/**
* 实现文件类
*
*/
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类:
import java.util.ArrayList;
/**
* 实现文件夹类
*
*/
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
方法里做了点文章,用一个非常简单的循环实现了对当前节点所有子孙节点的打印输出(这容易让人联想到什么?没错,是装饰者模式),看起来似乎有些不可思议,不过这正是使用组合模式的好处之一(给递归提供了天然的土壤)。
效果示例
上面实现了描述文件系统所需的类,不妨测试一下,看看效果:
测试类代码如下:
/**
* 实现一个测试类
* @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(")");
}
成功消除了碍眼的多余逗号
C(a.txt,b.txt,system(sys.dat),windows(win32(settings(),log.txt),win32.config))
多一点改变
如何打印输出所有关联程序为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
方法内部不就一直在用迭代器吗?(不是隐式迭代器就是显示迭代器。。)
上面的例子中用的迭代器被称为内部迭代器,也就是说,迭代器潜藏在组合模式的构成类中,所以不容易发现
当然,如果你喜欢的话也可以构造一个外部迭代器,就像这样:
在DirectoryIterator
中,我们需要手动维护一个栈结构来记录当前的位置(内部迭代器是由系统栈提供的支持),以实现hasNext
与next
方法
其实还存在一个问题,File类显然不支持iterator
方法,但它已经从父类继承过来了,我们应该如何处理?
- 返回null,那么调用者必须使用if语句进行判断
- 抛出异常,那么调用者必须做异常处理
- (推荐做法)返回一个空迭代器(
NullIterator
),空迭代器如何实现?hasNext
直接返回false
就好了。。这样做对调用者没有任何影响
总结
组合模式提供的树形层次结构使得我们能够一视同仁地对待单一对象与对象集合(获得了操作上的方便),但这样的好处是以牺牲类的单一责任原则换来的,而且组合模式是用继承来实现的,缺少弹性。
所以在使用组合模式的时候应当慎重考虑,想想这样的牺牲是否值得,如果不值得的话,考虑是不是可以用其它设计模式代替。。