Э. Гамма, Р. Хелм
Скачать 6.37 Mb.
|
Implementor ( WindowImp ) — реализатор: • определяет интерфейс для классов реализации. Он не обязан точно соответствовать интерфейсу класса Abstraction . На самом деле оба интерфейса могут быть совершенно различны. Обычно интерфейс класса Implementor предоставляет только примитивные операции, а класс Abstraction определяет операции более высокого уровня, основанные на этих примитивах; ConcreteImplementor ( XWindowImp , PMWindowImp ) — конкретный реализатор: • реализует интерфейс класса Implementor и определяет его конкретную реализацию. Отношения Объект Abstraction перенаправляет запросы клиента своему объекту Implementor Результаты Результаты применения паттерна мост: отделение реализации от интерфейса. Реализация больше не имеет по- стоянной привязки к интерфейсу. Реализация абстракции может на- страиваться во время выполнения. Объект может даже динамически изменять свою реализацию. Разделение классов Abstraction и Implementor устраняет также зависимо- сти от реализации, устанавливаемые на этапе компиляции. Чтобы изменить класс реализации, не обязательно перекомпилировать класс Abstraction и его клиентов. Это свойство особенно важно, если необходимо обеспечить двоичную совместимость между разными версиями библиотеки классов. Кроме того, такое разделение облегчает разбиение системы на слои и тем самым позволяет улучшить ее структуру. Высокоуровневые части систе- мы должны знать только о классах Abstraction и Implementor ; Паттерн Bridge (мост) 189 повышение степени расширяемости. Иерархии классов Abstraction и Implementor могут расширяться независимо; сокрытие деталей реализации от клиентов. Клиентов можно изолиро- вать от таких подробностей реализации, как совместное использование объектов класса Implementor и сопутствующего механизма подсчета ссылок. Реализация Если вы намереваетесь применить паттерн мост, то подумайте о таких аспек- тах реализации: только один класс Implementor. В ситуациях, когда есть только одна реа- лизация, создавать абстрактный класс Implementor необязательно. Это вырожденный случай паттерна мост — между классами Abstraction и Implementor существует взаимно однозначное соответствие. Тем не менее разделение все же полезно, если изменение реализации класса не должно отражаться на существующих клиентах (должно быть доста- точно заново скомпоновать программу, не перекомпилируя клиентский код). Для описания такого разделения Каролан (Carolan) [Car89] употребля- ет сочетание «чеширский кот». В C++ интерфейс класса Implementor можно определить в закрытом заголовочном файле, который не пере- дается клиентам. Это позволяет полностью скрыть реализацию класса от клиентов; создание правильного объекта Implementor. Как, когда и где принимается решение о том, экземпляр какого из классов Implementor следует соз- дать, если таких классов несколько? Если класс Abstraction располагает полной информацией обо всех классах ConcreteImplementor , то он может создать один из них в своем конструкторе; какой именно — зависит от переданных конструктору пара- метров. Так, если класс коллекции поддерживает несколько реализаций, то решение может приниматься в зависимости от размера коллекции. Для небольших коллекций применяется реализация в виде связанного списка, для больших — в виде хеш-таблиц. Другой подход — заранее выбрать реализацию по умолчанию, а позже изменять ее в соответствии с тем, как она используется. Например, если число элементов в коллекции превышает некоторую условную величину, то мы переключаемся с одной реализации на другую, более эффективную. 190 Глава 4. Структурные паттерны Также можно делегировать решение другому объекту. В примере с ие- рархиями Window / WindowImp уместно было бы ввести фабричный объект (см. паттерн абстрактная фабрика (113)), единственная задача которо- го — инкапсулировать платформенную специфику. Фабрика обладает информацией, объекты WindowImp какого вида надо создавать для данной платформы, а объект Window просто обращается к ней с запросом о предо- ставлении какого-нибудь объекта WindowImp и получает то, что нужно. Преимущество описанного подхода: класс Abstraction напрямую не привязан ни к одному из классов Implementor ; совместное использование реализаторов. Джеймс Коплиен показал, как в C++ можно применить идиому «описатель/тело», чтобы нескольки- ми объектами могла совместно использоваться одна и та же реализа- ция [Cop92]. В теле хранится счетчик ссылок, который увеличивается и уменьшается в классе описателя. Код для присваивания значений описателям, совместно использующим одно тело, в общем виде выгля- дит так: Handle& Handle::operator= (const Handle& other) { other._body->Ref(); _body->Unref(); if (_body->RefCount() == 0) { delete _body; } _body = other._body; return *this; } применение множественного наследования. В C++ для объединения ин- терфейса с его реализацией можно воспользоваться множественным наследованием [Mar91]. Например, класс может открыто наследовать классу Abstraction и закрыто — классу ConcreteImplementor . Но такое решение зависит от статического наследования и жестко привязыва- ет реализацию к ее интерфейсу. Поэтому реализовать настоящий мост с помощью множественного наследования невозможно — по крайней мере в C++. Пример кода В следующем коде на C++ реализован пример Window / WindowImp , который обсуждался в разделе «Мотивация». Класс Window определяет абстракцию окна для клиентских приложений: Паттерн Bridge (мост) 191 class Window { public: Window(View* contents); // запросы, обрабатываемые окном virtual void DrawContents(); virtual void Open(); virtual void Close(); virtual void Iconify(); virtual void Deiconify(); // запросы, перенаправляемые реализации virtual void SetOrigin(const Point& at); virtual void SetExtent(const Point& extent); virtual void Raise(); virtual void Lower(); virtual void DrawLine(const Point&, const Point&); virtual void DrawRect(const Point&, const Point&); virtual void DrawPolygon(const Point[], int n); virtual void DrawText(const char*, const Point&); protected: WindowImp* GetWindowImp(); View* GetView(); private: WindowImp* _imp; View* _contents; // содержимое окна }; В классе Window хранится ссылка на WindowImp — абстрактный класс, в ко- тором объявлен интерфейс к данной оконной системе: class WindowImp { public: virtual void ImpTop() = 0; virtual void ImpBottom() = 0; virtual void ImpSetExtent(const Point&) = 0; virtual void ImpSetOrigin(const Point&) = 0; virtual void DeviceRect(Coord, Coord, Coord, Coord) = 0; virtual void DeviceText(const char*, Coord, Coord) = 0; virtual void DeviceBitmap(const char*, Coord, Coord) = 0; // множество других функций для рисования в окне... protected: WindowImp(); }; 192 Глава 4. Структурные паттерны Подклассы Window определяют различные виды окон, как то: окно при- ложения, пиктограмма, временное диалоговое окно, плавающая палитра инструментов и т. д. Например, класс ApplicationWindow реализует операцию DrawContents для отрисовки содержимого экземпляра класса View , который в нем хра- нится: class ApplicationWindow : public Window { public: // ... virtual void DrawContents(); }; void ApplicationWindow::DrawContents () { GetView()->DrawOn(this); } А в классе IconWindow хранится имя растрового изображения для пикто- граммы: class IconWindow : public Window { public: // ... virtual void DrawContents(); private: const char* _bitmapName; }; и реализация операции DrawContents для рисования этого изображения в окне: void IconWindow::DrawContents() { WindowImp* imp = GetWindowImp(); if (imp != 0) { imp->DeviceBitmap(_bitmapName, 0.0, 0.0); } } Существует много других разновидностей класса Window . Окну класса TransientWindow иногда необходимо как-то сообщаться с создавшим его окном во время диалога, поэтому в объекте класса хранится ссылка на создателя. Окно класса PaletteWindow всегда располагается поверх других. Окно класса IconDockWindow (контейнер пиктограмм) хранит окна класса IconWindow и размещает их в ряд. Паттерн Bridge (мост) 193 Операции класса Window определяются в категориях интерфейса WindowImp Например, DrawRect вычисляет координаты по двум своим параметрам Point , перед тем как вызвать операцию WindowImp , которая рисует в окне прямоугольник: void Window::DrawRect (const Point& p1, const Point& p2) { WindowImp* imp = GetWindowImp(); imp->DeviceRect(p1.X(), p1.Y(), p2.X(), p2.Y()); } Конкретные подклассы WindowImp поддерживают разные оконные системы. Так, класс XWindowImp ориентирован на систему X Window: class XWindowImp : public WindowImp { public: XWindowImp(); virtual void DeviceRect(Coord, Coord, Coord, Coord); // прочие операции открытого интерфейса... private: // переменные, описывающие состояние, специфическое // для X Window system: Display* _dpy; Drawable _winid; // идентификатор окна GC _gc; // графический контекст окна }; Для Presentation Manager (PM) определяется класс PMWindowImp : class PMWindowImp : public WindowImp { public: PMWindowImp(); virtual void DeviceRect(Coord, Coord, Coord, Coord); // прочие операции открытого интерфейса... private: // переменные, описывающие состояние, специфическое для PM: HPS _hps; }; Эти подклассы реализуют операции WindowImp в контексте примитивов оконной системы. Например, DeviceRect для X Window реализуется так: void XWindowImp::DeviceRect ( Coord x0, Coord y0, Coord x1, Coord y1 ) { 194 Глава 4. Структурные паттерны int x = round(min(x0, x1)); int y = round(min(y0, y1)); int w = round(abs(x0 - x1)); int h = round(abs(y0 - y1)); XDrawRectangle(_dpy, _winid, _gc, x, y, w, h); } А реализация для PM выглядит так: void PMWindowImp::DeviceRect ( Coord x0, Coord y0, Coord x1, Coord y1 ) { Coord left = min(x0, x1); Coord right = max(x0, x1); Coord bottom = min(y0, y1); Coord top = max(y0, y1); PPOINTL point[4]; point[0].x = left; point[0].y = top; point[1].x = right; point[1].y = top; point[2].x = right; point[2].y = bottom; point[3].x = left; point[3].y = bottom; if ( (GpiBeginPath(_hps, 1L) == false) || (GpiSetCurrentPosition(_hps, &point[3]) == false) || (GpiPolyLine(_hps, 4L, point) == GPI_ERROR) || (GpiEndPath(_hps) == false) ) { // сообщить об ошибке } else { GpiStrokePath(_hps, 1L, 0L); } } Как окно получает экземпляр нужного подкласса WindowImp ? В данном примере предполагается, что за это отвечает класс Window . Его операция GetWindowImp получает подходящий экземпляр от абстрактной фабрики (см. описание паттерна абстрактная фабрика (113)), которая инкапсулирует все зависимости от оконной системы. WindowImp* Window::GetWindowImp () { if (_imp == 0) { _imp = WindowSystemFactory::Instance()->MakeWindowImp(); } return _imp; } Паттерн Bridge (мост) 195 WindowSystemFactory::Instance() возвращает абстрактную фабрику, кото- рая изготавливает все системнозависимые объекты. Для простоты мы сде- лали эту фабрику одиночкой (157) и позволили классу Window обращаться к ней напрямую. Известные применения Пример класса Window позаимствован из ET++ [WGM88]. В ET++ класс WindowImp называется WindowPort и имеет такие подклассы, как XWindowPort и SunWindowPort . Объект Window создает соответствующего себе реализатора Implementor , запрашивая его у абстрактной фабрики, которая называется WindowSystem . Эта фабрика предоставляет интерфейс для создания плат- форменнозависимых объектов: шрифтов, курсоров, растровых изображений и т. д. Дизайн классов Window / WindowPort в ET++ обобщает паттерн мост в том отношении, что WindowPort сохраняет также обратную ссылку на Window Класс-реализатор WindowPort использует эту ссылку для уведомления Window о событиях, специфичных для WindowPort : поступлении событий ввода, из- менениях размера окна и т. д. В работах Джеймса Коплиена [Cop92] и Бьерна Страуструпа [Str91] упо- минаются классы описателей и приводятся некоторые примеры. Основной акцент в этих примерах сделан на аспектах управления памятью, например совместном использовании представления строк и поддержки объектов переменного размера. Нас же в первую очередь интересует поддержка не- зависимых расширений абстракции и ее реализации. В библиотеке libg++ [Lea88] определены классы, которые реализуют рас- пространенные структуры данных: Set (множество), LinkedSet (множество как связанный список), HashSet (множество как хеш-таблица), LinkedList (связанный список) и HashTable (хеш-таблица). Set — это абстрактный класс, определяющий абстракцию множества, а LinkedList и HashTable — конкрет- ные реализации связанного списка и хеш-таблицы. LinkedSet и HashSet — реализаторы абстракции Set , формирующие мост между Set и LinkedList и HashTable соответственно. Перед вами пример вырожденного моста, по- скольку абстрактного класса Implementor здесь нет. В библиотеке NeXT AppKit [Add94] паттерн мост используется при реа- лизации и отображении графических изображений. Рисунок может быть представлен по-разному. Оптимальный способ его отображения на экране зависит от свойств дисплея и прежде всего от числа цветов и разрешения. Если бы не AppKit, то для каждого приложения разработчикам пришлось 196 Глава 4. Структурные паттерны бы самостоятельно выяснять, какой реализацией пользоваться в конкретных условиях. Чтобы избавить разработчика от этой ответственности, AppKit предостав- ляет мост NXImage / NXImageRep . Класс NXImage определяет интерфейс для обработки изображений. Реализация же определена в отдельной иерархии классов NXImageRep , в которой есть такие подклассы, как NXEPSImageRep , NXCachedImageRep и NXBitMapImageRep . В классе NXImage хранятся ссылки на один или более объектов NXImageRep . Если имеется более одной реализации изображения, то NXImage выбирает самую подходящую для данного дисплея. При необходимости NXImage даже может преобразовать изображение из од- ного формата в другой. Интересная особенность этого варианта моста в том, что NXImage может одновременно хранить несколько реализаций NXImageRep Родственные паттерны Паттерн абстрактная фабрика (113) может создать и сконфигурировать мост. Для обеспечения совместной работы не связанных между собой классов прежде всего предназначен паттерн адаптер (146). Обычно он применя- ется в уже готовых системах. Мост же участвует в проекте с самого начала и призван поддержать возможность независимого изменения абстракций и их реализаций. ПАТТЕРН COMPOSITE (КОМПОНОВЩИК) Название и классификация паттерна Компоновщик — паттерн, структурирующий объекты. Назначение Компонует объекты в древовидные структуры для представления иерархий «часть — целое». Позволяет клиентам единообразно трактовать индивиду- альные и составные объекты. Мотивация Такие приложения, как графические редакторы и редакторы электрических схем, позволяют пользователям строить сложные схемы из более простых компонентов. Проектировщик может сгруппировать мелкие компоненты для формирования более крупных, которые, в свою очередь, могут стать ос- Паттерн Composite (компоновщик) 197 новой для создания еще более крупных. В простой реализации можно было бы определить классы графических примитивов, например текста и линий, а также классы, используемые в качестве контейнеров для этих примитивов. Но у такого решения есть существенный недостаток. Программа, в которой эти классы используются, должна по-разному обращаться с примитивами и контейнерами, хотя пользователь чаще всего работает с ними единообраз- но. Необходимость различать эти объекты усложняет приложение. Паттерн компоновщик описывает, как можно применить рекурсивную композицию таким образом, что клиенту не придется проводить различие между про- стыми и составными объектами. Graphic Draw() Add(Graphic) Remove(Graphic) GetChild(int) Line Draw() Text Draw() Picture Draw() Add(Graphic g) Remove(Graphic) GetChild(int) Rectangle Draw() Для всех графических объектов g g.Draw() добавить g в список графических объектов Графические объекты Ключом к паттерну компоновщик является абстрактный класс, который представляет одновременно и примитивы, и контейнеры. В графической системе этот класс может называться Graphic . В нем объявляются операции, специфичные для каждого вида графического объекта (такие как Draw ), а также общие для всех составных объектов, например операции для доступа и управления потомками. Подклассы Line , Rectangle и Text (см. схему выше) определяют примитив- ные графические объекты. В них операция Draw реализована соответственно для рисования прямых, прямоугольников и текста. Поскольку у примитив- ных объектов нет потомков, то ни один из этих подклассов не реализует операции, относящиеся к управлению потомками. Класс Picture определяет агрегат, состоящий из объектов Graphic . Реализо- ванная в нем операция Draw вызывает одноименную функцию для каждого 198 Глава 4. Структурные паттерны потомка, а операции для работы с потомками уже не пусты. Поскольку интерфейс класса Picture соответствует интерфейсу Graphic , то в состав объекта Picture могут входить и другие такие же объекты. На следующей схеме показана типичная структура составного объекта, рекурсивно скомпонованного из объектов класса Graphic |