系列定位:软考软件设计师 / 系统架构设计师 --- 结构型模式专题第 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 class + operation() + add(Component) |
| Leaf(叶子节点) | 表示树中的叶子对象,没有子节点,实现 operation() 完成具体操作;add()/remove() 通常空实现或抛异常 |
implements Component + operation() 具体实现 |
| Composite(组合节点) | 表示有子节点的组合对象,存储一个 Component 列表(children),实现 add()/remove() 管理子组件;operation() 中遍历子组件并递归调用 |
implements Component + List<Component> children + 遍历递归 |
| Client(客户端) | 面向 Component 接口编程,统一对待 Leaf 和 Composite | Component c = new Leaf() 或 new Composite() |
类图识别要点 :软考类图中,组合模式的核心特征是 Composite 类内部持有
List<Component>或Component[](组合关系,一对多),且operation()方法中有 遍历子组件并调用operation()的递归逻辑。如果类图里是一个树形结构(一个节点包含多个子节点,子节点又可以是组合节点)→ 组合模式。
三、场景一:树形菜单系统(最经典的组合模式)
业务背景 :系统需要渲染一个多级菜单。菜单项有两种:① 叶子菜单项(如 "退出",点击后直接执行操作)② 子菜单(如 "文件",点击后展开下属菜单)。要求客户端统一调用 render() 方法渲染整个菜单树,无需区分叶子和子菜单。
说明:这是组合模式最经典的例子,也是软考下午题最爱考的 "树形结构统一操作" 场景。
3.1 代码实现
java
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 List<MenuComponent> 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 代码实现
java
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 List<FileSystemNode> 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 代码实现
java
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 List<OrgNode> 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());
}
}
六、三种场景对比与演进思路
| 维度 | 场景一:树形菜单 | 场景二:文件系统 | 场景三:组织架构 |
|---|---|---|---|
| Component | MenuComponent |
FileSystemNode |
OrgNode |
| Leaf | MenuItem(菜单项) |
File(文件) |
Employee(员工) |
| Composite | SubMenu(子菜单) |
Folder(文件夹) |
Department(部门) |
| operation() | render()(渲染) |
getSize()(获取大小) |
calculateSalary()(计算薪资) |
| 返回值 | void |
long |
double |
| 递归逻辑 | 遍历子组件调用 render() |
累加子组件 getSize() |
累加子组件 calculateSalary() |
| 叶子 add/remove | 抛异常 | 抛异常 | 抛异常 |
| 软考考法 | 下午大题(补全 render 遍历) | 下午大题(补全 size 累加) | 下午大题(补全薪资计算) |
七、组合模式 vs 装饰模式 vs 外观模式:核心对比
| 对比项 | 组合模式 | 装饰模式 | 外观模式 |
|---|---|---|---|
| 设计目的 | 统一 树形结构 中部分和整体的操作 | 增强 单个对象的功能(层层包装) | 简化 多个子系统的使用 |
| 结构特征 | 树形结构(一对多,递归组合) | 链式结构(一对一,层层包装) | 星形结构(一个 Facade 连接多个子系统) |
| 关系类型 | Composite 组合 多个 Component | Decorator 组合 一个 Component | Facade 关联 多个 Subsystem |
| 递归方向 | 自顶向下遍历树(分发操作) | 自外向内委托(增强操作) | 无递归,顺序调用 |
| Client 调用 | component.operation()(统一对待) |
decorator.operation()(层层增强) |
facade.operation()(简化入口) |
| 是否新增功能 | ❌ 不新增(只是统一接口) | ✅ 新增功能(如加料、加时间戳) | ❌ 不新增(只是封装调用) |
| 软考判断 | 看到 "树形结构"、"部分整体"、"统一处理文件和文件夹" → 组合 | 看到 "套娃"、"增强"、"添加职责" → 装饰 | 看到 "简化子系统"、"统一入口" → 外观 |
记忆口诀:组合是 "树形统一"(部分整体一视同仁),装饰是 "套娃增强"(层层加功能),外观是 "简化入口"(封装复杂子系统)。
八、软考高频考点与易混淆辨析
8.1 高频考点
| 考点 | 内容 |
|---|---|
| 模式分类 | 结构型模式(GoF 23 正式成员) |
| 核心三角色 | Component(抽象组件)、Leaf(叶子)、Composite(组合) |
| 核心思想 | 将对象组合成 树形结构 ,让客户端对 单个对象 和 组合对象 的使用具有一致性 |
| 透明性 | 客户端面向 Component 接口编程,无需区分 Leaf 和 Composite |
| Composite 的关键 | 内部维护 List<Component> children,operation() 中遍历子组件递归调用 |
| 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)。
java
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 List<OrgNode> (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) 必须是
List<OrgNode>类型的成员变量名,如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)。
java
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 List<Graphic> 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:误认为组合是创建型模式
组合属于 结构型 模式,因为它解决的是 对象之间的结构关系 (树形组合),而不是对象的创建问题。软考上午题如果问"以下属于创建型模式的是",选项里出现"组合" 不能选。
陷阱 2:Leaf 和 Composite 实现不同接口
这是软考 最高频的失分点 。组合模式的核心是 统一接口:
-
❌ 错误:
Leaf implements LeafInterface,Composite implements CompositeInterface(客户端需要区分) -
✅ 正确:
Leaf implements Component,Composite implements Component(客户端统一对待)
软考代码填空里,Leaf 和 Composite 必须都 implements 同一个接口(或 extends 同一个抽象类)。
陷阱 3:Composite 的 operation() 没有遍历子组件
这是软考 最常考的填空陷阱 。Composite 的 operation() 方法必须包含 遍历子组件并递归调用 的逻辑:
java
for (Component child : children) {
child.operation();
}
如果 Composite 的 operation() 只处理了自己而没有遍历子组件,那就失去了 "组合" 的意义,客户端需要自己遍历,模式就失效了。
陷阱 4:与装饰模式混淆
这是软考 最难的辨析题。
-
组合 :一个 Composite 持有 多个 Component(
List<Component>),形成 树形 结构。 -
装饰 :一个 Decorator 持有 一个 Component(
private Component component),形成 链式 结构。
快速判断技巧:看类图里组合对象持有的是一个引用还是多个引用。一个引用 → 装饰;多个引用(List)→ 组合。
陷阱 5:Leaf 的 add/remove 方法处理不当
Leaf 的 add() 和 remove() 方法通常有两种处理方式:
-
抛异常 :
throw new UnsupportedOperationException()(最规范,明确告知不支持) -
空实现:什么都不做(也可以,但不够严谨)
软考代码填空里,两种方式都算对,但抛异常更符合 "叶子节点没有子节点" 的语义。如果 Leaf 的 add() 里写了 children.add(c) 会 零分(Leaf 没有 children 列表)。
陷阱 6:与外观模式混淆
| 组合模式 | 外观模式 |
|---|---|
| 处理 树形层次 结构(部分-整体) | 简化 多个平级 子系统的调用 |
| 强调 统一接口(Leaf 和 Composite 同接口) | 强调 简化入口(Facade 提供高层接口) |
| 有 递归 遍历逻辑 | 无递归,顺序调用 |
陷阱 7:Component 接口设计过宽
Component 接口中声明了 add()、remove()、getChild() 等方法,但 Leaf 并不需要这些方法。这是组合模式的一个争议点("透明组合模式" vs "安全组合模式")。软考中默认考察的是 透明组合模式(Component 声明所有方法,Leaf 空实现或抛异常),因为这是 GoF 原书的标准做法。
十一、总结
| 要点 | 内容 |
|---|---|
| 定义 | 将对象组合成树形结构以表示 "部分-整体" 的层次结构,使得用户对单个对象和组合对象的使用具有一致性 |
| 分类 | 结构型模式(GoF 23 正式成员) |
| 核心三角色 | Component(抽象组件)、Leaf(叶子)、Composite(组合) |
| 核心思想 | 统一接口:Leaf 和 Composite 实现同一接口,客户端无需区分部分和整体 |
| Composite 的关键 | 维护 List<Component> children,operation() 中遍历子组件递归调用 |
| Leaf 的 add/remove | 通常抛 UnsupportedOperationException 或空实现 |
| 与装饰区别 | 组合:树形结构,一对多,统一处理部分和整体;装饰:链式结构,一对一,增强单个对象 |
| 与外观区别 | 组合:处理树形层次结构;外观:简化多个平级子系统的调用 |
| 适用场景 | 树形菜单、文件系统、组织架构、图形编辑器的组合图形 |
| 软考重点 | 代码填空(补全 Composite 的遍历递归逻辑);类图识别(树形结构 + 统一接口);与装饰的辨析 |
| 答题技巧 | 看到 "树形结构"、"部分整体"、"文件和文件夹统一处理"、"部门和员工统一计算" → 组合;看到 List<Component> + 递归遍历 → 确认组合 |
结构型模式完整总结 :至此,结构型模式七兄弟(适配器、桥接、组合、装饰、外观、享元、代理)已全部补完。它们共同解决的是 类与类之间的组合关系 问题:适配器转接口、桥接分维度、组合建树形、装饰套娃增强、外观简化入口、享元共享对象、代理控制访问。
系列预告 :下一篇将讲 装饰模式 ------ 当需要在不改变对象接口的前提下,动态地给对象添加额外职责时,如何用 "套娃" 的方式层层包装,实现功能的灵活扩展。咱们下回见。