设计模式-组合模式
组合模式
一、核心思想(一句话概括)
组合模式允许你将对象组合成树形结构来表示“部分-整体”的层次结构,并使得客户端能够以统一的方式处理单个对象(叶子节点)和对象的组合(组合节点)。
简单来说就是:无论是对一个单独的个体,还是对一个由许多个体组成的群体,你都可以用同样的操作来进行处理。
二、一个生动的比喻:文件系统
理解组合模式最好的例子就是电脑里的文件系统。
-
一个文件系统由文件夹(Folder)和文件(File)组成。
-
一个文件夹里面可以包含其他文件夹和文件。
-
一个文件只是一个独立个体,它里面不能再包含任何东西。
现在,我们想对文件系统进行操作,比如“计算大小”:
-
对于一个文件:计算大小很简单,就是文件本身的大小。
-
对于一个文件夹:它的总大小 = 文件夹内所有文件的大小 + 文件夹内所有子文件夹的大小(递归计算)。
你会发现,无论你面对的是一个文件还是一个文件夹,你都可以问它:“你的大小是多少?”。组合模式的精髓就在于,它能让你用 node.calculateSize() 这一行同样的代码,去处理一个文件节点或者一个复杂的文件夹节点,而无需关心其内部细节。
三、要解决的问题
如果没有组合模式,我们的代码可能会变成这样:
// 伪代码 void calculateSize(Object node) {if (node instanceof File) {// 处理文件的逻辑totalSize += ((File)node).getSize();} else if (node instanceof Folder) {// 处理文件夹的逻辑for (Object child : ((Folder)node).getChildren()) {calculateSize(child); // 递归调用}} }
这种代码的坏处显而易见:
-
客户端代码复杂:充满了 if-else 的类型判断,非常丑陋。
-
违反开闭原则:如果未来我们想增加一种新的节点类型,比如“带压缩的文件夹”,我们就必须修改 calculateSize 方法,在里面增加一个新的 else if 分支。
组合模式就是为了解决这个问题而生的。
四、组合模式的结构与实现
组合模式主要包含三个核心角色:
-
Component(组件):这是一个接口或抽象类,它为树形结构中的所有对象(包括叶子和组合)定义了统一的接口。比如 calculateSize()。
-
Leaf(叶子节点):表示树中的单个对象,它没有子节点。它实现了 Component 接口。在文件系统例子中,File 就是叶子节点。
-
Composite(组合节点):表示可以包含子节点的复杂对象。它也实现了 Component 接口,并且内部通常持有一个 Component 对象的集合(用来存放它的子节点)。它对 Component 接口方法的实现,通常是遍历其子节点,并调用子节点的相应方法。Folder 就是组合节点。
代码实现(用文件系统的例子)
第1步:定义统一的组件接口 (Component)
// Component: 定义了所有节点(文件和文件夹)的通用行为 public interface FileSystemNode {String getName();int getSize(); // 获取大小void print(String prefix); // 打印结构 }
第2步:创建叶子节点 (Leaf)
// Leaf: 文件节点,没有子节点 public class File implements FileSystemNode {private String name;private int size; public File(String name, int size) {this.name = name;this.size = size;} @Overridepublic String getName() {return this.name;} @Overridepublic int getSize() {// 对于文件,直接返回自身大小System.out.println("文件 '" + name + "' 的大小是 " + size + " KB");return this.size;} @Overridepublic void print(String prefix) {System.out.println(prefix + "- " + name + " (" + size + "KB)");} }
第3步:创建组合节点 (Composite)
import java.util.ArrayList; import java.util.List; // Composite: 文件夹节点,可以包含子节点 public class Folder implements FileSystemNode {private String name;private List<FileSystemNode> children = new ArrayList<>(); public Folder(String name) {this.name = name;} public void add(FileSystemNode node) {children.add(node);} public void remove(FileSystemNode node) {children.remove(node);} @Overridepublic String getName() {return this.name;} @Overridepublic int getSize() {int totalSize = 0;System.out.println("开始计算文件夹 '" + name + "' 的大小...");// 关键:遍历所有子节点,并递归调用它们的 getSize() 方法for (FileSystemNode child : children) {totalSize += child.getSize();}System.out.println("文件夹 '" + name + "' 的总大小是 " + totalSize + " KB\n");return totalSize;} @Overridepublic void print(String prefix) {System.out.println(prefix + "+ " + name);// 递归打印子节点for (FileSystemNode child : children) {child.print(prefix + " ");}} }
第4步:客户端使用 客户端现在可以非常简单地构建和操作这个树形结构,而无需关心具体类型。
public class Client {public static void main(String[] args) {// 创建树形结构Folder root = new Folder("我的文档");Folder musicFolder = new Folder("音乐");Folder videoFolder = new Folder("视频");File song1 = new File("周杰伦-青花瓷.mp3", 4000);File song2 = new File("陈奕迅-十年.mp3", 3500);File movie1 = new File("让子弹飞.mp4", 2000000); // 组合对象musicFolder.add(song1);musicFolder.add(song2);videoFolder.add(movie1);root.add(musicFolder);root.add(videoFolder);root.add(new File("个人简历.docx", 512)); // --- 统一操作 ---System.out.println("--- 打印文件结构 ---");// 对根节点调用 print,即可打印整个树,客户端无需关心内部是文件还是文件夹root.print("");System.out.println("\n--- 计算总大小 ---");// 对根节点调用 getSize,即可计算整个树的大小int totalSize = root.getSize();System.out.println("==================================");System.out.println("根目录总大小为: " + totalSize + " KB");} }
五、优缺点
优点
-
简化客户端代码:客户端可以统一对待所有对象,无需进行类型判断,代码更简洁。
-
易于扩展:可以轻松地添加新的叶子节点或组合节点,只要它们实现了 Component 接口,客户端代码无需任何修改,符合开闭原则。
-
天然的递归结构:非常适合用来表示具有层次关系的树形结构。
缺点
-
设计可能过于宽泛:Component 接口的设计是一个挑战。如果叶子节点和组合节点的功能差异很大,那么在公共接口中定义所有行为可能会让叶子节点需要实现一些它本不支持的方法(比如 add、remove)。通常的做法是在叶子节点中对这些方法抛出 UnsupportedOperationException。这被称为“透明性”和“安全性”的权衡。
-
透明方式:Component 接口包含所有管理子节点的方法,客户端使用方便,但对叶子节点不安全。
-
安全方式:Component 接口只包含通用方法,管理子节点的方法只在 Composite 类中,客户端需要类型转换,牺牲了透明性。
-
六、应用场景
只要你遇到需要处理树形结构数据,并且希望以统一方式操作其中所有对象(无论简单还是复杂)的场景,都可以考虑使用组合模式。
-
GUI 工具包:如 Java AWT/Swing,一个窗口(JFrame)可以包含面板(JPanel),面板又可以包含按钮(JButton)、文本框(JTextField)或者其他面板。paint() 或 repaint() 操作就可以在整个组件树上统一执行。
-
组织架构:公司、部门、小组、员工,可以形成一个树形结构。计算薪资总额、下达指令等操作可以统一处理。
-
菜单系统:一个菜单可以包含子菜单和菜单项。
-
XML/JSON 文档解析:文档节点可以是一个简单的值,也可以是一个包含其他节点的复杂对象。