Перейти к содержанию

Composite (Компоновщик)

Категория: структурный паттерн.

Проблема

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

Решение

  1. Вводится единый интерфейс для всех узлов дерева - и для простых ("листьев"), и для составных.
  2. Лист реализует этот интерфейс напрямую, выполняя операцию над собой.
  3. Составной узел (Composite) реализует тот же интерфейс, но его реализация операции рекурсивно вызывает ту же операцию у всех своих дочерних элементов и агрегирует результат.
  4. Клиентский код вызывает операцию на корне дерева, не зная (и не заботясь), сколько уровней вложенности внутри и что где - лист, а что - составной узел.

Структура

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

Когда применять

  • Предметная область естественно моделируется как дерево "часть - целое".
  • Клиентский код должен единообразно работать и с отдельными объектами, и с группами объектов, не различая их явно через if/проверку типа.
  • Планируется добавлять новые типы узлов (новые виды листьев или составных элементов) без изменения кода, который обходит дерево.

Плюсы

  • Убирает условную логику "это лист или группа?" из клиентского кода.
  • Упрощает добавление новых типов узлов - они просто реализуют существующий интерфейс.
  • Рекурсивная структура хорошо ложится на рекурсивные алгоритмы (подсчёт суммы, поиск, сериализация).

Минусы

  • Общий интерфейс иногда вынуждает добавлять в Leaf методы, которые ему на самом деле не нужны (например, Add(child) для листа без потомков) - либо такие методы бросают исключение, либо интерфейс приходится делить на более узкие части.
  • Если операции над листьями и составными узлами принципиально разные, единый интерфейс превращается в искусственную абстракцию, которая усложняет код больше, чем упрощает.

Пример в .NET Framework / BCL

  • System.Windows.Controls (WPF) и System.Windows.Forms.Control - элементы управления образуют дерево, где контейнер (Panel, GroupBox) хранит другие контролы, а операции вроде Render/Layout рекурсивно применяются ко всему поддереву.
  • System.Linq.Expressions.Expression - дерево выражений, где узлы (BinaryExpression, MethodCallExpression, ...) могут содержать другие узлы того же базового типа Expression.
  • XElement/XmlNode - DOM-подобные деревья, где узел документа может содержать другие узлы.

Пример реализации на C#

Composite.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace DesignPatterns.Structural.Composite
{
    // Общий интерфейс и для листьев (файлов), и для составных узлов (папок) -
    // клиент работает с деревом единообразно, не различая типы узлов.
    public interface IFileSystemEntry
    {
        string Name { get; }
        long GetSize();
        void Print(int indent = 0);
    }

    // Лист - не имеет дочерних элементов
    public sealed class FileEntry : IFileSystemEntry
    {
        public string Name { get; }
        private readonly long _size;

        public FileEntry(string name, long size)
        {
            Name = name;
            _size = size;
        }

        public long GetSize() => _size;

        public void Print(int indent = 0) =>
            Console.WriteLine(new string(' ', indent) + $"- {Name} ({_size} байт)");
    }

    // Составной узел - хранит коллекцию дочерних элементов,
    // которые сами могут быть либо листьями, либо другими составными узлами.
    public sealed class FolderEntry : IFileSystemEntry
    {
        public string Name { get; }
        private readonly List<IFileSystemEntry> _children = new();

        public FolderEntry(string name)
        {
            Name = name;
        }

        public void Add(IFileSystemEntry entry) => _children.Add(entry);

        // Размер папки - рекурсивная сумма размеров всех дочерних элементов.
        // Вызывающему коду не важно, лист это или снова папка.
        public long GetSize() => _children.Sum(c => c.GetSize());

        public void Print(int indent = 0)
        {
            Console.WriteLine(new string(' ', indent) + $"+ {Name}/ ({GetSize()} байт)");
            foreach (var child in _children)
            {
                child.Print(indent + 2);
            }
        }
    }

    public static class Demo
    {
        public static void Run()
        {
            var root = new FolderEntry("project");
            var src = new FolderEntry("src");
            src.Add(new FileEntry("Program.cs", 1200));
            src.Add(new FileEntry("Utils.cs", 800));

            var docs = new FolderEntry("docs");
            docs.Add(new FileEntry("README.md", 500));

            root.Add(src);
            root.Add(docs);
            root.Add(new FileEntry(".gitignore", 50));

            // Клиент вызывает Print/GetSize на корне, не зная заранее,
            // сколько уровней вложенности внутри.
            root.Print();
        }
    }
}

Открыть Composite.cs отдельно Скачать Composite.cs