Menu
Coddy logo textTech

Паттерн Компоновщик

Часть раздела Объектно-ориентированное программирование путешествия по C# на Coddy — урок 61 из 70.

Паттерн Компоновщик (Composite) — это структурный паттерн, который позволяет группировать объекты в древовидные структуры и работать с одиночными объектами и их группами единообразно. Он идеально подходит для представления иерархий «часть-целое», когда вы хотите взаимодействовать как с отдельными элементами, так и с коллекциями через один и тот же интерфейс.

Паттерн состоит из трех ключевых компонентов: интерфейса компонента, определяющего общие операции, классов листьев (leaf), представляющих отдельные объекты, и составных классов (composite), которые содержат дочерние элементы и реализуют тот же интерфейс:

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

Паттерн Компоновщик (Composite) идеально подходит для файловых систем, деревьев компонентов пользовательского интерфейса, организационных диаграмм или любых структур, где контейнеры и содержимое должны обрабатываться одинаково. Он упрощает клиентский код, устраняя необходимость различать простые и сложные элементы.

challenge icon

Задание

Легко

Давайте создадим систему организационной структуры, используя паттерн Компоновщик (Composite). Вы создадите структуру, в которой как отдельные сотрудники, так и отделы (содержащие сотрудников или другие отделы) могут рассматриваться единообразно, что позволит вам рассчитать общую стоимость заработной платы на любом уровне иерархии.

Вы организуете свой код в трех файлах:

  • OrgComponent.cs: Определите интерфейс IOrgComponent в пространстве имен Organization с двумя членами: свойством 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). Когда вы вызываете GetSalary() для Engineering, он рекурсивно вычисляет общую сумму всех вложенных компонентов — демонстрируя, как паттерн Компоновщик позволяет работать со сложными древовидными структурами через простой, единообразный интерфейс!

Шпаргалка

Паттерн Компоновщик (Composite) — это структурный паттерн, который объединяет объекты в древовидные структуры и позволяет работать с отдельными объектами и их группами единообразно через общий интерфейс. Он идеально подходит для иерархий «часть-целое».

Паттерн состоит из трех ключевых компонентов:

  • Интерфейс компонента (Component interface): определяет общие операции
  • Классы листьев (Leaf classes): представляют отдельные объекты
  • Классы компоновщиков (Composite classes): содержат дочерние элементы и реализуют тот же интерфейс

Пример базовой реализации:

// Интерфейс компонента
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

Паттерн Компоновщик полезен для файловых систем, деревьев компонентов пользовательского интерфейса, организационных диаграмм или любых структур, где контейнеры и содержимое должны обрабатываться одинаково.

Попробуйте сами

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Проверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование