Menu
CoddyTech

组合模式

本课属于 Coddy Java 学习旅程的“面向对象编程”单元。第 75 课,共 87 课。

组合模式是一种结构型设计模式,可将对象组合成树形结构,以表示部分与整体的层次关系。它通过统一的接口,让客户端能够以相同的方式处理单个对象和对象组合。

想一想文件系统:文件夹可以包含文件和其他文件夹,而这些文件夹又可以包含更多文件和文件夹。无论你处理的是单个文件还是整个文件夹结构,都希望以相同的方式执行“获取大小”等操作。这个模式定义了 Component 接口、Leaf 对象(单个元素)和 Composite 对象(包含子对象的容器):

interface FileComponent {
    void display(String indent);
    int getSize();
}

class File implements FileComponent {
    private String name;
    private int size;
    
    public File(String name, int size) {
        this.name = name;
        this.size = size;
    }
    
    public void display(String indent) {
        System.out.println(indent + name + " (" + size + "KB)");
    }
    
    public int getSize() {
        return size;
    }
}

class Folder implements FileComponent {
    private String name;
    private List<FileComponent> children = new ArrayList<>();
    
    public Folder(String name) {
        this.name = name;
    }
    
    public void add(FileComponent component) {
        children.add(component);
    }
    
    public void display(String indent) {
        System.out.println(indent + name + "/");
        for (FileComponent child : children) {
            child.display(indent + "  ");
        }
    }
    
    public int getSize() {
        return children.stream().mapToInt(FileComponent::getSize).sum();
    }
}

组合对象将操作委托给其子对象,从而支持递归结构。客户端可以与树交互,而无需知道自己处理的是叶节点还是组合对象:

Folder root = new Folder("Documents");
root.add(new File("resume.pdf", 150));

Folder photos = new Folder("Photos");
photos.add(new File("vacation.jpg", 2000));
root.add(photos);

root.display("");
System.out.println("Total: " + root.getSize() + "KB");

组合模式非常适合表示组织结构图、UI 组件或菜单系统等层次结构,这些场景中你需要以相同的方式处理群组和个体。

challenge icon

挑战

简单

让我们使用组合模式构建一个组织架构系统!你将创建一种结构,其中部门可以包含员工和其他子部门,从而统一计算总薪资并展示层级结构,无论你查看的是单个员工还是整个部门。

你将把代码分布在 4 个文件中:

  • OrganizationComponent.java:定义组件接口,员工和部门都将实现该接口。接口应声明两个方法:showDetails(String indent),用于以合适的缩进显示组件;以及 getSalary(),用于以整数形式返回总薪资。
  • Employee.java:创建表示单个员工的叶子类。Employee 有姓名(String)和薪资(int),两者都通过构造函数设置。调用 showDetails 时,应打印 [indent][name]: $[salary]。getSalary 方法只需返回员工的薪资。
  • Department.java:创建可以容纳员工和子部门的组合类。Department 有一个名称(String),并维护一个由 OrganizationComponent 子项组成的列表。它应提供 add(OrganizationComponent component) 方法来添加成员。调用 showDetails 时,打印 [indent][name] Department,然后对每个子项调用 showDetails,并增加缩进(添加两个空格)。getSalary 方法返回所有子项薪资的总和。
  • Main.java:构建你的组织!你将收到 4 项输入:两名员工的姓名和薪资,以及一个子部门的名称和一名员工的信息。

    创建一个名为 "Engineering" 的部门作为根部门。使用前两组姓名和薪资创建两名员工,并将他们添加到该部门。然后,使用第三项输入创建一个子部门,使用第四组姓名和薪资创建一名员工并添加到该子部门,最后将这个子部门添加到 Engineering。

    在 Engineering 部门上调用 showDetails(""),然后按 Total Salary: $[amount] 的格式打印总薪资。

你将按以下顺序收到输入:employee1 的姓名(String)、employee1 的薪资(int)、employee2 的姓名(String)、employee2 的薪资(int)、子部门名称(String)、employee3 的姓名(String)、employee3 的薪资(int)。

例如,输入为 Alice、75000、Bob、65000、QA、Charlie、55000 时,输出如下:

Engineering Department
  Alice: $75000
  Bob: $65000
  QA Department
    Charlie: $55000
Total Salary: $195000

注意,无论是在单个员工还是在包含嵌套子部门的整个部门上调用,相同的 showDetails 和 getSalary 方法都能顺畅工作。组合结构会自动处理递归,让你可以统一处理整个层级结构!

亲手试试

import java.util.Scanner;

class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        // 读取输入
        String emp1Name = scanner.nextLine();
        int emp1Salary = Integer.parseInt(scanner.nextLine());
        String emp2Name = scanner.nextLine();
        int emp2Salary = Integer.parseInt(scanner.nextLine());
        String subDeptName = scanner.nextLine();
        String emp3Name = scanner.nextLine();
        int emp3Salary = Integer.parseInt(scanner.nextLine());
        
        // TODO:创建 "Engineering" 部门作为根节点
        
        // TODO:使用 emp1 和 emp2 的数据创建两名员工,并将他们添加到 Engineering 部门
        
        // TODO:使用 subDeptName 创建子部门
        
        // TODO:使用 emp3 的数据创建一名员工,并将其添加到子部门
        
        // TODO:将子部门添加到 Engineering 部门
        
        // TODO:对 Engineering 部门调用 showDetails("")
        
        // TODO:按以下格式输出薪资总额:Total Salary: $[amount]
    }
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Java 在线编译器