Э. Гамма, Р. Хелм
Скачать 6.37 Mb.
|
178 Глава 4. Структурные паттерны TreeDisplay (Client, Target) DirectoryTreeDisplay (Adapter) GetChildren(Node) CreateGraphicNode(Node) Display() BuildTree(Node n) GetChildren(Node) CreateGraphicNode(Node) FileSystemEntity (Adaptee) GetChildren(n) Для каждого потомка { AddGraphicNode(CreateGraphicNode(child)) BuildTree(child) } • использование объектов-делегатов. При таком подходе TreeDisplay переадресует запросы на доступ к иерархической структуре объек- ту-делегату. TreeDisplay может реализовывать различные стратегии адаптации, подставляя разных делегатов. Например, предположим, что существует класс DirectoryBrowser , ко- торый использует TreeDisplay DirectoryBrowser может быть делега- том для адаптации TreeDisplay к иерархической структуре каталогов. В динамически типизированных языках вроде Smalltalk или Objective C такой подход требует интерфейса для регистрации делегата в адаптере. Тогда TreeDisplay просто переадресует запросы делегату. В системе NEXTSTEP [Add94] этот подход активно используется для уменьшения числа подклассов. TreeDisplay (Client) SetDelegate(Delegate) Display() BuildTree(Node n) TreeAccessorDelegate (Target) GetChildren(TreeDisplay, Node) CreateGraphicNode(TreeDisplay, Node) DirectoryBrowser(Adapter) GetChildren(TreeDisplay, Node) CreateGraphicNode(TreeDisplay, Node) CreateFile() DeleteFile() delegate>GetChildren(this, n) Для каждого потомка { AddGraphicNode( delete>CreateGraphicNode(this, child) ) BuildTree(child) } FileSystemEntity (Adaptee) delegate Паттерн Adapter (адаптер) 179 В статически типизированных языках вроде C++ требуется явно опреде- лять интерфейс для делегата. Специфицировать такой интерфейс можно, поместив «узкий» интерфейс, который необходим классу TreeDisplay , в абстрактный класс TreeAccessorDelegate . После этого возможно добавить этот интерфейс к выбранному делегату — в данном случае DirectoryBrowser — с помощью наследования. Если у DirectoryBrowser еще нет существующего родительского класса, то используется одиночное наследование, если есть — множественное. Подобное смешивание классов проще, чем добавление нового подкласса TreeDisplay и реализация его операций по отдельности; • параметризованные адаптеры. Обычно в Smalltalk для поддержки сменных адаптеров параметризуют адаптер одним или несколькими блоками. Конструкция блока поддерживает адаптацию без порожде- ния подклассов. Блок может адаптировать запрос, а адаптер может хранить блок для каждого отдельного запроса. В нашем примере это означает, что TreeDisplay хранит один блок для преобразования узла в GraphicNode , а другой — для доступа к потомкам узла. Например, чтобы создать класс TreeDisplay для отображения иерархии каталогов, мы пишем: directoryDisplay := (TreeDisplay on: treeRoot) getChildrenBlock: [:node | node getSubdirectories] createGraphicNodeBlock: [:node | node createGraphicNode]. Если интерфейс адаптации встраивается в класс, то этот способ дает удобную альтернативу подклассам. Пример кода Приведем краткий обзор реализации адаптеров класса и объекта для при- мера, обсуждавшегося в разделе «Мотивация», при этом начнем с классов Shape и TextView : class Shape { public: Shape(); virtual void BoundingBox( Point& bottomLeft, Point& topRight ) const; virtual Manipulator* CreateManipulator() const; 180 Глава 4. Структурные паттерны }; class TextView { public: TextView(); void GetOrigin(Coord& x, Coord& y) const; void GetExtent(Coord& width, Coord& height) const; virtual bool IsEmpty() const; }; Класс Shape предполагает, что ограничивающий фигуру прямоуголь- ник определяется двумя противоположными углами. Напротив, в классе TextView он определяется начальной точкой, высотой и шириной. В классе Shape определена также операция CreateManipulator для создания объекта- манипулятора класса Manipulator , который знает, как анимировать фигуру в ответ на действия пользователя 1 . В TextView эквивалентной операции нет. Класс TextShape является адаптером между двумя этими интерфейсами. Для адаптации интерфейса адаптер класса использует множественное на- следование. Принцип адаптера класса состоит в наследовании интерфейса по одной ветви и реализации — по другой. В C++ интерфейс обычно насле- дуется открыто, а реализация — закрыто. Мы будем придерживаться этого соглашения при определении адаптера TextShape : class TextShape : public Shape, private TextView { public: TextShape(); virtual void BoundingBox( Point& bottomLeft, Point& topRight ) const; virtual bool IsEmpty() const; virtual Manipulator* CreateManipulator() const; }; Операция BoundingBox преобразует интерфейс TextView к интерфейсу Shape : void TextShape::BoundingBox ( Point& bottomLeft, Point& topRight ) const { Coord bottom, left, width, height; GetOrigin(bottom, left); GetExtent(width, height); 1 CreateManipulator — пример фабричного метода. Паттерн Adapter (адаптер) 181 bottomLeft = Point(bottom, left); topRight = Point(bottom + height, left + width); } Операция IsEmpty демонстрирует прямую переадресацию запросов, общих для обоих классов: bool TextShape::IsEmpty () const { return TextView::IsEmpty(); } Наконец, операция CreateManipulator (отсутствующая в классе TextView ) будет определена с нуля. Предположим, класс TextManipulator , который поддерживает манипуляции с TextShape , уже реализован: Manipulator* TextShape::CreateManipulator () const { return new TextManipulator(this); } Адаптер объектов применяет композицию объектов для объединения клас- сов с разными интерфейсами. При таком решении адаптер TextShape содер- жит указатель на TextView : class TextShape : public Shape { public: TextShape(TextView*); virtual void BoundingBox( Point& bottomLeft, Point& topRight ) const; virtual bool IsEmpty() const; virtual Manipulator* CreateManipulator() const; private: TextView* _text; }; Объект TextShape должен инициализировать указатель на экземпляр TextView . Делается это в конструкторе. Кроме того, он должен вызывать операции объекта TextView всякий раз, как вызываются его собственные опе- рации. В этом примере предполагается, что клиент создает объект TextView и передает его конструктору класса TextShape : TextShape::TextShape (TextView* t) { _text = t; } 182 Глава 4. Структурные паттерны void TextShape::BoundingBox ( Point& bottomLeft, Point& topRight ) const { Coord bottom, left, width, height; _text->GetOrigin(bottom, left); _text->GetExtent(width, height); bottomLeft = Point(bottom, left); topRight = Point(bottom + height, left + width); } bool TextShape::IsEmpty () const { return _text->IsEmpty(); } Реализация CreateManipulator не зависит от версии адаптера класса, по- скольку реализована с нуля и не использует повторно никакой функцио- нальности TextView : Manipulator* TextShape::CreateManipulator () const { return new TextManipulator(this); } Сравним этот код с кодом адаптера класса. Для написания адаптера объекта нужно потратить чуть больше усилий, но зато он оказывается более гибким. Например, вариант адаптера объекта TextShape будет прекрасно работать и с подклассами TextView : клиент просто передает экземпляр подкласса TextView конструктору TextShape Известные применения Пример, приведенный в разделе «Мотивация», заимствован из графиче- ского приложения ET++Draw, основанного на каркасе ET++ [WGM88]. ET++Draw повторно использует классы ET++ для редактирования текста, применяя для адаптации класс TextShape В библиотеке InterViews 2.6 определен абстрактный класс Interactor для таких элементов пользовательского интерфейса, как полосы прокрутки, кнопки и меню [VL88]. Есть также абстрактный класс Graphic для структу- рированных графических объектов: прямых, окружностей, многоугольников и сплайнов. И Interactor , и Graphic имеют графическое представление, но у них разные интерфейсы и реализации (общих родительских классов нет), а значит, они несовместимы: нельзя непосредственно вложить структури- рованный графический объект, скажем, в диалоговое окно. Паттерн Adapter (адаптер) 183 Вместо этого InterViews 2.6 определяет адаптер объектов GraphicBlock — подкласс Interactor , который содержит экземпляр Graphic GraphicBlock адаптирует интерфейс класса Graphic к интерфейсу Interactor , позволяет отображать, прокручивать и изменять масштаб экземпляра Graphic внутри структуры класса Interactor Сменные адаптеры широко применяются в системе ObjectWorks\Smalltalk [Par90]. В стандартном Smalltalk определен класс ValueModel для представ- лений, которые отображают единственное значение. Для обращения к значе- нию ValueModel определяет интерфейс value , value: . Эти методы являются абстрактными. Авторы приложений обращаются к значению по имени, более соответствующему предметной области (например, width и width: ), но они не обязаны порождать от ValueModel подклассы для адаптации таких зави- сящих от приложения имен к интерфейсу ValueModel Вместо этого ObjectWorks\Smalltalk включает подкласс ValueModel с именем PluggableAdaptor . Объект этого класса адаптирует другие объекты к интер- фейсу ValueModel ( value, value: ). Его можно параметризовать блоками для получения и установки нужного значения. Внутри PluggableAdaptor эти блоки используются для реализации интерфейса value , value: . Этот класс позволяет также передавать имена селекторов (например, width, width: ) непосредственно для удобства синтаксиса. Такие селекторы преобразуются в соответствующие блоки автоматически. ValueModel value: value PluggableAdaptor value: value getBlock setBlock ^getBlock value: adaptee Object adaptee Еще один пример из ObjectWorks\Smalltalk — это класс TableAdaptor . Он может адаптировать последовательность объектов к табличному пред- ставлению. В таблице отображается по одному объекту в строке. Клиент параметризует TableAdaptor множеством сообщений, которые используются таблицей для получения от объекта значения в колонках. 184 Глава 4. Структурные паттерны В некоторых классах библиотеки NeXT AppKit [Add94] используются объ- екты-делегаты для реализации интерфейса адаптации. В качестве примера можно привести класс NXBrowser, который способен отображать иерар- хические списки данных. NXBrowser пользуется объектом-делегатом для обращений и адаптации данных. Придуманная Скоттом Мейером (Scott Meyer) конструкция «брак по рас- чету» (Marriage of Convenience) [Mey88] это разновидность адаптера класса. Мейер описывает, как класс FixedStack адаптирует реализацию класса Array к интерфейсу класса Stack . Результат представляет собой стек, содержащий фиксированное число элементов. Родственные паттерны Структура паттерна мост (184) аналогична структуре адаптера, но у моста иное назначение. Он отделяет интерфейс от реализации, чтобы то и другое можно было изменять независимо. Адаптер же призван изменить интерфейс существующего объекта. Паттерн декоратор (209) расширяет функциональность объекта, изменяя его интерфейс. Таким образом, декоратор более прозрачен для приложения, чем адаптер. Как следствие, декоратор поддерживает рекурсивную компози- цию, что для «чистых» адаптеров невозможно. Заместитель (246) определяет представителя или суррогат другого объекта, но не изменяет его интерфейс. ПАТТЕРН BRIDGE (МОСТ) Название и классификация паттерна Мост — паттерн, структурирующий объекты. Назначение Отделить абстракцию от ее реализации так, чтобы то и другое можно было изменять независимо. Другие названия Handle/Body (описатель/тело). Паттерн Bridge (мост) 185 Мотивация Если некоторая абстракция может иметь несколько возможных реализаций, то обычно применяют наследование. Абстрактный класс определяет интер- фейс абстракции, а его конкретные подклассы по-разному реализуют его. Но такой подход не всегда обладает достаточной гибкостью. Наследование жестко привязывает реализацию к абстракции, что затрудняет независимую модификацию, расширение и повторное использование абстракции и ее реализации. Рассмотрим реализацию переносимой абстракции окна в библиотеке для разработки пользовательских интерфейсов. Написанные с ее помощью при- ложения должны работать в разных средах, например под X Window System и Presentation Manager (PM) от компании IBM. С помощью наследования можно было бы определить абстрактный класс Window и его подклассы XWindow и PMWindow , реализующие интерфейс окна для разных платформ. Но у такого решения есть два недостатка: абстракцию Window неудобно расширять для новых видов окон или но- вых платформ. Представьте себе подкласс IconWindow , который специ- ализирует абстракцию окна для пиктограмм. Чтобы поддержать пик- тограммы на обеих платформах, нам придется реализовать два новых подкласса XIconWindow и PMIconWindow . Более того, по два подкласса не- обходимо определять для каждого вида окон. А для поддержки третьей платформы придется определять для всех видов окон новый подкласс Window ; Window XWindow PMWindow Window XWindow PMWindow IconWindow XIconWindow PMIconWindow клиентский код становится платформеннозависимым. При создании окна клиент создает экземпляр конкретного класса, имеющего впол- не определенную реализацию. Например, создавая объект XWindow , мы привязываем абстракцию окна к ее реализации для системы X Window; следовательно, код клиента становится ориентированным именно на 186 Глава 4. Структурные паттерны эту оконную систему. В свою очередь, это усложняет перенос клиента на другие платформы. Клиенты должны иметь возможность создавать окно без привязки к кон- кретной реализации. Только сама реализация окна должна зависеть от платформы, на которой работает приложение. Поэтому в клиентском коде экземпляры окон должны создаваться без упоминания конкретных платформ. Паттерн мост решает все эти проблемы: абстракция окна и ее реализация помещаются в раздельные иерархии классов. Таким образом, существует одна иерархия для интерфейсов окон ( Window , IconWindow , TransientWindow ) и другая (с корнем WindowImp ) — для платформеннозависимых реализаций. Так, подкласс XWindowImp предоставляет реализацию для системы X Window System. Window DrawText() DrawRect() WindowImp DevDrawText() DevDrawLine() IconWindow DrawBorder() TransientWindow DrawCloseBox() XWindowImp DevDrawText() DevDrawLine() PMWindowImp DevDrawLine() DevDrawText() imp>DevDrawLine() imp>DevDrawLine() imp>DevDrawLine() imp>DevDrawLine() DrawRect() DrawText() XDrawLine() XDrawString() imp Мост DrawRect() Все операции подклассов Window реализованы в категориях абстрактных операций из интерфейса WindowImp . Это отделяет абстракцию окна от различных ее платформенно-зависимых реализаций. Отношение между классами Window и WindowImp мы будем называть мостом, поскольку между абстракцией и реализацией строится мост, и они могут изменяться неза- висимо. Паттерн Bridge (мост) 187 Применимость Основные условия для применения паттерна мост: требуется избежать постоянной привязки абстракции к реализации. Так, например, бывает, когда реализация должна выбираться во время выполнения программы; и абстракции, и реализации должны расширяться новыми подклассами. В таком случае паттерн мост позволяет комбинировать разные абстрак- ции и реализации и изменять их независимо; изменения в реализации абстракции не должны отражаться на клиен- тах, то есть клиентский код не должен перекомпилироваться; (только для C++) требуется полностью скрыть от клиентов реализа- цию абстракции. В C++ представление класса видимо через его интер- фейс; число классов стремительно разрастается, как на первой диаграмме из раздела «Мотивация». Это признак того, что иерархию следует разде- лить на две части. Для таких иерархий классов Рамбо (Rumbaugh) ис- пользует термин «вложенные обобщения» [RBP+91]; реализация должна совместно использоваться несколькими объектами (например, на базе подсчета ссылок), и этот факт должен быть скрыт от клиента. Простой пример — это разработанный Джеймсом Коплиеном класс String [Cop92], в котором разные объекты могут разделять одно и то же представление строки ( StringRep ). Структура Abstraction Operation() Implementor OperationImp() ConcreteImplementorA OperationImp() ConcreteImplementorB OperationImp() RefinedAbstraction imp>OperationImp(); imp Client 188 Глава 4. Структурные паттерны Участники Abstraction ( Window ) — абстракция: • определяет интерфейс абстракции; • хранит ссылку на объект типа Implementor ; RefinedAbstraction ( IconWindow ) — уточненная абстракция: • расширяет интерфейс, определенный абстракцией Abstraction ; |