Э. Гамма, Р. Хелм
Скачать 6.37 Mb.
|
aPicture aText aLine aRectangle aLine aRectangle Применимость Основные условия для применения паттерна компоновщик: требуется представить иерархию объектов вида «часть — целое»; клиенты должны по единым правилам работать с составными и индиви- дуальными объектами. Структура Component Operation() Add(Component) Remove(Component) GetChild(int) Leaf Composite Operation() Operation() Add(Component) Remove(Component) GetChild(int) Для всех потомков g g.Operation(); Client Потомки Паттерн Composite (компоновщик) 199 Структура типичного составного объекта могла бы выглядеть так: aComposite aComposite aLeaf aLeaf aLeaf aLeaf aLeaf aLeaf Участники Component ( Graphic ) — компонент: • объявляет интерфейс для компонуемых объектов; • реализует поведение по умолчанию для интерфейсов, общих для всех классов; • объявляет интерфейс для обращения к потомкам и управления ими; • (не обязательно) определяет интерфейс для обращения к родителю ком- понента в рекурсивной структуре и при необходимости реализует его; Leaf ( Rectangle , Line , Text и т. п.) — лист: • представляет листовые узлы композиции и не имеет потомков; • определяет поведение примитивных объектов в композиции; Composite ( Picture ) — составной объект: • определяет поведение компонентов, у которых есть потомки; • хранит компоненты-потомки; • реализует относящиеся к управлению потомками операции в интер- фейсе класса Component ; Client — клиент: • манипулирует объектами композиции через интерфейс Component Отношения Клиенты используют интерфейс класса Component для взаимодействия с объектами в составной структуре. Если получателем запроса является 200 Глава 4. Структурные паттерны листовый объект Leaf , то он и обрабатывает запрос. Когда же получателем является составной объект Composite , то обычно он перенаправляет запрос своим потомкам — возможно, с выполнением некоторых дополнительных операций до или после перенаправления. Результаты Паттерн компоновщик: определяет иерархии классов, состоящие из примитивных и составных объектов. Из примитивных объектов можно составлять более сложные, которые, в свою очередь, участвуют в более сложных композициях и так далее. Любой клиент, ожидающий получить примитивный объект, мо- жет работать и с составным; упрощает архитектуру клиента. Клиенты могут единообразно рабо- тать с индивидуальными и объектами и с составными структурами. Обычно клиенту неизвестно, взаимодействует ли он с листовым или составным объектом. Это упрощает код клиента, поскольку нет необхо- димости писать функции, ветвящиеся в зависимости от того, с объектом какого класса они работают; облегчает добавление новых видов компонентов. Новые подклассы клас- сов Composite или Leaf будут автоматически работать с уже существую- щими структурами и клиентским кодом. Изменять клиент при добавле- нии новых компонентов не нужно; способствует созданию общего дизайна. Впрочем, такая простота до- бавления новых компонентов имеет и свои отрицательные стороны: становится трудно установить ограничения на то, какие объекты могут входить в состав композиции. Иногда бывает нужно, чтобы составной объект мог включать только определенные виды компонентов. Паттерн компоновщик не позволяет воспользоваться для реализации таких огра- ничений статической системой типов. Вместо этого приходится прово- дить проверки во время выполнения. Реализация При реализации паттерна компоновщик приходится учитывать целый ряд аспектов: явные ссылки на родителей. Хранение в компоненте ссылки на своего родителя может упростить обход структуры и управление ею. Наличие такой ссылки облегчает передвижение вверх по структуре и удаление Паттерн Composite (компоновщик) 201 компонента. Кроме того, ссылки на родителей помогают реализовать паттерн цепочка обязанностей (263). Обычно ссылку на родителя определяют в классе Component . Классы Leaf и Composite могут наследовать саму ссылку и операции с ней. При наличии ссылки на родителя важно поддерживать следующий инвариант: для всех потомков в составном объекте родителем является составной объект, для которого они в свою очередь являются потомками. Простейший способ гарантировать соблюдение этого условия — из- менять родителя компонента только тогда, когда он добавляется или удаляется из составного объекта. Если это удается один раз реализовать в операциях Add и Remove , то реализация будет унаследована всеми подклассами, а следовательно, инвариант будет поддерживаться авто- матически; совместное использование компонентов. Часто бывает полезно органи- зовать совместное использование компонентов — например, для умень- шения объема занимаемой памяти. Но если у компонента может быть более одного родителя, то совместное использование становится про- блемой. Возможное решение — позволить компонентам хранить ссылки на не- скольких родителей. Однако в таком случае при распространении запроса по структуре могут возникнуть неоднозначности. Паттерн приспособле- нец (231) показывает, как следует изменить дизайн, чтобы вовсе отка- заться от хранения родителей. Работает он в тех случаях, когда потомки могут избежать отправки сообщений своим родителям, вынеся за свои границы часть внутреннего состояния; максимизация интерфейса класса Component. Одна из целей паттерна компоновщик — избавить клиентов от необходимости знать, работают ли они с листовым или составным объектом. Для достижения этой цели класс Component должен сделать как можно больше операций общими для классов Composite и Leaf . Обычно класс Component предоставляет для этих операций реализации по умолчанию, а подклассы Composite и Leaf замещают их. Однако иногда эта цель вступает в конфликт с принципом проектиро- вания иерархии классов, согласно которому класс должен определять только логичные для всех его подклассов операции. Класс Component поддерживает много операций, не имеющих смысла для класса Leaf . Как же тогда предоставить для них реализацию по умолчанию? 202 Глава 4. Структурные паттерны Иногда некоторая изобретательность позволяет перенести в класс Component операцию, которая, на первый взгляд, имеет смысл только для составных объектов. Например, интерфейс для обращений к по- томкам является фундаментальной частью класса Composite , но вовсе не обязательно класса Leaf . Однако если рассматривать Leaf как Component , у которого никогда не бывает потомков, то в классе Component можно опре- делить операцию обращения к потомкам как никогда не возвращающую потомков. Тогда подклассы Leaf могут использовать эту реализацию по умолчанию, а в подклассах Composite она будет переопределена, чтобы возвращать потомков. Операции управления потомками создают немало проблем; они будут рассмотрены в следующем разделе; объявление операций для управления потомками. Хотя в классе Composite реализованы операции Add и Remove для добавления и удаления по- томков, но для паттерна компоновщик важно, в каких классах эти опе- рации объявлены. Надо ли объявлять их в классе Component и тем самым делать доступными в Leaf , или их следует объявить и определить только в классе Composite и его подклассах? Ответ на этот вопрос подразумевает компромисс между безопасностью и прозрачностью: • если определить интерфейс для управления потомками в корне ие- рархии классов, мы добиваемся прозрачности, так как все компонен- ты удается трактовать единообразно. Однако за это расплачиваться приходится безопасностью, поскольку клиент может попытаться выполнить бессмысленное действие вроде добавления или удаления объекта из листового узла; • если управление потомками определяется в классе Composite , то безопасность будет обеспечена — ведь любая попытка добавить или удалить объекты из листьев в статически типизированном языке вроде C++ будет перехвачена на этапе компиляции. Но прозрачность при этом теряется, так как листовые и составные объекты обладают разными интерфейсами. В паттерне компоновщик мы придаем особое значение прозрачности, а не безопасности. Если для вас важнее безопасность, будьте готовы к тому, что в некоторых случаях вы можете потерять информацию о типе, и ком- понент придется преобразовывать к типу составного объекта. Как это сделать, не прибегая к небезопасным приведениям типов? Паттерн Composite (компоновщик) 203 Можно, например, объявить в классе Component операцию Composite* GetComposite() . Класс Component реализует ее по умолчанию, возвращая null -указатель. А в классе Composite эта операция переопределена, чтобы она возвращала текущий объект в виде указателя this : class Composite; class Component { public: // ... virtual Composite* GetComposite() { return 0; } }; class Composite : public Component { public: void Add(Component*); // ... virtual Composite* GetComposite() { return this; } }; class Leaf : public Component { // ... }; Благодаря операции GetComposite можно спросить у компонента, являет- ся ли он составным. К возвращаемому этой операцией составному объекту допустимо безопасно применять операции Add и Remove : Composite* aComposite = new Composite; Leaf* aLeaf = new Leaf; Component* aComponent; Composite* test; aComponent = aComposite; if (test = aComponent->GetComposite()) { test->Add(new Leaf); } aComponent = aLeaf; if (test = aComponent->GetComposite()) { test->Add(new Leaf); // лист не добавляется } Аналогичные проверки на принадлежность классу Composite в C++ вы- полняются с помощью оператора dynamic_cast 204 Глава 4. Структурные паттерны Разумеется, недостаток такого подхода заключается в том, что мы не обращаемся со всеми компонентами единообразно. Снова приходится проверять тип, перед тем как предпринять то или иное действие. Единственный способ обеспечить прозрачность — это включить в класс Component реализации операций Add и Remove по умолчанию. Но тогда появится новая проблема: нельзя реализовать Component::Add так, чтобы не появилась возможность ошибки. Можно, конечно, сделать данную операцию пустой, но тогда нарушается важное проектное ограничение: попытка добавить что-то в листовый объект, скорее всего, свидетельствует об ошибке. Допустимо было бы заставить ее удалять свой аргумент, но такое поведение может оказаться неожиданным для клиента. Обычно лучшим решением является такая реализация Add и Remove по умолчанию, при которой они завершаются с ошибкой (возможно, воз- буждая исключение), если компоненту не разрешено иметь потомков (для Add ) или аргумент не является чьим-либо потомком (для Remove ). Другая возможность — слегка изменить семантику операции «удаление». Если компонент хранит ссылку на родителя, то можно было бы считать, что Component::Remove удаляет самого себя. Тем не менее, для операции Add по-прежнему нет разумной интерпретации; должен ли Component реализовывать список компонентов? Возможно, вам захочется определить множество потомков в виде переменной эк- земпляра класса Component , в котором объявлены операции обращения к потомкам и управления ими. Но размещение указателя на потомков в базовом классе создает лишние затраты памяти во всех листовых уз- лах, хотя у листа потомков быть не может. Такое решение может ис- пользоваться только в том случае, если в структуре относительно мало потомков; упорядочение потомков. Во многих случаях важен порядок следования потомков составного объекта. В рассмотренном выше примере клас- са Graphic под порядком может пониматься Z-порядок расположения потомков. В составных объектах, описывающих деревья синтаксиче- ского разбора, составные операторы могут быть экземплярами класса Composite , порядок следования потомков которых отражает семантику программы. Если порядок следования потомков важен, необходимо учитывать его при проектировании интерфейсов доступа и управления потомками. В этом может помочь паттерн итератор (302); Паттерн Composite (компоновщик) 205 кэширование для повышения производительности. Если приходится часто выполнять обход или поиск в композициях, то класс Composite может кэшировать либо непосредственно полученные результаты, либо только информацию, достаточную для ускорения обхода или поиска. Например, класс Picture из примера, приведенного в разделе «Моти- вация», мог бы кэшировать охватывающие прямоугольники своих по- томков. При рисовании или выделении эта информация позволила бы пропускать тех потомков, которые не видимы в текущем окне. При любом изменении компонента кэшированная информация всех его родителей должна становиться недействительной. Наиболее эффективен такой подход в случае, когда компонентам известно об их родителях. Поэтому, если вы решите воспользоваться кэшированием, необходимо определить интерфейс, позволяющий уведомить составные объекты о не- действительности содержимого их кэшей; кто должен удалять компоненты? В языках, где нет уборщика мусора, лучше всего поручить классу Composite удалять своих потомков в мо- мент уничтожения. Исключением из этого правила является случай, когда листовые объекты постоянны и, следовательно, могут использо- ваться совместно; выбор структуры данных для хранения компонентов. Составные объ- екты могут хранить своих потомков в самых разных структурах дан- ных, включая связанные списки, деревья, массивы и хеш-таблицы. Выбор структуры данных определяется, как всегда, эффективностью. Собственно говоря, вовсе не обязательно пользоваться какой-либо из универсальных структур. Иногда в составных объектах каждый пото- мок представляется отдельной переменной. Правда, для этого каждый подкласс Composite должен реализовывать свой собственный интер- фейс управления памятью. См. пример в описании паттерна интерпре- татор. Пример кода Такие изделия, как компьютеры и стереосистемы, часто имеют иерархи- ческую структуру. Например, в раме монтируются дисковые накопители и плоские электронные платы, к шине подсоединяются различные карты, а корпус содержит раму, шины и т. д. Подобные структуры моделируются с помощью паттерна компоновщик. Класс Equipment определяет интерфейс для всех видов аппаратуры в иерар- хии вида «часть — целое»: 206 Глава 4. Структурные паттерны class Equipment { public: virtual Equipment(); const char* Name() { return _name; } virtual Watt Power(); virtual Currency NetPrice(); virtual Currency DiscountPrice(); virtual void Add(Equipment*); virtual void Remove(Equipment*); virtual Iterator* CreateIterator(); protected: Equipment(const char*); private: const char* _name; }; В классе Equipment объявлены операции, которые возвращают атрибуты аппаратного блока, например энергопотребление и стоимость. Подклас- сы реализуют эти операции для конкретных видов оборудования. Класс Equipment объявляет также операцию CreateIterator , возвращающую итератор Iterator (см. приложение В) для обращения к отдельным частям. Реализация этой операции по умолчанию возвращает итератор NullIterator , умеющий обходить только пустое множество. Среди подклассов Equipment могут быть листовые классы, представляющие дисковые накопители, микросхемы и переключатели: class FloppyDisk : public Equipment { public: FloppyDisk(const char*); virtual FloppyDisk(); virtual Watt Power(); virtual Currency NetPrice(); virtual Currency DiscountPrice(); }; CompositeEquipment — это базовый класс для оборудования, содержащего другое оборудование. Одновременно это подкласс класса Equipment : class CompositeEquipment : public Equipment { public: virtual CompositeEquipment(); virtual Watt Power(); virtual Currency NetPrice(); Паттерн Composite (компоновщик) 207 virtual Currency DiscountPrice(); virtual void Add(Equipment*); virtual void Remove(Equipment*); virtual Iterator* CreateIterator(); protected: CompositeEquipment(const char*); private: List _equipment; }; CompositeEquipment определяет операции для доступа и управления вну- тренними аппаратными блоками. Операции Add и Remove добавляют и уда- ляют оборудование из списка, хранящегося в переменной _equipment Операция CreateIterator возвращает итератор (точнее, экземпляр класса ListIterator ), который будет обходить этот список. Реализация по умолчанию операции NetPrice могла бы использовать CreateIterator для суммирования цен на отдельные блоки 1 : Currency CompositeEquipment::NetPrice () { Iterator* i = CreateIterator(); Currency total = 0; for (i->First(); !i->IsDone(); i->Next()) { total += i->CurrentItem()->NetPrice(); } delete i; return total; } Теперь мы можем представить аппаратный блок компьютера в виде под- класса к CompositeEquipment под названием Chassis Chassis наследует порожденные операции класса CompositeEquipment class Chassis : public CompositeEquipment { public: Chassis(const char*); virtual Chassis(); virtual Watt Power(); virtual Currency NetPrice(); virtual Currency DiscountPrice(); }; 1 Очень легко забыть об удалении итератора после завершения работы с ним. В описании паттерна итератор рассказано, как защититься от таких ошибок. |