Menu
CoddyTech

组合模式

本课属于 Coddy C# 学习旅程的“面向对象编程”单元。第 61 课,共 70 课。

组合模式是一种结构型模式,它可以将对象组合成树形结构,并以统一的方式处理单个对象和对象组。它非常适合表示整体与部分的层次结构,让你可以通过同一个接口处理单个项和集合。

该模式有三个关键组成部分:定义通用操作的组件接口、表示单个对象的叶子类,以及包含子对象并实现相同接口的组合类:

public interface IFileSystemItem
{
    string Name { get; }
    int GetSize();
}

public class File : IFileSystemItem
{
    public string Name { get; }
    private int _size;
    
    public File(string name, int size)
    {
        Name = name;
        _size = size;
    }
    
    public int GetSize() => _size;
}

public class Folder : IFileSystemItem
{
    public string Name { get; }
    private List<IFileSystemItem> _items = new List<IFileSystemItem>();
    
    public Folder(string name) => Name = name;
    
    public void Add(IFileSystemItem item) => _items.Add(item);
    
    public int GetSize() => _items.Sum(item => item.GetSize());
}

这种模式的优点是,客户端不需要知道自己处理的是单个文件还是整个文件夹层级结构:

var root = new Folder("root");
root.Add(new File("doc.txt", 100));

var subFolder = new Folder("images");
subFolder.Add(new File("photo.jpg", 500));
subFolder.Add(new File("icon.png", 50));

root.Add(subFolder);

Console.WriteLine(root.GetSize());  // 650

组合模式适用于文件系统、UI 组件树、组织结构图,以及任何需要以相同方式处理容器和内容的结构。它通过消除区分简单元素和复杂元素的需要,简化客户端代码。

challenge icon

挑战

简单

让我们使用组合模式构建一个组织架构系统。你将创建一种结构,让单个员工和部门(部门中包含员工或其他部门)都能以统一的方式处理,从而计算层级结构中任意级别的总薪资成本。

你将把代码分到以下三个文件中:

  • OrgComponent.cs:在 Organization 命名空间中定义 IOrgComponent 接口,其中包含两个成员:Name 属性(字符串)和返回整数的 GetSalary() 方法。这个接口表示单个员工和部门都要遵循的共同约定。
  • OrgClasses.cs:在同一命名空间中创建两个类:
    • Employee:表示单个员工的叶子类。它在构造函数中接收姓名和薪资,并通过返回自己的薪资来实现该接口。
    • Department:表示一个群组的组合类。它在构造函数中接收名称,并维护一个包含 IOrgComponent 项目的列表。添加一个 Add(IOrgComponent component) 方法来添加成员。它的 GetSalary() 应返回所有成员薪资的总和。
  • Program.cs:根据输入构建组织结构并计算总薪资。你将创建部门和员工,按适当的层级嵌套它们,并展示组合模式如何让你以统一的方式处理整个层级结构。

你将收到以下输入:

  • 要创建的组件数量
  • 对于每个组件:类型(employee 或 department),后面跟着名称;如果是员工,还会提供薪资。如果是部门,还会提供父部门名称(如果是顶层部门,则为 root)

每个组件的输入格式:

employee
{name}
{salary}
{parent_department}

department
{name}
{parent_department}

构建结构后,按照 {root_name} Total Salary: {amount} 的格式输出根部门的总薪资。

例如,输入如下:

5
department
Engineering
root
employee
Alice
5000
Engineering
employee
Bob
4500
Engineering
department
QA
Engineering
employee
Charlie
4000
QA

输出应为:

Engineering Total Salary: 13500

Engineering 部门包含 Alice(5000)、Bob(4500)和 QA 部门。QA 中包含 Charlie(4000)。在 Engineering 上调用 GetSalary() 时,它会递归计算所有嵌套组件的薪资总和,这展示了组合模式如何让你通过简单、统一的接口处理复杂的树形结构!

亲手试试

using System;
using System.Collections.Generic;
using Organization;

class Program
{
    public static void Main(string[] args)
    {
        int n = Convert.ToInt32(Console.ReadLine());
        
        // TODO: 创建一个按名称存储部门的字典
        // 这样你在添加成员时就能找到上级部门
        Dictionary<string, Department> departments = new Dictionary<string, Department>();
        
        // TODO: 记录根部门
        Department root = null;
        
        for (int i = 0; i < n; i++)
        {
            string type = Console.ReadLine();
            
            if (type == "department")
            {
                string name = Console.ReadLine();
                string parent = Console.ReadLine();
                
                // TODO: 创建部门
                // TODO: 将其添加到 departments 字典中
                // TODO: 如果 parent 是 "root",这个部门就是根部门
                // TODO: 否则,找到上级部门并将这个部门添加到其中
            }
            else if (type == "employee")
            {
                string name = Console.ReadLine();
                int salary = Convert.ToInt32(Console.ReadLine());
                string parent = Console.ReadLine();
                
                // TODO: 创建员工
                // TODO: 找到上级部门并将员工添加到其中
            }
        }
        
        // TODO: 输出根部门的工资总额
        // 格式:"{root_name} Total Salary: {amount}"
        Console.WriteLine($"{root.Name} Total Salary: {root.GetSalary()}");
    }
}
quiz icon自我测试

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

面向对象编程中的所有课

自己练习: C# 在线编译器