组合 Composite
组合模式是一种结构型设计模式,你可以使用它将对象组合成树状结构,并且能像使用独立对象一样使用它们。
将对象组合成树形结构以表示整个部分的层次结构。组合模式可以让用户统一对待单个对象和对象的组合。
为什么要使用?
组合模式的对象职责:
将对象组合成树状结构,并且能像使用独立对象一样使用它们。
组合模式就是一种容器与内容组合在一起的模式。它能够使容器与内容具有一致性。
例如,在文件系统中,有文件夹📁和文件📄两种对象,它们间的层级关系就是树状关系🌲。文件夹属于容器、文件属于内容,但是它们都属于目录条目。所以我们能像使用独立对象一样使用它们。
那么在树状结构中,树节点就是容器🫙,能够承载更多的树节点和叶子节点,叶子节点就是内容。它们都属于节点,可以组合成一棵树。两者不同的是树节点会记录它的子节点而已。
如果你有以下需求,可以考虑使用组合模式。
- 希望一组对象按照某种层级结构进行管理。
- 需要按照统一的行为来处理复杂结构中的对象。
- 能够快速拓展对象组合。
模式结构
-
组件(Component)接口描述了树中简单项目和复杂项目所共有的操作。
-
叶节点(Leaf)是树的基本结构,它不包含子项目。一般情况下,叶节点最终会完成大部分的实际工作,因为它们无法将工作指派给其他部分。
-
容器(Container)——又名“组合(Composite)”——是包含叶节点或其他容器等子项目的单位。容器不知道其子项目所属的具体类,它只通过通用的组件接口与其子项目交互。容器接收到请求后会将工作分配给自己的子项目,处理中间结果,然后将最终结果返回给客户端。
-
客户端(Client)通过组件接口与所有项目交互。因此,客户端能以相同方式与树状结构中的简单或复杂项目交互。
组合模式的类图:
模式实现
文件中只包含自己的信息,属于单个对象;文件夹中能够存储文件和文件夹,属于组合对象。该示例使用组合模式将文件和文件夹归为目录条目,使其具有一致性,方便管理。
示例程序的类图
代码实现
组件
package example;
/** 目录条目抽象组件 */
public interface Entry {
/**
* 获取文件夹或文件名字
* @return 文件夹或文件名字
*/
String getName();
/**
* 获取文件夹的子文件夹和子文件总数或文件大小
* @return 文件夹的子文件夹和子文件总数或文件大小
*/
int getSize();
/**
* 打印文件夹或文件路径
* @param prefix 父文件夹路径
*/
void printList(String prefix);
}
叶节点
package example;
/** 文件叶节点 */
public class File implements Entry {
/** 文件名 */
private String name;
/** 文件大小 */
private int size;
public File(String name, int size) {
this.name = name;
this.size = size;
}
/**
* 获取文件夹或文件名字
* @return 文件夹或文件名字
*/
@Override
public String getName() {
return name;
}
/**
* 获取文件夹的子文件夹和子文件总数或文件大小
* @return 文件夹的子文件夹和子文件总数或文件大小
*/
@Override
public int getSize() {
return size;
}
/**
* 打印文件夹或文件路径
* @param prefix 父文件夹路径
*/
@Override
public void printList(String prefix) {
System.out.println(prefix + "/" + name);
}
}
容器
package example;
import java.util.ArrayList;
/** 文件夹容器 */
public class Directory implements Entry {
/** 文件夹名 */
private String name;
/** 子文件列表 */
private ArrayList<Entry> directory = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
/**
* 添加文件夹或文件
* @param entry 文件夹或文件
* @return 当前文件夹
*/
public Entry add(Entry entry) {
directory.add(entry);
return this;
}
/**
* 获取文件夹或文件名字
* @return 文件夹或文件名字
*/
@Override
public String getName() {
return name;
}
/**
* 获取文件夹的子文件夹和子文件总数或文件大小
* @return 文件夹的子文件夹和子文件总数或文件大小
*/
@Override
public int getSize() {
return directory.size();
}
/**
* 打印文件夹或文件路径
* @param prefix 父文件夹路径
*/
@Override
public void printList(String prefix) {
String path = prefix + "/" + name;
System.out.println(path);
for (Entry entry : directory) {
entry.printList(path);
}
}
}
代码测试
import example.Directory;
import example.File;
/** 测试组合模式 */
public class Test {
public static void main(String[] args) {
File txt = new File("test.txt", 1);
File png = new File("test.png", 10);
Directory root = new Directory("root");
Directory child = new Directory("child");
System.out.println("root文件夹添加child文件夹:");
root.add(child).printList("~");
System.out.println("文件数量:" + root.getSize());
System.out.println("\nroot文件夹添加test.png文件:");
root.add(png).printList("~");
System.out.println("文件数量:" + root.getSize());
System.out.println("\nchild文件夹添加test.txt文件:");
child.add(txt).printList("~/root");
System.out.println("文件数量:" + child.getSize());
}
}
输出结果
root文件夹添加child文件夹:
~/root
~/root/child
文件数量:1
root文件夹添加test.png文件:
~/root
~/root/child
~/root/test.png
文件数量:2
child文件夹添加test.txt文件:
~/root/child
~/root/child/test.txt
文件数量:1
你也可以在 Entry
接口中加入获取父类文件路径的方法,就无需在创建 File
和 Directory
时,直接输入字符串常量。
常用场景和解决方案
- 处理一个树型结构。比如,公司人员组织架构、订单信息等。
- 跨越多个层次结构聚合数据。比如,统计文件夹下文件总数。
- 统一处理一个结构中的多个对象。比如,遍历文件夹下所有
XML
类型文件内容。 - 如果你希望客户端代码以相同方式处理简单和复杂元素,可以使用该模式。组合模式中定义的所有元素共用同一个接口。在这一接口的帮助下,客户端不必在意其所使用的对象的具体类。
模式的优缺点
优点 | 缺点 |
---|---|
你可以利用多态和递归机制更方便地使用复杂树结构。 | 对于功能差异较大的类,提供公共接口或许会有困难。在特定情况下,你需要过度一般化组件接口,使其变得令人难以理解。 |
开闭原则。无需更改现有代码,你就可以在应用中添加新元素,使其成为对象树的一部分。 |
使用组合模式的优势
- 清晰定义分层结构。
- 快速新增节点,提升组合灵活性。
- 简化了使用者使用复杂结构数据的代码。组合模式为你提供了两种共享公共接口的基本元素类型:简单叶节点和复杂容器。容器中可以包含叶节点和其他容器。这使得你可以构建树状嵌套递归对象结构。
使用组合模式的劣势
- 难以限制节点类型。
- 需要增加很多运行时的检查,增加了代码复杂度。
- 错误的遍历算法可能会影响系统性能。
拓展知识
- 你可以在创建复杂组合树时使用生成器模式,因为这可使其构造步骤以递归的方式运行。
- 责任链模式通常和组合模式结合使用。在这种情况下,叶组件接收到请求后,可以将请求沿包含全体父组件的链一直传递至对象树的底部。
- 你可以使用迭代器模式来遍历组合树。
- 你可以使用访问者模式对整个组合树执行操作。
- 你可以使用享元模式实现组合树的共享叶节点以节省内存。
🔙 设计模式
📌最后:希望本文能够给您提供帮助,文章中有不懂或不正确的地方,请在下方评论区💬留言!
🔗参考文献:
▶️ bilibili-趣学设计模式;黄靖锋. --拉勾教育
📖 图解设计模式 /(日)结城浩著;杨文轩译. --北京:人民邮电出版社,2017.1