11.1 Composite 模式
在计算机的文件系统中,有“文件夹”的概念。文件夹里面既可以放入文件,也可以放入其他文件夹(子文件夹)。以此类推,文件夹是形成了一种容器结构、递归结构。
虽然文件夹与文件是不同类型的对象,但是它们都“可以被放入到文件夹中”。文件夹和文件有时也被称为“目录条目”。在目录条目中,文件夹和文件被当作是同一种对象看待(即一致性)。
有时,与将文件夹和文件都作为目录条目看待一样,将容器和内容作为同一种东西看待,可以帮助我们方便地处理问题。
能够使容器与内容具有一致性,创造出递归结构的模式就是 Composite 模式。Composite 含义有 “混合物”、“复合物” 的意思。
11.2 示例程序
下面我们来看一段 Composite 模式的示例程序,该示例程序的功能是:列出文件和文件夹的一览。在示例程序中,表示文件的是File 类,表示文件夹的是 Directory 类,为了能将它们统一起来,我们设计了父类 Entry,表示 “目录条目” 的类,这样就实现了 File 类和 Directory 类的一致性。
|| Entry 类
Entry 类是一个表示目录条目的抽象类。
拥有一个 getName 方法获取名字,getSize 方法获取大小。
向文件夹中放入文件和文件夹的方式是 add 方法。不过实现这个 add 方法的目录是子类 Directory 类。在 Entry 类中,只是简单的抛出异常而已。
toString 方法定义了实例的标准文字显示方法。getName 和 getSize 都是抽象方法,需要子类去实现这些方法(Template Method 模式)。
/**
* Composite - Object.
*/
public abstract class Entry {
public abstract String getName();
public abstract int getSize();
protected abstract void printList(String prefix);
public Entry add(Entry entry) throws FileTreatmentException {
throw new FileTreatmentException();
}
public void printList() {
printList("");
}
@Override
public String toString() {
// Template Method
return getName() + " (" + getSize() + ")";
}
}
|| File 类
File 类是表示文件的类,它是 Entry 类的子类。因为 File 类实现了父类 Entry 的 abstract 方法,因此 File 类自身就不是抽象类了。
/**
* one son of entry.
*/
public class File extends Entry{
private String name;
private int size;
public File(String name, int size) {
this.name = name;
this.size = size;
}
@Override
public String getName() {
return name;
}
@Override
public int getSize() {
return size;
}
@Override
protected void printList(String prefix) {
System.out.println(prefix + "/" + this);
}
}
|| Directory 类
Directory 类是表示文件夹的类。它也是 Entry 类的子类。
其中 getName 方法只是简单的返回了 name。但在 getSize 方法中则进行了计算处理。它会遍历 directory 字段中的所有元素,然后计算出它们的大小的总和。
size += entry.getSize();
在变量 size 中加上了 entry 的大小,但 entry 可能是 File 类的实例,也可能是 Directory 类的实例。不过,不论是哪个类的实例,都可以通过 getSize 方法得到他的大小。这就是 Composite 模式的特征 —— “容器与内容的一致性” —— 的表现。即使将来编写了其他 Entry 的子类,它也会实现 getSize 方法,因此 Directory 类的这部分代码无需做任何修改。
add 方法用于向文件夹中加入文件和子文件夹。不会判断接收到的 entry 实例的具体类型,直接添加到 list 列表中。
printList 方法用于显示文件夹的目录条目的一览。printList 方法也会递归调用,与 getSize 方法一样。因为容器和内容的一致性。
/**
* one son of Entry.
*/
public class Directory extends Entry{
private String name;
private List<Entry> directory = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public int getSize() {
int size = 0;
for (Entry entry : directory) {
size += entry.getSize();
}
return size;
}
/**
* 递归调用
* @param prefix
*/
@Override
protected void printList(String prefix) {
System.out.println(prefix + "/" + this);
for (Entry entry : directory) {
entry.printList(prefix + "/" + getName());
}
}
/**
* override baseClass add method
* @param entry
* @return
* @throws FileTreatmentException
*/
@Override
public Entry add(Entry entry) throws FileTreatmentException {
directory.add(entry);
return this;
}
}
|| FileTreatMentException 类
FileTreatMentException 类是对文件调用 add 方法时抛出的异常。是我们为本示例程序编写的异常类。
public class FileTreatmentException extends Exception{
public FileTreatmentException(){}
public FileTreatmentException(String msg) {
super(msg);
}
}
|| Main 类
Main 类将使用以上的类建成下面这样的文件夹结构。在 Main 中,我们首先新建 root、bin、tmp、usr 这 4 个文件夹,然后在 bin 文件夹中放入 vi 文件和 latex 文件。
接着,我们在 usr 文件夹下新建 yuki、hanako、tomura 这个文件夹,然后将3个用户各自的文件分别放入到这些文件夹中。
public class Main {
public static void main(String[] args) {
try {
System.out.println("Making root entries...");
Directory rootDir = new Directory("root");
Directory binDir = new Directory("bin");
Directory tmpDir = new Directory("tmp");
Directory usrDir = new Directory("usr");
rootDir.add(binDir).add(tmpDir).add(usrDir);
binDir.add(new File("vi", 10000));
binDir.add(new File("latex", 20000));
rootDir.printList();
System.out.println("");
System.out.println("Making user entries...");
Directory yuki = new Directory("yuki");
Directory hanako = new Directory("hanako");
Directory tomura = new Directory("tomura");
usrDir.add(yuki).add(hanako).add(tomura);
yuki.add(new File("diary.html", 100));
yuki.add(new File("Composite.java", 200));
hanako.add(new File("memo.tex", 300));
tomura.add(new File("game.doc", 400));
tomura.add(new File("junk.mail", 500));
rootDir.printList();
} catch (FileTreatmentException e) {
e.printStackTrace();
}
}
}
运行结果:
Making root entries...
/root (30000)
/root/bin (30000)
/root/bin/vi (10000)
/root/bin/latex (20000)
/root/tmp (0)
/root/usr (0)
Making user entries...
/root (31500)
/root/bin (30000)
/root/bin/vi (10000)
/root/bin/latex (20000)
/root/tmp (0)
/root/usr (1500)
/root/usr/yuki (300)
/root/usr/yuki/diary.html (100)
/root/usr/yuki/Composite.java (200)
/root/usr/hanako (300)
/root/usr/hanako/memo.tex (300)
/root/usr/tomura (900)
/root/usr/tomura/game.doc (400)
/root/usr/tomura/junk.mail (500)
11.3 Composite 模式中的登场角色
在 Composite 模式中有以下登场角色。
◆ Leaf (树叶)
表示 “内容” 的角色,在该角色中不能放入其他对象。在示例程序中,由 File 类扮演此角色。
◆ Composite (复合物)
表示容器的角色,可以在其中放入 leaf 角色和 Composite 角色。在示例程序中,由 Directory 类扮演此角色。
◆ Component
使 Leaf 角色和 Component 角色具有一致性的角色。Composite 角色是 Leaf 和 Composite 角色的父类。在示例程序中,由 Entry 类扮演此角色。
◆ Client
使用 Composite 模式的角色。在示例程序中,由 Main 类扮演此角色。
Composite 模式的类图如下图所示。在该图中,我们可以将 Composite 角色与它内部的 Component 角色(Leaf 或 Composite角色)看成是父亲与孩子们的关系。getChild 方法的作用是从 Component 角色获取这些 “孩子们”。
11.4 拓展思路的要点
|| 多个和单个的一致性
使用 Composite 模式可以使容器与内容具有一致性,也可以称其为多个和单个的一致性,即将多个对象结合在一起,当作一个对象进行处理。
例如,试想以下测试程序行为时的场景。假设 Test1 是用测试输入数据来自键盘输入时的程序的行为, Test2 是用来测试输入数据来自文件时的程序的行为,Test3 是用来测试输入数据来自网络时的程序行为。如果我们想将这 3 种测试统一为 “输入测试”,那么 Composite 模式就有了用武之地了。我们可以将这几个测试结合起来作为 “输入测试”,或是将其他几个测出是结合一起作为 “输出测试”,甚至最后可以将 “输入测试” 和 “输出测试” 结合在一起作为 “输入输出测试”。
|| Add 方法应该放在哪里
在示例程序中,Entry 类中定义了 add 方法,所做的处理就是抛出异常,这是因为能使用 add 方法的只能是 Directory 类。
-
方法1:定义在 Entry 类中,报错
将 add 方法定义在 Entry 类中,让其报错,这也是示例程序中的做法。能使用 add 方法的只有 Directory 类,它会重写 add 方法,根据需求实现其处理。 -
方法2:定义在 Entry 类中,但什么都不做
将 add 方法定义在 Entry 类中,但什么也不做。 -
方法3:声明在 Entry 类中,但不实现
在 Entry 类中声明 add 抽象方法。如果子类需要 add 方法就根据需求实现该方法,如果不需要 add 方法,则可以简单的报错。该方法的优点是所有的子类必须都实现 add 方法,不需要 add 方法时的处理也可以交给子类自己去做决定。但同时也是缺点,因为不需要的类中也必须定义本来完全不需要的 add 方法。 -
方法4:只定义在 Directory 类中
因为只有 Directory 类中可以使用 add 方法,所以可以不在 Entry 类中定义 add 方法,而是只将其定义在 Directory 类中。
|| 到处都存在递归结构
在示例程序中,我们以文件夹的结构为例进行了学习,但实际上在程序世界中,到处都存在递归结构和 Composite 模式。例如,在 视窗系统中,一个窗口下可以含有多个子窗口。通常来说,树结构的数据结构都适用 Composite 模式。