【软考设计模式】组合模式:“部分-整体“层次结构与树形对象统一操作精讲

📅 2026/7/11 13:40:34
【软考设计模式】组合模式:“部分-整体“层次结构与树形对象统一操作精讲
系列定位软考软件设计师 / 系统架构设计师 — 结构型模式专题第 7 讲结构型模式补完篇考察分值上午题 1-2 分下午题常作为代码填空或类图识别出现难度等级⭐⭐⭐☆☆树形结构直观递归遍历是填空重点一、考纲定位与模式定义1.1 考纲要求组合模式在软考中属于结构型模式的基础内容。考察形式包括上午选择题判断场景描述所属模式识别组合模式与装饰模式、外观模式的区别判断类图中是否存在 部分-整体 树形结构区分 Leaf叶子与 Composite组合的角色下午设计题补全 Composite 类的add()/remove()方法维护子组件列表补全operation()方法中遍历子组件并递归调用的逻辑识别类图中 Component、Leaf、Composite 三个角色1.2 模式定义组合模式将对象组合成树形结构以表示 部分-整体 的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。核心意图当系统中存在树形结构如文件系统、组织架构、菜单树、部门层级且客户端需要统一地处理单个节点Leaf和组合节点Composite时组合模式让两者实现同一个接口Component客户端无需区分 这是一个文件还是文件夹、这是一个员工还是部门统一调用operation()即可。通俗理解公司的组织架构是一棵树。CEO 是根节点Composite下面有技术部、市场部等部门Composite部门下面有具体的员工Leaf。如果公司要发一封全员邮件CEO 把邮件发给各部门各部门再发给下属员工。对发邮件这个操作来说部门和员工都支持 接收邮件只是部门的实现是转发给下属员工的实现是自己阅读。组合模式让 发邮件 这个操作对部门和员工一视同仁。二、UML 类图与角色划分┌────────────────────────────────────────────────────────────┐ │ Client │ │ main() │ │ ├── Component leaf new Leaf(); │ │ └── leaf.operation(); │ │ ├── Component composite new Composite(); │ │ ├── composite.add(leaf); │ │ └── composite.operation(); // 递归调用子组件的 operation │ └───────────────────────┬────────────────────────────────────┘ │ uses uniformly ▼ ┌─────────────────────────────────────────────────────────┐ │ interface │ │ Component │ │ operation() │ │ add(Component c) ◄── 可选Leaf 中可能空实现 │ │ remove(Component c) ◄── 可选Leaf 中可能空实现 │ │ getChild(int i): Component │ └───────────────────────┬─────────────────────────────────┘ ┌────────────┴────────────┐ ▼ ▼ ┌─────────────────────┐ ┌────────────────────────┐ │ Leaf │ │ Composite │ │ (叶子节点无子节点) │ │ (组合节点有子节点) │ │ operation() { │ │ - children: List │ │ // 执行具体操作 │ │ Component │ │ } │ │ add(Component) { │ │ add(c) { │ │ children.add(c) │ │ // 空实现或抛异常│ │ } │ │ } │ │ remove(Component){ │ │ remove(c) { │ │ children.remove(c)│ │ // 空实现或抛异常│ │ } │ │ } │ │ operation() { │ │ │ │ // 遍历子组件 │ │ │ │ for (c:children) │ │ │ │ c.operation(); │ │ │ │ } │ └─────────────────────┘ └────────────────────────┘角色职责软考填空关键词Component抽象组件定义 Leaf 和 Composite 的共同接口声明operation()、add()、remove()等方法interface/abstract classoperation()add(Component)Leaf叶子节点表示树中的叶子对象没有子节点实现operation()完成具体操作add()/remove()通常空实现或抛异常implements Componentoperation()具体实现Composite组合节点表示有子节点的组合对象存储一个 Component 列表children实现add()/remove()管理子组件operation()中遍历子组件并递归调用implements ComponentListComponent children 遍历递归Client客户端面向 Component 接口编程统一对待 Leaf 和 CompositeComponent c new Leaf()或new Composite()类图识别要点软考类图中组合模式的核心特征是Composite 类内部持有ListComponent或Component[]组合关系一对多且operation()方法中有遍历子组件并调用operation()的递归逻辑。如果类图里是一个树形结构一个节点包含多个子节点子节点又可以是组合节点→ 组合模式。三、场景一树形菜单系统最经典的组合模式业务背景系统需要渲染一个多级菜单。菜单项有两种① 叶子菜单项如 退出点击后直接执行操作② 子菜单如 文件点击后展开下属菜单。要求客户端统一调用render()方法渲染整个菜单树无需区分叶子和子菜单。说明这是组合模式最经典的例子也是软考下午题最爱考的 树形结构统一操作 场景。3.1 代码实现import java.util.ArrayList; import java.util.List; // Component菜单组件接口 public interface MenuComponent { void render(); void add(MenuComponent component); void remove(MenuComponent component); MenuComponent getChild(int index); } // Leaf叶子菜单项无子节点 public class MenuItem implements MenuComponent { private String name; private String action; public MenuItem(String name, String action) { this.name name; this.action action; } Override public void render() { System.out.println( 菜单项: name [操作: action ]); } Override public void add(MenuComponent component) { throw new UnsupportedOperationException(叶子节点不能添加子组件); } Override public void remove(MenuComponent component) { throw new UnsupportedOperationException(叶子节点不能移除子组件); } Override public MenuComponent getChild(int index) { throw new UnsupportedOperationException(叶子节点没有子组件); } } // Composite子菜单有子节点 public class SubMenu implements MenuComponent { private String name; private ListMenuComponent children new ArrayList(); public SubMenu(String name) { this.name name; } Override public void add(MenuComponent component) { children.add(component); } Override public void remove(MenuComponent component) { children.remove(component); } Override public MenuComponent getChild(int index) { return children.get(index); } Override public void render() { System.out.println(子菜单: name {); for (MenuComponent child : children) { child.render(); } System.out.println(}); } } // Client客户端统一渲染 public class Client { public static void main(String[] args) { MenuComponent rootMenu new SubMenu(系统菜单); MenuComponent fileMenu new SubMenu(文件); fileMenu.add(new MenuItem(新建, create)); fileMenu.add(new MenuItem(打开, open)); fileMenu.add(new MenuItem(保存, save)); MenuComponent editMenu new SubMenu(编辑); editMenu.add(new MenuItem(撤销, undo)); editMenu.add(new MenuItem(重做, redo)); MenuComponent prefMenu new SubMenu(首选项); prefMenu.add(new MenuItem(界面设置, ui)); prefMenu.add(new MenuItem(快捷键, shortcut)); editMenu.add(prefMenu); rootMenu.add(fileMenu); rootMenu.add(editMenu); rootMenu.add(new MenuItem(退出, exit)); rootMenu.render(); } }运行结果子菜单: 系统菜单 { 子菜单: 文件 { 菜单项: 新建 [操作: create] 菜单项: 打开 [操作: open] 菜单项: 保存 [操作: save] } 子菜单: 编辑 { 菜单项: 撤销 [操作: undo] 菜单项: 重做 [操作: redo] 子菜单: 首选项 { 菜单项: 界面设置 [操作: ui] 菜单项: 快捷键 [操作: shortcut] } } 菜单项: 退出 [操作: exit] }关键体会SubMenu.render()中通过for-each遍历children列表对每个子组件调用render()。如果子组件是SubMenu会再次进入SubMenu.render()递归展开如果是MenuItem则直接渲染。客户端完全不需要if (child instanceof SubMenu)这样的类型判断。四、场景二文件系统文件与文件夹统一操作业务背景文件系统中文件File和文件夹Folder都支持 显示大小 操作。文件的大小就是自身字节数文件夹的大小是内部所有文件大小的总和。客户端统一调用getSize()获取任何节点的大小无需区分文件和文件夹。说明这是软考下午题中常见的 递归计算 场景Composite 的getSize()需要遍历子节点并累加。4.1 代码实现import java.util.ArrayList; import java.util.List; // Component文件系统节点接口 public interface FileSystemNode { String getName(); long getSize(); void add(FileSystemNode node); void remove(FileSystemNode node); } // Leaf文件叶子节点 public class File implements FileSystemNode { private String name; private long size; public File(String name, long size) { this.name name; this.size size; } Override public String getName() { return name; } Override public long getSize() { return size; } Override public void add(FileSystemNode node) { throw new UnsupportedOperationException(文件不能添加子节点); } Override public void remove(FileSystemNode node) { throw new UnsupportedOperationException(文件不能移除子节点); } } // Composite文件夹组合节点 public class Folder implements FileSystemNode { private String name; private ListFileSystemNode children new ArrayList(); public Folder(String name) { this.name name; } Override public String getName() { return name; } Override public long getSize() { long totalSize 0; for (FileSystemNode child : children) { totalSize child.getSize(); } return totalSize; } Override public void add(FileSystemNode node) { children.add(node); } Override public void remove(FileSystemNode node) { children.remove(node); } } // Client public class Client { public static void main(String[] args) { FileSystemNode root new Folder(根目录); FileSystemNode docs new Folder(文档); docs.add(new File(简历.pdf, 1024)); docs.add(new File(报告.docx, 2048)); FileSystemNode photos new Folder(照片); photos.add(new File(旅行.jpg, 5120)); photos.add(new File(聚会.png, 3072)); root.add(docs); root.add(photos); root.add(new File(readme.txt, 100)); System.out.println(根目录总大小: root.getSize() 字节); System.out.println(文档文件夹大小: docs.getSize() 字节); System.out.println(照片文件夹大小: photos.getSize() 字节); } }关键体会Folder.getSize()通过递归累加所有子节点的getSize()。如果子节点是Folder会再次递归如果是File直接返回文件大小。客户端只需调用root.getSize()整个树的大小自动计算出来。软考高频考点组合模式的operation()中必须有递归遍历子组件的逻辑。如果代码填空里 Composite 的getSize()只返回自身大小而没有遍历子节点会判错。五、场景三组织架构与薪资计算带返回值递归业务背景公司组织架构是树形结构。员工Employee是叶子节点有基本工资。部门Department是组合节点包含下属员工和子部门。要求统一计算任意节点的月度薪资总额员工的薪资是自己的工资部门的薪资是下属所有节点薪资的总和。说明这是软考下午题中 带返回值的递归计算 场景比void render()稍复杂但核心逻辑相同。5.1 代码实现import java.util.ArrayList; import java.util.List; // Component组织节点接口 public interface OrgNode { String getName(); double calculateSalary(); void add(OrgNode node); void remove(OrgNode node); } // Leaf员工叶子节点 public class Employee implements OrgNode { private String name; private double salary; public Employee(String name, double salary) { this.name name; this.salary salary; } Override public String getName() { return name; } Override public double calculateSalary() { return salary; } Override public void add(OrgNode node) { throw new UnsupportedOperationException(员工不能添加下属); } Override public void remove(OrgNode node) { throw new UnsupportedOperationException(员工不能移除下属); } } // Composite部门组合节点 public class Department implements OrgNode { private String name; private ListOrgNode members new ArrayList(); public Department(String name) { this.name name; } Override public String getName() { return name; } Override public double calculateSalary() { double total 0; for (OrgNode member : members) { total member.calculateSalary(); } return total; } Override public void add(OrgNode node) { members.add(node); } Override public void remove(OrgNode node) { members.remove(node); } } // Client public class Client { public static void main(String[] args) { OrgNode company new Department(总公司); OrgNode techDept new Department(技术部); techDept.add(new Employee(张三, 15000)); techDept.add(new Employee(李四, 18000)); OrgNode qaTeam new Department(测试组); qaTeam.add(new Employee(王五, 12000)); qaTeam.add(new Employee(赵六, 12000)); techDept.add(qaTeam); OrgNode marketDept new Department(市场部); marketDept.add(new Employee(孙七, 10000)); marketDept.add(new Employee(周八, 10000)); company.add(techDept); company.add(marketDept); company.add(new Employee(CEO, 50000)); System.out.println(公司月度薪资总额: ¥ company.calculateSalary()); System.out.println(技术部月度薪资总额: ¥ techDept.calculateSalary()); System.out.println(测试组月度薪资总额: ¥ qaTeam.calculateSalary()); } }六、三种场景对比与演进思路维度场景一树形菜单场景二文件系统场景三组织架构ComponentMenuComponentFileSystemNodeOrgNodeLeafMenuItem菜单项File文件Employee员工CompositeSubMenu子菜单Folder文件夹Department部门operation()render()渲染getSize()获取大小calculateSalary()计算薪资返回值voidlongdouble递归逻辑遍历子组件调用render()累加子组件getSize()累加子组件calculateSalary()叶子 add/remove抛异常抛异常抛异常软考考法下午大题补全 render 遍历下午大题补全 size 累加下午大题补全薪资计算七、组合模式 vs 装饰模式 vs 外观模式核心对比对比项组合模式装饰模式外观模式设计目的统一树形结构中部分和整体的操作增强单个对象的功能层层包装简化多个子系统的使用结构特征树形结构一对多递归组合链式结构一对一层层包装星形结构一个 Facade 连接多个子系统关系类型Composite 组合多个ComponentDecorator 组合一个ComponentFacade 关联多个Subsystem递归方向自顶向下遍历树分发操作自外向内委托增强操作无递归顺序调用Client 调用component.operation()统一对待decorator.operation()层层增强facade.operation()简化入口是否新增功能❌ 不新增只是统一接口✅ 新增功能如加料、加时间戳❌ 不新增只是封装调用软考判断看到 树形结构、部分整体、统一处理文件和文件夹 → 组合看到 套娃、增强、添加职责 → 装饰看到 简化子系统、统一入口 → 外观记忆口诀组合是 树形统一部分整体一视同仁装饰是 套娃增强层层加功能外观是 简化入口封装复杂子系统。八、软考高频考点与易混淆辨析8.1 高频考点考点内容模式分类结构型模式GoF 23 正式成员核心三角色Component抽象组件、Leaf叶子、Composite组合核心思想将对象组合成树形结构让客户端对单个对象和组合对象的使用具有一致性透明性客户端面向 Component 接口编程无需区分 Leaf 和 CompositeComposite 的关键内部维护ListComponent childrenoperation()中遍历子组件递归调用Leaf 的 add/remove通常空实现或抛异常UnsupportedOperationException与装饰区别组合树形结构一对多统一处理部分和整体装饰链式结构一对一增强单个对象与外观区别组合处理树形层次结构外观简化多个平级子系统的调用适用场景① 树形菜单/目录 ② 文件系统 ③ 组织架构 ④ 图形编辑器中的组合图形8.2 易混淆辨析组合 vs 装饰最难辨析对比项组合模式装饰模式UML 结构都有 Component 接口都有组合关系都有 Component 接口都有组合关系结构形态树形一个 Composite 包含多个子 Component链式一个 Decorator 包装一个 Component子对象数量Composite 有多个子对象Decorator 只有一个被装饰对象递归方向自顶向下分发操作父节点调用子节点自外向内委托操作外层调用内层目的让部分和整体被统一处理给单个对象动态添加职责Client 代码root.add(child1); root.add(child2); root.operation();c new DecoratorA(new DecoratorB(component)); c.operation();软考判断看到 树形、文件夹和文件、部门和员工 → 组合看到 层层包装、增强、加料 → 装饰终极判断技巧看类图里组合对象持有的是一个引用还是多个引用。一个引用 → 装饰多个引用List/Array→ 组合。九、真题风格模拟与代码填空模拟题 1上午选择题以下关于组合模式的叙述中正确的是。A. 组合模式属于创建型模式主要用于创建树形结构的对象B. 在组合模式中Leaf 和 Composite 必须实现不同的接口以便客户端区分处理C. 组合模式使得客户端可以统一地对待单个对象和组合对象无需区分部分和整体D. 组合模式与装饰模式的核心区别在于组合模式用于增强对象功能装饰模式用于构建树形结构答案C解析A 错误组合属于结构型模式不是创建型。B 错误Leaf 和 Composite 实现同一个Component 接口这正是组合模式的核心 —— 统一接口。C 正确这是组合模式的核心定义。D 错误说反了。组合模式用于构建树形结构统一处理部分和整体装饰模式用于增强功能层层包装。模拟题 2下午代码填空 — 补全 Composite 的遍历逻辑某系统使用组合模式实现部门架构的薪资计算。OrgNode是抽象组件Employee是叶子节点Department是组合节点。请补全1~3。import java.util.ArrayList; import java.util.List; // Component interface OrgNode { double calculateSalary(); void add(OrgNode node); } // Leaf class Employee implements OrgNode { private double salary; public Employee(double salary) { this.salary salary; } public double calculateSalary() { return salary; } public void add(OrgNode node) { throw new UnsupportedOperationException(); } } // Composite class Department implements OrgNode { private ListOrgNode (1)______ new ArrayList(); public void add(OrgNode node) { (2)______; } public double calculateSalary() { double total 0; for (OrgNode member : (3)______) { total member.calculateSalary(); } return total; } } public class Client { public static void main(String[] args) { OrgNode dept new Department(); dept.add(new Employee(5000)); dept.add(new Employee(6000)); System.out.println(部门薪资总额: dept.calculateSalary()); } }答案(1)members或children/subordinates(2)members.add(node)或children.add(node)(3)members或children阅卷要点(1) 必须是ListOrgNode类型的成员变量名如members、children、subordinates。这是组合节点存储子组件的核心数据结构。(2) 必须是members.add(node)将子组件加入列表。如果写this.add(node)会死循环零分。(3) 必须是遍历子组件列表的变量名与 (1) 一致。for-each遍历是组合模式operation()的核心逻辑缺少遍历会判错。模拟题 3下午代码填空 — 补全 Component 接口与 Leaf 空实现某系统使用组合模式实现图形编辑器中的图形组合。Graphic是抽象组件Line和Rectangle是叶子Picture是组合节点。Picture可以包含多个Graphic。请补全1~4。import java.util.ArrayList; import java.util.List; // Component interface Graphic { void draw(); void (1)______(Graphic g); void remove(Graphic g); } // Leaf直线 class Line implements Graphic { public void draw() { System.out.println(绘制直线); } public void add(Graphic g) { (2)______; } public void remove(Graphic g) { throw new UnsupportedOperationException(直线不能移除子图形); } } // Composite图形组合 class Picture implements Graphic { private ListGraphic graphics new ArrayList(); public void add(Graphic g) { graphics.add(g); } public void remove(Graphic g) { graphics.remove(g); } public void draw() { System.out.println(组合图形 {); for (Graphic g : graphics) { (3)______; } System.out.println(}); } } public class Client { public static void main(String[] args) { Graphic pic new Picture(); pic.add(new Line()); pic.add(new Line()); (4)______; } }答案(1)add(2)throw new UnsupportedOperationException(直线不能添加子图形)或类似空实现/异常(3)g.draw()(4)pic.draw()阅卷要点(1) 必须是add这是 Component 接口声明的统一添加方法。(2) 叶子节点的add()必须抛异常或空实现。如果写graphics.add(g)会零分叶子没有graphics列表。(3) 必须是g.draw()递归调用子图形的绘制方法。这是组合模式的核心遍历逻辑。(4) 必须是pic.draw()客户端统一调用绘制方法。如果写pic.add(...)会扣分因为题目要求的是 统一绘制。十、常见陷阱与注意事项陷阱 1误认为组合是创建型模式组合属于结构型模式因为它解决的是对象之间的结构关系树形组合而不是对象的创建问题。软考上午题如果问以下属于创建型模式的是选项里出现组合不能选。陷阱 2Leaf 和 Composite 实现不同接口这是软考最高频的失分点。组合模式的核心是统一接口❌ 错误Leaf implements LeafInterfaceComposite implements CompositeInterface客户端需要区分✅ 正确Leaf implements ComponentComposite implements Component客户端统一对待软考代码填空里Leaf 和 Composite 必须都implements同一个接口或extends同一个抽象类。陷阱 3Composite 的 operation() 没有遍历子组件这是软考最常考的填空陷阱。Composite 的operation()方法必须包含遍历子组件并递归调用的逻辑for (Component child : children) { child.operation(); }如果 Composite 的operation()只处理了自己而没有遍历子组件那就失去了 组合 的意义客户端需要自己遍历模式就失效了。陷阱 4与装饰模式混淆这是软考最难的辨析题。组合一个 Composite 持有多个ComponentListComponent形成树形结构。装饰一个 Decorator 持有一个Componentprivate Component component形成链式结构。快速判断技巧看类图里组合对象持有的是一个引用还是多个引用。一个引用 → 装饰多个引用List→ 组合。陷阱 5Leaf 的 add/remove 方法处理不当Leaf 的add()和remove()方法通常有两种处理方式抛异常throw new UnsupportedOperationException()最规范明确告知不支持空实现什么都不做也可以但不够严谨软考代码填空里两种方式都算对但抛异常更符合 叶子节点没有子节点 的语义。如果 Leaf 的add()里写了children.add(c)会零分Leaf 没有 children 列表。陷阱 6与外观模式混淆组合模式外观模式处理树形层次结构部分-整体简化多个平级子系统的调用强调统一接口Leaf 和 Composite 同接口强调简化入口Facade 提供高层接口有递归遍历逻辑无递归顺序调用陷阱 7Component 接口设计过宽Component 接口中声明了add()、remove()、getChild()等方法但 Leaf 并不需要这些方法。这是组合模式的一个争议点透明组合模式 vs 安全组合模式。软考中默认考察的是透明组合模式Component 声明所有方法Leaf 空实现或抛异常因为这是 GoF 原书的标准做法。十一、总结要点内容定义将对象组合成树形结构以表示 部分-整体 的层次结构使得用户对单个对象和组合对象的使用具有一致性分类结构型模式GoF 23 正式成员核心三角色Component抽象组件、Leaf叶子、Composite组合核心思想统一接口Leaf 和 Composite 实现同一接口客户端无需区分部分和整体Composite 的关键维护ListComponent childrenoperation()中遍历子组件递归调用Leaf 的 add/remove通常抛UnsupportedOperationException或空实现与装饰区别组合树形结构一对多统一处理部分和整体装饰链式结构一对一增强单个对象与外观区别组合处理树形层次结构外观简化多个平级子系统的调用适用场景树形菜单、文件系统、组织架构、图形编辑器的组合图形软考重点代码填空补全 Composite 的遍历递归逻辑类图识别树形结构 统一接口与装饰的辨析答题技巧看到 树形结构、部分整体、文件和文件夹统一处理、部门和员工统一计算 → 组合看到ListComponent 递归遍历 → 确认组合结构型模式完整总结至此结构型模式七兄弟适配器、桥接、组合、装饰、外观、享元、代理已全部补完。它们共同解决的是类与类之间的组合关系问题适配器转接口、桥接分维度、组合建树形、装饰套娃增强、外观简化入口、享元共享对象、代理控制访问。系列预告下一篇将讲装饰模式—— 当需要在不改变对象接口的前提下动态地给对象添加额外职责时如何用 套娃 的方式层层包装实现功能的灵活扩展。咱们下回见。