引言
昨天我写了一篇迭代器模式的文章,其中用到餐厅菜单的例子,如果你细想过,肯定是能发现一些问题的,比如昨天的菜单中只有一级菜单(不清楚的同学可以先看看我上一篇文章,但这只是一个引子,并不影响后面的阅读),那当某些餐厅需要往自己的菜单中添加子菜单列表(比如甜品),之前实现的迭代器就无法正确工作了,因此我们需要新的模式来解决这个问题,也就是今天的主角——组合模式。
定义
回到问题的本质,为什么添加甜品后迭代器就无法工作了?因为昨天是针对菜品实现的迭代器,而甜品是一个子菜单,并不支持菜品的某些操作(获取价格),也就是它们的操作不一致导致迭代器需要作出更多复杂的判断才能完成昨天同样的功能。那为什么组合模式就可以解决这个问题呢?先来看看它的定义:
组合模式允许你将对象组合成树形结构来表现“整体/部分”层次结构。组合能让客户以一致的方式处理个别对象及对象组合。
定义很简单,抓住关键词“树形结构”、“一致的方式”,没错,组合模式的关键就是通过将所有对象组合为一个树形结构,再通过某种技巧就能让我们以统一的方式来对待这些对象。那某些技巧是什么呢?你觉得面向接口编程如何?
通过图来看,甜品是新加入的,它对于整个菜单而言是一个子菜单,同时也是一个菜单项,虽然它不支持菜品的一些操作,同时菜品也不支持菜单的特有操作(如显示所有的菜品),但是我们可以将其抽象出一个公共的接口,也就能为他们添加默认的行为(稍后会看到如何实现),如果子类与其父类默认行为不符合时,将其覆盖即可。又因为在子菜单中还包含了许多的菜品,因此整个结构就像一棵树一样,这样我们就可以采用递归的方式对整颗树进行迭代显示所有的菜品(迭代肯定需要操作统一个类型,否则就需要类型判断等复杂的操作,回到了问题的原点)。
好吧,但你讲的还是太复杂了,看不明白。
没关系,现在让我们忘掉过时的例子,以一个非常常见的例子来做代码演示。
Coding
还有什么比电脑的文件夹更能直观的说明这个模式的呢?下面我实现一个简单的listFiles功能。首先我们需要一个公共的抽象类(可以是接口也可以是抽象类):
public abstract class File {
protected String name;
protected double size;
public File(String name) {
this.name = name;
}
/**
* 往文件夹添加文件,返回false表示不支持添加或添加失败
*/
public boolean addFile(File file) {
return false;
}
public String getName() {
return name;
}
public double getSize() {
return size;
}
public boolean isFolder() {
return false;
}
/**
* 修改文件内容,返回false表示不支持修改
*/
public boolean edit() {
return false;
}
/**
* 显示文件夹下所有的文件,返回false表示不是文件夹不支持该操作
*/
public boolean print() {
return false;
}
}
文件夹本质上也是一个文件,所以我这里抽象出一个File抽象类,提供了添加文件、修改文件、是否是文件夹以及显示文件夹下所有的文件等操作,当然文件和文件夹不可能都支持所有的操作,因此你可以将方法都默认抛出一个UnsupportedOperationException异常,不过我这里是返回一个默认的boolean值(想一想为什么?)。接下来实现具体的文件类:
public class ImageFile extends File {
public ImageFile(String name, double size) {
super(name);
this.size = size;
}
}
public class TextFile extends File {
public TextFile(String name, double size) {
super(name);
this.size = size;
}
@Override
public boolean edit() {
System.out.println("修改成功!");
return true;
}
}
这里我实现了一个图片文件和一个文本文件类,它们不支持文件夹才有的print方法,所以使用默认的返回一个false;而文本是可以直接编辑的,因此需要覆盖来支持该项操作。
public class Folder extends File {
private ArrayList<File> files;
public Folder(String name) {
super(name);
files = new ArrayList<>();
}
/**
* 添加文件到文件夹并计算总大小
*/
@Override
public boolean addFile(File file) {
this.files.add(file);
this.size += file.getSize();
return true;
}
@Override
public boolean isFolder() {
return true;
}
@Override
public boolean print() {
Iterator<File> iterator = files.iterator();
while (iterator.hasNext()) {
File file = iterator.next();
// 当前文件是文件夹,则递归显示内部子文件
if (file.isFolder()) {
System.out.println("Folder name: " + file.getName() + ", total size: " + file.getSize());
file.print();
} else {
System.out.println("File name: " + file.getName() + ", file size: " + file.getSize());
}
}
return true;
}
}
文件夹系统则只需要实现isFolder和print方法,前者告诉调用者这是一个文件夹,后者则显示出其下所有的文件,这里需要注意的是如果你在处理不支持的操作时是抛出的异常,那这里就需要捕获异常,这样代码不仅不优雅,还会影响性能(异常实例的构造是相当昂贵的,若非必要,尽量不要使用,感兴趣的可以参考《深入理解JVM》一书)。最后测试一下:
public static void main(String[] args) {
// 构造测试数据
File root = new Folder("root");
File imageFile = new ImageFile("a", 10);
File imageFile1 = new ImageFile("b", 4);
File imageFile2 = new ImageFile("c", 12);
Folder folder = new Folder("图片");
folder.addFile(imageFile);
folder.addFile(imageFile1);
folder.addFile(imageFile2);
File textFile = new TextFile("1", 2);
File textFile1 = new TextFile("2", 5);
File textFile2 = new TextFile("3", 6);
Folder folder1 = new Folder("文本");
folder1.addFile(textFile);
folder1.addFile(textFile1);
folder1.addFile(textFile2);
File file = new ImageFile("d", 53);
File file1 = new TextFile("4", 34);
root.addFile(file);
root.addFile(file1);
root.addFile(folder);
root.addFile(folder1);
// 显示所有文件
root.print();
}
客户端只需要调用print方法就能显示出所有的文件夹和文件,无需知道具体的实现细节,所以对客户而言组合(文件夹)和叶节点(文件)是透明的(也就是组合和叶节点能够统一处理),这样看组合模式是不是很强大?
但是组合模式也是有缺点的,它违反了单一职责原则,它需要维护整个组合的层次结构,同时还要执行相关的操作,这就让Component有了多个改变的理由,不过以此换取对客户的透明性还是很有必要的。
总结
通过组合模式我们学到了如何对客户保证透明性,使得客户能够非常便捷的使用我们提供的方法,也使代码变得更加整洁优美。
同时我们也应该明白不能固执的遵守设计原则,有时打破比遵守能呈现出更好的设计。