组合模式 发表于 2025-07-11 更新于 2025-07-15
组合模式 Rif 2025-07-11 2025-07-15 设计模式笔记:组合模式 (Composite Pattern) 一、一句话概括 将对象组合成树形结构 以表示“部分-整体”的层次结构,并使得客户端能够统一地对待单个对象(叶子)和对象组合(容器) 。
二、为什么需要它 (Why - The Pain Point) 在很多应用场景中,我们需要处理具有层级结构的数据。最经典的例子就是文件系统:一个目录下既可以包含文件(单个对象),也可以包含其他目录(对象组合),而这些子目录又可以继续包含文件和目录,形成一个无限嵌套的树形结构。
痛点:
如果你不使用组合模式,客户端代码在处理这种结构时会变得非常复杂和混乱。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void display (Object node) { if (node instanceof File) { File file = (File) node; System.out.println("File: " + file.getName()); } else if (node instanceof Directory) { Directory dir = (Directory) node; System.out.println("Directory: " + dir.getName()); for (Object child : dir.getChildren()) { display(child); } } }
这种代码的问题显而易见:
客户端逻辑复杂 :代码中充斥着 instanceof 判断和类型转换,既不美观也容易出错。
违反开闭原则 :如果未来我们想增加一种新的节点类型,比如“符号链接(Symbolic Link)”,那么就必须修改所有包含 if-else 判断的客户端代码。
核心问题 :如何让客户端代码忽略“简单对象”和“复杂容器对象”之间的差异,用一种统一的方式来操作整个树形结构?
三、它是什么 (What - The Solution) 组合模式通过定义一个共同的抽象,完美地解决了这个问题。它让“叶子节点”和“容器节点”实现同一个接口,从而在客户端眼中,它们都变成了同一种东西。
核心思想和角色:
**组件 (Component)**:
这是一个接口或抽象类,为组合中的所有对象(包括叶子和容器)定义了一个统一的接口。
它声明了所有节点共有的操作,例如 display(), getSize() 等。
**叶子 (Leaf)**:
表示树中的基本对象,它没有子节点 。
它实现了 Component 接口,并提供了这些操作的具体实现。例如,一个 File 对象。
**组合 (Composite)**:
表示树中的容器节点,它可以包含子节点 (这些子节点本身也是 Component)。
它也实现了 Component 接口。对于接口中的操作,它通常会将请求委托 给它的子节点去处理,并可能对子节点返回的结果进行汇总。例如,一个 Directory 对象的 getSize() 就是其所有子节点大小的总和。
它还提供了管理子节点的方法,如 add(), remove(), getChild()。
关键设计抉择:透明性 vs. 安全性
在设计 Component 接口时,有一个经典的两难选择:管理子节点的方法(add, remove)应该放在哪里?
**透明模式 (Transparency)**:将 add/remove 等方法声明在 Component 接口中。
优点 :客户端完全无需区分叶子和容器,可以对任何 Component 对象调用 add 方法,非常“透明”。
缺点 :不“安全”。叶子节点本身不能有子节点,所以它必须为空实现或抛出 UnsupportedOperationException。这在某种程度上违反了里氏替换原则。
**安全模式 (Safety)**:只在 Composite 类中声明 add/remove 方法。
优点 :类型安全。你永远不会对一个叶子对象调用 add 方法。
缺点 :不“透明”。客户端必须先判断一个对象是不是 Composite 类型,然后才能调用 add 方法,这又回到了 instanceof 的老路。
在实际应用中,透明模式更为常见 ,因为它最大化地简化了客户端代码,这也是组合模式的主要目标。
四、怎么做 (How - The Blueprint) 基本步骤(以文件系统为例):
创建 Component 抽象类/接口 FileSystemComponent,声明通用方法如 display() 和 getSize()。为了透明性,也在这里声明 add/remove 方法(可以提供默认的空实现或抛出异常)。
创建 Leaf 类 FileNode,继承 FileSystemComponent。实现 display 和 getSize。add/remove 方法可以沿用父类的异常实现。
创建 Composite 类 DirectoryNode,继承 FileSystemComponent。
内部持有一个 List<FileSystemComponent> 来存储子节点。
实现 display 方法,通过遍历子节点并调用它们的 display 方法来完成。
实现 add/remove 方法来管理子节点列表。
客户端通过组合 FileNode 和 DirectoryNode 对象来构建一棵树,然后只需对根节点调用 display() 即可展示整个文件系统的结构。
Java 示例代码 (采用透明模式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 import java.util.ArrayList;import java.util.List;abstract class FileSystemComponent { protected String name; public FileSystemComponent (String name) { this .name = name; } public String getName () { return name; } public abstract void display (String indent) ; public abstract long getSize () ; public void add (FileSystemComponent component) { throw new UnsupportedOperationException ("Cannot add to a file." ); } public void remove (FileSystemComponent component) { throw new UnsupportedOperationException ("Cannot remove from a file." ); } } class FileNode extends FileSystemComponent { private long size; public FileNode (String name, long size) { super (name); this .size = size; } @Override public void display (String indent) { System.out.println(indent + "- " + getName() + " (" + getSize() + " bytes)" ); } @Override public long getSize () { return this .size; } } class DirectoryNode extends FileSystemComponent { private List<FileSystemComponent> children = new ArrayList <>(); public DirectoryNode (String name) { super (name); } @Override public void add (FileSystemComponent component) { children.add(component); } @Override public void remove (FileSystemComponent component) { children.remove(component); } @Override public void display (String indent) { System.out.println(indent + "+ " + getName()); for (FileSystemComponent component : children) { component.display(indent + " " ); } } @Override public long getSize () { long totalSize = 0 ; for (FileSystemComponent component : children) { totalSize += component.getSize(); } return totalSize; } } public class CompositePatternDemo { public static void main (String[] args) { DirectoryNode root = new DirectoryNode ("root" ); DirectoryNode documents = new DirectoryNode ("documents" ); DirectoryNode pictures = new DirectoryNode ("pictures" ); FileNode resume = new FileNode ("resume.docx" , 1024 ); FileNode photo1 = new FileNode ("photo1.jpg" , 2048 ); FileNode photo2 = new FileNode ("photo2.png" , 4096 ); DirectoryNode project = new DirectoryNode ("project" ); FileNode code = new FileNode ("main.java" , 512 ); root.add(documents); root.add(pictures); documents.add(resume); documents.add(project); project.add(code); pictures.add(photo1); pictures.add(photo2); System.out.println("--- Displaying File System Structure ---" ); root.display("" ); System.out.println("\n--- Calculating Sizes ---" ); System.out.println("Size of 'documents' directory: " + documents.getSize() + " bytes" ); System.out.println("Total size of 'root' directory: " + root.getSize() + " bytes" ); } }
五、UML 类图 下面是上述例子的 PlantUML 代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @startuml skinparam classAttributeIconSize 0 hide empty members abstract class FileSystemComponent { # String name + {abstract} void display(String indent) + {abstract} long getSize() + void add(FileSystemComponent c) + void remove(FileSystemComponent c) } class FileNode extends FileSystemComponent { - long size + void display(String indent) + long getSize() } class DirectoryNode extends FileSystemComponent { - List<FileSystemComponent> children + void display(String indent) + long getSize() + void add(FileSystemComponent c) + void remove(FileSystemComponent c) } ' 组合关系:DirectoryNode 包含多个 FileSystemComponent DirectoryNode *-- "0..*" FileSystemComponent class CompositePatternDemo { + {static} void main(String[] args) } CompositePatternDemo ..> FileSystemComponent : uses @enduml
图示解读:
FileNode (叶子) 和 DirectoryNode (组合) 都继承自 FileSystemComponent (组件)。
DirectoryNode 内部包含一个 FileSystemComponent 的集合,表示它可以拥有子节点。这个关系由一个带星号和菱形的线(组合关系 *--)表示。
客户端 CompositePatternDemo 只与抽象的 FileSystemComponent 交互。
六、优缺点分析 优点 (Pros):
简化客户端代码 :客户端可以统一地处理所有对象,无需关心它是叶子还是容器,大大降低了代码的复杂性。
易于增加新组件 :增加新的叶子或容器类非常容易,只要它们实现了 Component 接口即可,符合开闭原则。
结构清晰 :定义了清晰的“部分-整体”层次结构,代码易于理解。
缺点 (Cons):
设计变得过于通用(透明模式的代价) :Component 接口中包含了它所有子类(叶子和容器)可能用到的方法。这使得叶子类会继承到它本不该有的方法(如add/remove),可能需要在运行时抛出异常来处理,这在编译时是无法检查的。
难以限制组合中的组件类型 :有时候我们希望一个容器里只能放特定类型的叶子,组合模式本身不提供这样的约束机制,需要额外添加代码来实现。
七、在 Java 中的应用
Java AWT/Swing GUI 框架 : 这是组合模式最经典的教科书级应用。
java.awt.Component 是抽象组件。
JButton, JTextField 等是叶子节点。
java.awt.Container 是组合节点(JPanel, JFrame 都继承自它)。一个 Container 可以包含 Component(包括其他的 Container),形成复杂的 GUI 布局。你可以对一个 JPanel 调用 setVisible(true),它会自动让其内部所有组件都可见。
**JavaServer Faces (JSF)**:JSF 的 UI 组件树也是一个典型的组合模式应用。