Menu
Coddy logo textTech

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

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

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

У этого паттерна есть два типа участников: узлы Leaf (индивидуальные объекты без дочерних элементов) и узлы Composite (контейнеры, содержащие другие компоненты). Оба реализуют общий интерфейс Component:

abstract class FileSystemItem {
  String get name;
  int getSize();
  void display([String indent = '']);
}

class File implements FileSystemItem {
  @override
  final String name;
  final int size;

  File(this.name, this.size);

  @override
  int getSize() => size;

  @override
  void display([String indent = '']) {
    print('$indent- $name ($size KB)');
  }
}

class Folder implements FileSystemItem {
  @override
  final String name;
  final List<FileSystemItem> _children = [];

  Folder(this.name);

  void add(FileSystemItem item) => _children.add(item);

  @override
  int getSize() => _children.fold(0, (sum, item) => sum + item.getSize());

  @override
  void display([String indent = '']) {
    print('$indent+ $name/');
    for (var child in _children) {
      child.display('$indent  ');
    }
  }
}

void main() {
  var docs = Folder('Documents');
  docs.add(File('resume.pdf', 150));
  docs.add(File('photo.jpg', 2400));

  var projects = Folder('Projects');
  projects.add(File('main.dart', 25));
  docs.add(projects);

  docs.display();
  print('Total: ${docs.getSize()} KB');
}

Класс Folder может содержать как объекты File, так и другие объекты Folder. Когда вы вызываете getSize() для папки, он рекурсивно вычисляет общий размер всего её содержимого. Клиентскому коду не нужно знать, работает ли он с отдельным файлом или с целой иерархией папок — и те, и другие отвечают на одни и те же методы.

challenge icon

Задание

Легко

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

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

  • organization.dart: Этот файл содержит интерфейс компонента, а также классы «листа» и «композита». Создайте абстрактный класс OrganizationUnit с геттером name, методом getSalaryBudget(), который возвращает int, и методом display([String indent = '']) для отображения иерархии. Создайте класс Employee (лист), который реализует OrganizationUnit. У него должны быть имя и зарплата, а метод getSalaryBudget() должен возвращать зарплату сотрудника. Его метод display() должен выводить [indent]- [name] ($[salary]). Затем создайте класс Department (композит), который также реализует OrganizationUnit. У него должно быть имя и список дочерних элементов OrganizationUnit. Добавьте метод add() для включения сотрудников или подотделов. Его метод getSalaryBudget() должен рекурсивно суммировать бюджеты всех дочерних элементов. Его метод display() должен выводить [indent]+ [name]/, после чего отображать каждого ребенка с увеличенным отступом (добавьте два пробела).
  • main.dart: Импортируйте файл организации и постройте структуру компании. Создайте отдел Engineering, содержащий двух сотрудников: Alice с зарплатой 80000 и Bob с зарплатой 75000. Создайте отдел QA с одним сотрудником: Carol с зарплатой 65000. Теперь создайте отдел Technology и добавьте Engineering и QA в качестве подотделов. Вызовите display() для отдела Technology, затем выведите Total Budget: $, а за ним — общий бюджет зарплат.

Прелесть паттерна Компоновщик заключается в том, что Technology.getSalaryBudget() автоматически рассчитывает общую сумму по всем вложенным отделам и сотрудникам — клиентскому коду не нужно знать внутреннюю структуру!

Ожидаемый вывод:

+ Technology/
  + Engineering/
    - Alice ($80000)
    - Bob ($75000)
  + QA/
    - Carol ($65000)
Total Budget: $220000

Шпаргалка

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

Паттерн состоит из:

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

Пример: Файловая система

abstract class FileSystemItem {
  String get name;
  int getSize();
  void display([String indent = '']);
}

class File implements FileSystemItem {
  @override
  final String name;
  final int size;

  File(this.name, this.size);

  @override
  int getSize() => size;

  @override
  void display([String indent = '']) {
    print('$indent- $name ($size KB)');
  }
}

class Folder implements FileSystemItem {
  @override
  final String name;
  final List<FileSystemItem> _children = [];

  Folder(this.name);

  void add(FileSystemItem item) => _children.add(item);

  @override
  int getSize() => _children.fold(0, (sum, item) => sum + item.getSize());

  @override
  void display([String indent = '']) {
    print('$indent+ $name/');
    for (var child in _children) {
      child.display('$indent  ');
    }
  }
}

void main() {
  var docs = Folder('Documents');
  docs.add(File('resume.pdf', 150));
  docs.add(File('photo.jpg', 2400));

  var projects = Folder('Projects');
  projects.add(File('main.dart', 25));
  docs.add(projects);

  docs.display();
  print('Total: ${docs.getSize()} KB');
}

Класс Folder может содержать как объекты File, так и другие объекты Folder. При вызове getSize() для папки он рекурсивно вычисляет общий размер всего её содержимого. Клиентский код работает с отдельными файлами и целыми иерархиями папок единообразно.

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

import 'organization.dart';

void main() {
  // TODO: Создайте отдел Engineering
  // TODO: Добавьте Alice с зарплатой 80000
  // TODO: Добавьте Bob с зарплатой 75000
  
  // TODO: Создайте отдел QA
  // TODO: Добавьте Carol с зарплатой 65000
  
  // TODO: Создайте отдел Technology
  // TODO: Добавьте Engineering и QA в качестве подотделов
  
  // TODO: Вызовите display() для отдела Technology
  
  // TODO: Выведите "Total Budget: $" и общий бюджет зарплат
}
quiz iconПроверьте себя

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

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