Назначение: компонует объекты в древовидные структуры для представления иерархии часть-целое. Позволяет клиентам единообразно трактовать индивидуальные и составные объекты.
Задача. Такие приложения, как графические редакторы, позволяют пользователям строить сложные документы из более простых компонентов. Проектировщик может сгруппировать мелкие компоненты для формирования более крупных, которые, в свою очередь, могут стать основой для создания еще более крупных. В простой реализации допустимо было бы определить классы графических примитивов, например текста и линий, а также классы, выступающие в роли контейнеров для этих примитивов. Но у такого решения есть существенный недостаток. Программа, в которой эти классы используются, должна по-разному обращаться с примитивами и контейнерами, хотя пользователь чаще всего работает с ними единообразно. Необходимость различать эти объекты усложняет приложение.
Решение. Паттерн Компоновщик предлагает применить рекурсивную композицию таким образом, что клиенту не придется проводить различие между простыми и составными объектами.
Ключевым моментом здесь является абстрактный класс, который представляет одновременно и примитивы и контейнеры. В графической системе этот класс может называться 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, то обычно он перенаправляет запрос вложенным в него объектам, возможно, выполняя некоторые дополнительные операции до или после перенаправления.
Применимость: используйте этот паттерн, если нужно представить иерархию объектов вида часть-целое, а также когда необходимо, чтобы клиенты единообразно трактовали составные и примитивные объекты.
Результаты. Паттерн Компоновщик:
- определяет иерархии классов, состоящие из примитивных и составных объектов. Из примитивных объектов можно составлять более сложные, которые, в свою очередь, участвуют в более сложных композициях и так далее. Любой клиент, ожидающий примитивный объект, может работать и с составным;
- упрощает архитектуру клиента. Клиенты могут единообразно работать с индивидуальными и объектами и с составными структурами. Обычно клиенту неизвестно, взаимодействует ли он с листовым или составным объектом. Это упрощает код клиента, поскольку нет необходимости писать функции, ветвящиеся в зависимости от того, с объектом какого класса они работают;
- облегчает добавление новых видов компонентов. Новые подклассы классов Composite или Leaf будут автоматически работать с уже существующими структурами и клиентским кодом. Изменять клиента при добавлении новых компонентов не нужно;
- способствует созданию общего дизайна компонентов. Однако простота добавления новых компонентов имеет и свои отрицательные стороны – становится трудно наложить ограничения на то, какие объекты могут входить в состав композиции. Иногда желательно, чтобы составной объект мог включать только определенные виды компонентов. Паттерн Компоновщик не позволяет воспользоваться для реализации таких ограничений статической системой типов. Вместо этого следует проводить проверки во время выполнения.
Особенности реализации. При реализации паттерна Компоновщик приходится рассматривать много вопросов:
- явные ссылки на владельцев: хранение в компоненте ссылки на своего владельца может упростить обход структуры и управление ею. Наличие такой ссылки облегчает передвижение вверх по структуре и удаление компонента. Кроме того, ссылки на владельцев помогают поддерживать паттерн Цепочка Обязанностей. Обычно ссылку на владельца определяют в классе Component. Классы Leaf и Composite могут унаследовать саму ссылку и операции с ней. При этом важно поддерживать следующий инвариант: если некоторый объект в составной структуре ссылается на другой составной объект как на своего владельца, то для последнего первый является вложенным объектом. Простейший способ гарантировать соблюдение этого условия – изменять владельца компонента только тогда, когда он добавляется или удаляется из составного объекта. Если это удается один раз реализовать в операциях AddItem и RemoveItem, то реализация будет унаследована всеми подклассами, и, значит, инвариант будет поддерживаться автоматически;
- разделение компонентов: часто бывает полезно разделять компоненты, например для уменьшения объема занимаемой памяти. Но если у компонента может быть более одного владельца, то разделение становится проблемой. Возможное решение – позволить компонентам хранить ссылки на нескольких владельцев. Однако в таком случае при распространении запроса по структуре могут возникнуть неоднозначности. Паттерн Приспособленец показывает, как следует изменить дизайн, чтобы вовсе отказаться от хранения подобных указателей. Работает он в тех случаях, когда вложенные объекты могут и не посылать сообщений своим владельцам, вынеся за свои границы часть внутреннего состояния;
- максимизация интерфейса класса Component: одна из целей паттерна Компоновщик – избавить клиентов от необходимости знать, работают ли они с листовым или составным объектом. Для достижения этой цели класс Component должен сделать как можно больше операций общими для классов Composite и Leaf. Обычно класс Component предоставляет для этих операций реализации по умолчанию, а подклассы Composite и Leaf замещают их. Однако иногда эта цель вступает в конфликт с принципом проектирования иерархии классов, согласно которому класс должен определять только логичные для всех его подклассов операции. Класс Component поддерживает много операций, не имеющих смысла для класса Leaf. Однако можно рассматривать Leaf как Component, у которого никогда не бывает вложенных объектов. Тогда мы можем определить в классе Component операцию доступа к вложенным объектам таким образом, что она никогда не будет возвращать объекты. Подклассы Leaf могут использовать эту реализацию по умолчанию, а в подклассах Composite она будет переопределена, чтобы возвращать реальные внутренние объекты составного компонента.
- объявление операций для управления вложенными объектами: хотя в классе Composite реализованы операции AddItem и RemoveItem для добавления и удаления внутренних объектов, но для паттерна Компоновщик важно, в каких классах эти операции объявлены. Надо ли объявлять их в классе 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);
- должен ли Component реализовывать список компонентов: может возникнуть желание определить множество вложенных объектов на уровне класса Component, в котором объявлены операции доступа и управления внутренними объектами. Но размещение указателя на внутренние объекты в базовом классе приводит к непроизводительному расходу памяти во всех листовых узлах, так как у листа вложенных объектов быть не может. Такой прием можно применить, только если в структуре не слишком много вложенных листовых объектов;
- упорядочение внутренних объектов: во многих случаях порядок следования вложенных объектов важен. В рассмотренном выше примере класса Graphic под порядком может пониматься Z-порядок расположения внутренних объектов. В составных объектах, описывающих деревья синтаксического разбора, составные операторы могут быть экземплярами класса Composite, порядок следования внутренних объектов которых отражает семантику программы. Если порядок их следования важен, необходимо учитывать его при проектировании интерфейсов доступа и управления вложенными объектами. В этом может помочь паттерн Итератор;
- кэширование для повышения производительности: если приходится часто обходить композицию или производить в ней поиск, то класс Composite может кэшировать информацию об обходе и поиске. Кэшировать разрешается либо полученные результаты, либо только информацию, достаточную для ускорения обхода или поиска. Например, класс Picture из приведенного в начале примера, мог бы кэшировать охватывающие прямоугольники своих вложенных объектов. При рисовании или выборе эта информация позволила бы пропускать те объекты, которые невидимы в текущем окне. Любое изменение компонента должно делать кэши всех его владельцев недействительными. Поэтому, если вы решите воспользоваться кэшированием, необходимо определить интерфейс, позволяющий уведомить составные объекты о недействительности их кэшей;
- какая структура данных лучше всего подходит для хранения компонентов: составные объекты могут хранить свои вложенные объекты в самых разных структурах данных, включая связанные списки, деревья, массивы и хэш-таблицы. Выбор структуры данных определяется, как всегда, эффективностью. Собственно говоря, вовсе не обязательно пользоваться какой-либо из универсальных структур. Может быть, будет удобно каждый внутренний объект представить в виде отдельной переменной. Правда, для этого каждый подкласс Composite должен будет реализовывать свой собственный интерфейс управления памятью.
Родственные паттерны.
Отношение «компонент–родитель» используется в паттерне Цепочка Обязанностей.
Паттерн Декоратор часто применяется совместно с Компоновщиком. Когда декораторы и компоновщики используются вместе, у них обычно бывает общий родительский класс. Поэтому декораторам придется поддержать интерфейс компонентов такими операциями, как AddItem, RemoveItem и GetItem.
Паттерн Приспособленец позволяет разделять компоненты, но ссылаться на своих владельцев они уже не могут.
Итератор можно использовать для обхода составных объектов.
Посетитель локализует операции и поведение, которые в противном случае пришлось бы распределять между классами Composite и Leaf.