Menu
Coddy logo textTech

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

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

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

Представьте себе файловую систему: папка может содержать файлы и другие папки, которые, в свою очередь, могут содержать еще больше файлов и папок. Независимо от того, имеете ли вы дело с отдельным файлом или со всей структурой папок, вы хотите выполнять такие операции, как «получить размер», одинаковым способом. Паттерн определяет интерфейс 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");

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

challenge icon

Задание

Легко

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

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

  • 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: Постройте свою организацию! Вы получите входные данные: имена двух сотрудников с их зарплатами, а также название подотдела с одним сотрудником.

    Создайте отдел "Engineering" в качестве корневого. Добавьте в него двух сотрудников, используя первые две пары имя/зарплата. Затем создайте подотдел, используя третий входной параметр, добавьте в него одного сотрудника, используя четвертую пару имя/зарплата, и добавьте этот подотдел в Engineering.

    Вызовите showDetails("") для отдела Engineering, затем выведите общую зарплату в формате 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 бесперебойно работают независимо от того, вызываются ли они для одного сотрудника или для целого отдела, содержащего вложенные подотделы. Составная структура обрабатывает рекурсию автоматически, позволяя вам единообразно работать со всей иерархией!

Шпаргалка

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

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

  • 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");

Композит рекурсивно делегирует операции своим дочерним элементам. Клиенты взаимодействуют с деревом, не зная, работают ли они с листом или с композитом.

Случаи использования: Организационные диаграммы, компоненты пользовательского интерфейса, системы меню, файловые системы — любая иерархия, где группы и отдельные объекты требуют одинакового обращения.

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

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: Создайте и добавьте двух сотрудников в Engineering, используя данные emp1 и emp2
        
        // TODO: Создайте подотдел, используя subDeptName
        
        // TODO: Создайте и добавьте одного сотрудника в подотдел, используя данные emp3
        
        // TODO: Добавьте подотдел в Engineering
        
        // TODO: Вызовите showDetails("") для отдела Engineering
        
        // TODO: Выведите общую зарплату в формате: Total Salary: $[amount]
    }
}
quiz iconПроверьте себя

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

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