Э. Гамма, Р. Хелм
Скачать 6.37 Mb.
|
ГЛАВА 4 СТРУКТУРНЫЕ ПАТТЕРНЫ В структурных паттернах рассматривается вопрос о том, как из классов и объектов образуются более крупные структуры. Структурные паттерны уровня класса используют наследование для составления композиций из интерфейсов и реализаций. Простой пример — использование множествен- ного наследования для объединения нескольких классов в один. В резуль- тате получается класс, обладающий свойствами всех своих родителей. Этот паттерн особенно полезен для организации совместной работы нескольких независимо разработанных библиотек. Другой пример паттерна уровня класса — адаптер (171). В общем случае адаптер делает интерфейс одного класса (адаптируемого) совместимым с интерфейсом другого, обеспечивая тем самым унифицированную абстракцию разнородных интерфейсов. Это достигается за счет закрытого наследования адаптируемому классу. После этого адаптер выражает свой интерфейс в терминах операций адаптируемого класса. Вместо композиции интерфейсов или реализаций структурные паттерны уровня объекта компонуют объекты для получения новой функциональ- ности. Дополнительная гибкость в этом случае связана с возможностью изменить композицию объектов во время выполнения, что недопустимо для статической композиции классов. Примером структурного паттерна уровня объектов является компоновщик (196). Он описывает построение иерархии классов для двух видов объек- тов: примитивных и составных. Последние позволяют создавать структуры произвольной сложности из примитивных и других составных объектов. В паттерне заместитель (246) объект берет на себя функции другого объекта. 170 Глава 4. Структурные паттерны У заместителя есть много применений. Он может действовать как локальный представитель объекта, находящегося в удаленном адресном пространстве; может представлять большой объект, загружаемый по требованию, или ограничивать доступ к критически важному объекту. Заместитель вводит до- полнительный косвенный уровень доступа к отдельным свойствам объекта. Поэтому он может ограничивать, расширять или изменять эти свойства. Паттерн приспособленец (231) определяет структуру для совместного использования объектов. Владельцы совместно используют объекты, по меньшей мере, по двум причинам: для достижения эффективности и непро- тиворечивости. Приспособленец акцентирует внимание на эффективности использования памяти. В приложениях, в которых участвует очень много объектов, должны снижаться накладные расходы на хранение. Значительной экономии можно добиться за счет разделения объектов вместо их дублиро- вания. Но объект может быть разделяемым, только если его состояние не зависит от контекста. У объектов-приспособленцев такой зависимости нет. Любая дополнительная информация передается им по мере необходимости. В отсутствие состояния, зависящего от контекста, объекты-приспособленцы могут легко использоваться совместно. Если паттерн приспособленец дает способ работы с большим числом мелких объектов, то паттерн фасад (221) показывает, как один объект может пред- ставлять целую подсистему. Фасад представляет набор объектов и выполняет свои функции, перенаправляя сообщения объектам, которые он представ- ляет. Паттерн мост (184) отделяет абстракцию объекта от его реализации, так что их можно изменять независимо. Паттерн декоратор (209) описывает динамическое добавление объектам но- вых обязанностей. Это структурный паттерн, который рекурсивно компонует объекты с целью реализации заранее неизвестного числа дополнительных функций. Например, объект-декоратор, содержащий некоторый элемент пользовательского интерфейса, может добавить к нему оформление в виде рамки или тени либо новую функциональность, например возможность прокрутки или изменения масштаба. Два разных оформления прибавляются путем простого вложения одного декоратора в другой. Для достижения этой цели каждый объект-декоратор должен соблюдать интерфейс своего компо- нента и перенаправлять ему сообщения. Свои функции (скажем, рисование рамки вокруг компонента) декоратор может выполнять как до, так и после перенаправления сообщения. Многие структурные паттерны в той или иной мере связаны друг с другом. Эти отношения обсуждаются в конце главы. Паттерн Adapter (адаптер) 171 ПАТТЕРН ADAPTER (АДАПТЕР) Название и классификация паттерна Адаптер — паттерн, структурирующий классы и объекты. Назначение Преобразует интерфейс одного класса в другой интерфейс, на который рассчитаны клиенты. Адаптер обеспечивает совместную работу классов с несовместимыми интерфейсами, которая без него была бы невозможна. Другие названия Wrapper (обертка). Мотивация Иногда класс из инструментальной библиотеки, спроектированный для повторного использования, не удается использовать только потому, что его интерфейс не соответствует тому, который нужен конкретному при- ложению. Рассмотрим, например, графический редактор, благодаря которому поль- зователи могут рисовать на экране графические элементы (линии, много- угольники, текст и т. д.) и организовывать их в виде картинок и диаграмм. Основной абстракцией графического редактора является графический объект, форма которого может редактироваться пользователем и который умеет выполнять прорисовку самого себя. Интерфейс графических объек- тов определен абстрактным классом Shape . Редактор определяет подкласс класса Shape для каждого вида графических объектов: LineShape для прямых, PolygonShape для многоугольников и т. д. Классы для элементарных геометрических фигур, например LineShape и PolygonShape , реализуются сравнительно просто, поскольку заложенные в них возможности рисования и редактирования ограничены по своей приро- де. Но подкласс TextShape , умеющий отображать и редактировать текст, уже значительно сложнее, поскольку даже для простейших операций редактиро- вания текста нужно нетривиальным образом обновлять экран и управлять буферами. В то же время, возможно, существует уже готовая библиотека для разработки пользовательских интерфейсов, которая предоставляет хорошо проработанный класс TextView , позволяющий отображать и редактировать текст. В идеале мы хотели бы повторно использовать TextView для реали- 172 Глава 4. Структурные паттерны зации TextShape , но библиотека разрабатывалась без учета классов Shape , поэтому использовать TextView вместо Shape не удастся. Так каким же образом существующие и независимо разработанные классы вроде TextView могут работать в приложении, спроектированном под дру- гой, несовместимый интерфейс? Можно было бы так изменить интерфейс класса TextView , чтобы он соответствовал интерфейсу Shape , но для этого понадобится исходный код. Впрочем, если он доступен, то вряд ли будет разумно изменять TextView ; библиотека не должна приспосабливаться к интерфейсам каждого конкретного приложения только для того, чтобы приложение заработало. Вместо этого можно было бы определить класс TextShape так, что он будет адаптировать интерфейс TextView к интерфейсу Shape . Это можно сделать двумя способами: (1) наследованием интерфейса от Shape , а реализации от TextView ; (2) включением экземпляра TextView в TextShape и реализацией TextShape в категориях интерфейса TextView . Два данных подхода соответ- ствуют вариантам паттерна адаптер в его классовой и объектной ипостасях. Класс TextShape мы будем называть адаптером. DrawingEditor Shape BoundingBox() CreateManipulator() Line BoundingBox() CreateManipulator() TextShape BoundingBox() CreateManipulator() TextView GetExtent() return text >GetExtent() return new TextManipulator text На этой схеме показан адаптер объекта. Видно, как запрос BoundingBox , объявленный в классе Shape , преобразуется в запрос GetExtent , определен- ный в классе TextView . Поскольку класс TextShape адаптирует TextView к интерфейсу Shape , графический редактор может воспользоваться классом TextView , хотя тот и имеет несовместимый интерфейс. Часто адаптер отвечает за функциональность, которую не может предоста- вить адаптируемый класс. На схеме показано, как адаптер реализует такого рода обязанности. У пользователя должна быть возможность перемещать Паттерн Adapter (адаптер) 173 любой объект класса Shape в другое место, но в классе TextView такая опе- рация не предусмотрена. TextShape может добавить недостающую функцио- нальность, самостоятельно реализовав операцию CreateManipulator класса Shape , которая возвращает экземпляр подходящего подкласса Manipulator Manipulator — это абстрактный класс для объектов, которые умеют аними- ровать Shape в ответ на такие действия пользователя, как перетаскивание фигуры в другое место. У класса Manipulator имеются подклассы для различных фигур. Например, TextManipulator — подкласс для TextShape Возвращая экземпляр TextManipulator , объект класса TextShape добавляет новую функциональность, которой в классе TextView нет, а классу Shape требуется. Применимость Основные условия для применения паттерна адаптер: вы хотите использовать существующий класс, но его интерфейс не соот- ветствует вашим потребностям; требуется создать повторно используемый класс, который должен взаи- модействовать с заранее неизвестными или не связанными с ним клас- сами, имеющими несовместимые интерфейсы; (только для адаптера объектов!) нужно использовать несколько суще- ствующих подклассов, но непрактично адаптировать их интерфейсы путем порождения новых подклассов от каждого. В этом случае адаптер объектов может приспосабливать интерфейс их общего родительского класса. Структура Адаптер класса использует множественное наследование для адаптации одного интерфейса к другому. Client Target Request() Adapter Request() Adaptee SpecificRequest() (реализация) SpecificRequest() 174 Глава 4. Структурные паттерны Адаптер объекта применяет композицию объектов. Client Target Request() Adaptee SpecificRequest() Adapter Request() adaptee>SpecificRequest() adaptee Участники Target ( Shape ) — целевой: • определяет зависящий от предметной области интерфейс, которым пользуется Client ; Client ( DrawingEditor ) — клиент: • вступает во взаимоотношения с объектами, удовлетворяющими ин- терфейсу Target ; Adaptee ( TextView ) — адаптируемый: • определяет существующий интерфейс, который нуждается в адапта- ции; Adapter ( TextShape ) — адаптер: • адаптирует интерфейс Adaptee к интерфейсу Target Отношения Клиенты вызывают операции экземпляра адаптера Adapter . В свою очередь адаптер вызывает операции адаптируемого объекта или класса Adaptee , который и выполняет запрос. Результаты Адаптеры объектов и классов обладают разными достоинствами и недо- статками. Адаптер класса: адаптирует Adaptee к Target , перепоручая действия конкретному классу Adaptee . Поэтому данный паттерн не будет работать, если мы захотим одновременно адаптировать класс и его подклассы; Паттерн Adapter (адаптер) 175 позволяет адаптеру Adapter заместить некоторые операции адаптиру- емого класса Adaptee , так как Adapter есть не что иное, как подкласс Adaptee ; вводит только один новый объект. Чтобы добраться до адаптируемого класса, не нужно никакого дополнительного обращения по указателю. Адаптер объектов: позволяет одному адаптеру Adapter работать со многим адаптируемы- ми объектами Adaptee , то есть с самим Adaptee и его подклассами (если таковые имеются). Адаптер может добавить новую функциональность сразу всем адаптируемым объектам; затрудняет замещение операций класса Adaptee . Для этого потребует- ся породить от Adaptee подкласс и заставить Adapter ссылаться на этот подкласс, а не на сам Adaptee Ниже перечислены другие аспекты, которые следует рассмотреть, принимая решение о применении паттерна адаптер: объем работы по адаптации. Адаптеры сильно отличаются по объему работы, необходимой для адаптации интерфейса Adaptee к интерфейсу Target . Это может быть как простейшее преобразование (например, из- менение имен операций), так и поддержка совершенно другого набора операций. Объем работы зависит от того, насколько сильно отличаются друг от друга интерфейсы целевого и адаптируемого классов; сменные адаптеры. Степень повторной используемости класса тем выше, чем меньше предположений делается о тех классах, которые бу- дут его применять. Встраивая адаптацию интерфейса в класс, вы сни- маете предположение о том, что другие классы должны «видеть» тот же интерфейс. Другими словами, адаптация интерфейса позволяет вклю- чить ваш класс в существующие системы, которые спроектированы для класса с другим интерфейсом. В системе ObjectWorks\Smalltalk [Par90] используется термин сменный адаптер (pluggable adapter) для обозна- чения классов со встроенной адаптацией интерфейса. Рассмотрим виджет TreeDisplay , позволяющий графически отображать древовидные структуры. Если бы это был специализированный виджет, предназначенный только для одного приложения, то мы могли бы по- требовать специального интерфейса от объектов, которые он отображает (например, чтобы все они происходили от абстрактного класса Tree ). Но если мы хотим сделать его повторно используемым (например, частью библиотеки полезных виджетов), то устанавливать такое требование не- 176 Глава 4. Структурные паттерны разумно. Скорее всего, разные приложения будут определять собственные классы для представления древовидных структур, и не следует заставлять их пользоваться именно нашим абстрактным классом Tree . А у разных структур деревьев будут и разные интерфейсы. Например, в иерархии каталогов добраться до потомков удастся с помо- щью операции GetSubdirectories , тогда как для иерархии наследования соответствующая операция может называться GetSubclasses . Повторно используемый виджет TreeDisplay должен «уметь» отображать иерархии обоих видов, даже если у них разные интерфейсы. Другими словами, в TreeDisplay должна быть встроена возможность адаптации интерфей- сов. О способах встраивания адаптации интерфейсов в классы говорится в разделе «Реализация»; использование двусторонних адаптеров для обеспечения прозрачности. Потенциальный недостаток адаптеров заключается в том, что они не- прозрачны для всех клиентов. Адаптированный объект уже не обладает интерфейсом Adaptee , так что его нельзя использовать там, где Adaptee был применим. Двусторонние адаптеры способны обеспечить такую прозрачность. А конкретнее, они полезны в тех случаях, когда разные клиенты должны видеть объект по-разному. Рассмотрим двусторонний адаптер, который интегрирует каркас гра- фических редакторов Unidraw [VL90] и библиотеку для разрешения ограничений QOCA [HHMV92]. В обеих системах есть классы, явно представляющие переменные: в Unidraw это StateVariable , а в QOCA — ConstraintVariable . Чтобы заставить Unidraw работать совместно с QOCA, ConstraintVariable нужно адаптировать к StateVariable . А для того чтобы решения QOCA распространялись на Unidraw, StateVariable следует адаптировать к ConstraintVariable ConstraintVariable ConstraintStateVariable StateVariable (к иерархии классов QOCA) (к иерархии классов Unidraw) Паттерн Adapter (адаптер) 177 Здесь применен двусторонний адаптер класса ConstraintStateVariable , который является подклассом одновременно S t a t e V a r i a b l e и ConstraintVariable и адаптирует оба интерфейса друг к другу. Мно- жественное наследование в данном случае вполне приемлемо, посколь- ку интерфейсы адаптированных классов существенно различаются. Двусторонний адаптер класса соответствует интерфейсам каждого из адаптируемых классов и может работать в любой системе. Реализация Хотя реализация адаптера обычно не вызывает затруднений, все же необхо- димо учитывать ряд аспектов: реализация адаптеров классов в C++. В C++ реализация адаптера клас- са Adapter открыто наследует от класса Target и закрыто — от Adaptee Таким образом, Adapter должен быть подтипом Target , но не Adaptee ; сменные адаптеры. Рассмотрим три способа реализации сменных адап- теров для описанного выше виджета TreeDisplay , который может авто- матически отображать иерархические структуры. Первый шаг, общий для всех трех реализаций, — поиск «узкого» ин- терфейса для Adaptee , то есть наименьшего подмножества операций, позволяющего выполнить адаптацию. «Узкий» интерфейс, состоящий всего из пары итераций, легче адаптировать, чем интерфейс из несколь- ких десятков операций. Для TreeDisplay адаптации подлежит любая иерархическая структура. Минимальный интерфейс мог бы включать всего две операции: одна определяет графическое представление узла в иерархической структуре, другая — доступ к потомкам узла. «Узкий» интерфейс приводит к трем подходам к реализации: • использование абстрактных операций. Определим в классе TreeDisplay абстрактные операции, которые соответствуют «узко- му» интерфейсу класса Adaptee . Подклассы должны реализовывать эти абстрактные операции и адаптировать иерархически структу- рированный объект. Например, подкласс DirectoryTreeDisplay при их реализации будет осуществлять доступ к структуре каталогов файловой системы. DirectoryTreeDisplay специализирует узкий интерфейс таким образом, чтобы он мог отображать структуру каталогов, составленную из объектов FileSystemEntity ; |