Назначение: компонует объекты в древовидные структуры для представления иерархии часть-целое. Позволяет клиентам единообразно трактовать индивидуальные и составные объекты.

Задача. Такие приложения, как графические редакторы, позволяют пользователям строить сложные документы из более простых компонентов. Проектировщик может сгруппировать мелкие компоненты для формирования более крупных, которые, в свою очередь, могут стать основой для создания еще более крупных. В простой реализации допустимо было бы определить классы графических примитивов, например текста и линий, а также классы, выступающие в роли контейнеров для этих примитивов. Но у такого решения есть существенный недостаток. Программа, в которой эти классы используются, должна по-разному обращаться с примитивами и контейнерами, хотя пользователь чаще всего работает с ними единообразно. Необходимость различать эти объекты усложняет приложение.

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

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

 

 

Подклассы Line, Rectangle и Text определяют примитивные графические объекты. В них операция Draw реализована соответственно для рисования прямых, прямоугольников и текста. Поскольку у примитивных объектов нет вложенных объектов, то ни один из этих подклассов не реализует операции, относящиеся к управлению контейнером.

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

 

 

Общая структура решения.

 

 

Component (Graphic) – компонент: объявляет интерфейс для компонуемых объектов; предоставляет подходящую реализацию операций по умолчанию, общую для всех классов; объявляет интерфейс для управления внутренними объектами; при необходимости реализует интерфейс для доступа к владельцу компонента в рекурсивной структуре.

Leaf (Rectangle, Line, Text, и т.п.) – лист: представляет листовые узлы композиции и не имеет внутренних объектов; определяет поведение примитивных объектов в композиции.

Composite (Picture) – составной объект: определяет поведение компонентов, у которых есть вложенные объекты; хранит внутренние объекты и реализует операции интерфейса класса Component, относящиеся к управлению этими объектами.

Client – клиент:  манипулирует объектами композиции через интерфейс Component.

Клиенты используют интерфейс класса Component для взаимодействия с объектами в составной структуре. Если получателем запроса является листовый объект Leaf, то он и обрабатывает запрос. Когда же получателем является составной объект Composite, то обычно он перенаправляет запрос вложенным в него объектам, возможно, выполняя некоторые дополнительные операции до или после перенаправления.

Применимость: используйте этот паттерн, если нужно представить иерархию объектов вида часть-целое, а также когда необходимо, чтобы клиенты единообразно трактовали составные и примитивные объекты.

Результаты. Паттерн Компоновщик:

Особенности реализации. При реализации паттерна Компоновщик приходится рассматривать много вопросов:

1) если определить интерфейс для управления потомками в корне иерархии классов, то мы добиваемся прозрачности, так как все компоненты удается трактовать единообразно. Однако расплачиваться приходится безопасностью, поскольку клиент может попытаться выполнить бессмысленное действие, например добавить или удалить объект из листового узла;

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

В паттерне Компоновщик особое значение придается прозрачности, а не безопасности. Если для вас важнее безопасность, будьте готовы к тому, что иногда вы можете потерять информацию о типе и придется преобразовывать компонент к типу составного объекта. Как это сделать, не прибегая к небезопасным приведениям типов? Можно, например, объявить в классе Component операцию GetComposite(): Composite. Класс Component реализует ее по умолчанию, возвращая нулевой указатель. А в классе Composite эта операция переопределена и возвращает указатель this на сам объект. Благодаря этой операции GetComposite можно спросить у компонента, является ли он составным. К возвращаемому этой операцией составному объекту допустимо безопасно применять операции AddItem и RemoveItem.

Аналогичные проверки на принадлежность классу Composite можно выполнить, просто проверив соответствие типу. Разумеется, при таком подходе мы не обращаемся со всеми компонентами единообразно, что плохо. Снова приходится проверять тип, перед тем как предпринять то или иное действие.

Единственный способ обеспечить прозрачность – это включить в класс Component реализации операции AddItem и RemoveItem по умолчанию. Но появится новая проблема: нельзя реализовать Component.AddItem() так, чтобы она никогда не приводила к ошибке. Можно, конечно, сделать данную операцию пустой, но тогда нарушается важное проектное ограничение: попытка добавить что-то в листовый объект, скорее всего, свидетельствует об ошибке.

Обычно лучшим решением является такая реализация AddItem и RemoveItem по умолчанию, при которой они завершаются с ошибкой (возможно, возбуждая исключение), если компоненту не разрешено иметь вложенные объекты (для AddItem) или аргумент не является вложенным объектом (для RemoveItem);

Родственные паттерны.

Отношение «компонент–родитель» используется в паттерне Цепочка Обязанностей.

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

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

Итератор можно использовать для обхода составных объектов.

Посетитель локализует операции и поведение, которые в противном случае пришлось бы распределять между классами Composite и Leaf.