Э. Гамма, Р. Хелм
Скачать 6.37 Mb.
|
76 Глава 2. Практический пример: проектирование редактора документов создания глифов-виджетов. В нем есть такие операции, как CreateScrollBar и CreateButton , для создания экземпляров различных видов виджетов. Подклассы GUIFactory реализуют эти операции, возвращая глифы вроде MotifScrollBar и PMButton , реализующие конкретное оформление и по- ведение. На рис. 2.9 показана иерархия классов для объектов GUIFactory GUIFactory CreateScrollBar() CreateButton() CreateMenu() ... PMFactory CreateScrollBar() CreateButton() CreateMenu() return new PMMenu return new PMButton return new PMScrollBar MotifFactory CreateScrollBar() CreateButton() CreateMenu() return new MotifMenu return new MotifButton return new MotifScrollBar MacFactory CreateScrollBar() CreateButton() CreateMenu() return new MacMenu return new MacButton return new MacScrollBar Рис. 2.9. Иерархия классов GUIFactory Мы говорим, что фабрики изготавливают объекты. Все объекты, изготов- ленные на фабриках, связаны друг с другом; в нашем случае все такие про- дукты — это виджеты, имеющие один и тот же внешний облик. На рис. 2.10 показаны некоторые классы, необходимые для того, чтобы фабрика могла изготавливать глифы-виджеты. Остается ответить на последний вопрос: как получить экземпляр GUIFactory ? Да как угодно, лишь бы это было удобно. Переменная guiFactory может быть глобальной, может быть статическим членом хорошо известного класса или даже локальной, если весь пользовательский интерфейс создается внутри одного класса или функции. Существует специальный паттерн проектирова- ния одиночка (157), предназначенный для работы с такого рода объектами, существующими в единственном экземпляре. Важно, однако, чтобы фабрика guiFactory была инициализирована до того, как начнет использоваться для производства объектов, но после того, как стало известно, какое оформление требуется. 2.5. Поддержка нескольких стандартов оформления 77 Glyph ScrollBar ScrollTo(int) MotifScrollBar ScrollTo(int) MacScrollBar ScrollTo(int) PMScrollBar ScrollTo(int) Button Press() MotifButton Press() MacButton Press() PMButton Press() Menu Popup() MotifMenu Popup() MacMenu Popup() PMMenu Popup() Рис. 2.10. Абстрактные классы-продукты и их конкретные подклассы Когда вариант оформления известен на этапе компиляции, то guiFactory можно инициализировать простым присваиванием в начале программы: GUIFactory* guiFactory = new MotifFactory; Если же пользователь может задать нужный вариант оформления с помощью строки-параметра при запуске, то код создания фабрики мог бы выглядеть так: GUIFactory* guiFactory; const char* styleName = getenv("LOOK_AND_FEEL"); // Задается пользователем или средой при запуске if (strcmp(styleName, "Motif") == 0) { guiFactory = new MotifFactory; } else if (strcmp(styleName, "Presentation_Manager") == 0) { guiFactory = new PMFactory; } else { guiFactory = new DefaultGUIFactory; } Существуют и более сложные способы выбора фабрики во время выполне- ния. Например, можно было бы вести реестр, в котором символьные строки ассоциируются с объектами фабрик. Это позволяет зарегистрировать эк- земпляр новой фабрики без изменения существующего кода, как требуется при предыдущем подходе. И вам не придется связывать с приложением код фабрик для всех конкретных платформ. Это существенно, поскольку связать код для MotifFactory с приложением, работающим на платформе, где Motif не поддерживается, может оказаться невозможным. 78 Глава 2. Практический пример: проектирование редактора документов Впрочем, важно лишь то, что после настройки приложения для работы с кон- кретной фабрикой объектов, мы получаем нужный вариант оформления. Если впоследствии мы изменим решение, то сможем инициализировать guiFactory по-другому, чтобы изменить внешний облик, а затем динамически перестро- им интерфейс. Независимо от того, когда и как будет инициализироваться guiFactory , можно быть уверенным в том, что после этого приложение сможет создать необходимый вариант оформления без каких-либо изменений. ПАТТЕРН ABSTRACT FACTORY (АБСТРАКТНАЯ ФАБРИКА) Фабрики и их продукция — ключевые участники паттерна абстрактная фабрика (113). Этот паттерн может создавать семейства объектов без яв- ного создания экземпляров. Применять его лучше всего тогда, когда число и общий вид изготавливаемых объектов остаются постоянными, но между конкретными семействами продуктов имеются различия. Выбор того или иного семейства осуществляется путем создания экземпляра конкретной фабрики, после чего она используется для создания всех объектов. Под- ставив вместо одной фабрики другую, можно заменить все семейство объ- ектов целиком. В паттерне абстрактная фабрика акцент делается на создании семейств объектов, и это отличает его от других порождающих паттернов, создающих только один какой-то вид объектов. 2.6. ПОДДЕРЖКА НЕСКОЛЬКИХ ОКОННЫХ СИСТЕМ Как должно выглядеть приложение — это лишь один из многих вопросов, встающих при переносе приложения на другую платформу. Еще одна про- блема из той же серии — оконная среда, в которой работает Lexi. Данная среда создает иллюзию наличия нескольких перекрывающихся окон на одном растровом дисплее. Она распределяет между окнами площадь экрана и на- правляет им события клавиатуры и мыши. Сегодня существует несколько широко распространенных и во многом не совместимых между собой окон- ных систем (например, Macintosh, Presentation Manager, Windows, X). Мы хотели бы, чтобы Lexi работал в любой оконной среде по тем же причинам, по которым мы поддерживаем несколько стандартов оформления. МОЖНО ЛИ ВОСПОЛЬЗОВАТЬСЯ АБСТРАКТНОЙ ФАБРИКОЙ? На первый взгляд представляется, что и в этом случае можно воспользовать- ся паттерном абстрактная фабрика. Но ограничения, связанные с переносом на 2.6. Поддержка нескольких оконных систем 79 другие оконные системы, существенно отличаются от тех, что накладывают независимость от оформления. Применяя паттерн абстрактная фабрика, мы предполагали, что удастся определить конкретный класс глифов-виджетов для каждого стандарта оформления. Это означало, что можно будет произвести конкретный класс для конкретного стандарта (например, MotifScrollBar и MacScrollBar ) от абстрактного класса (допустим, ScrollBar ). Предположим, однако, что у нас уже есть несколько иерархий классов, полученных от разных поставщи- ков, — по одной для каждого стандарта. Крайне маловероятно, что данные иерархии будут совместимы между собой. Поэтому в приложении не будет общих абстрактных изготавливаемых классов для каждого вида виджетов ( ScrollBar , Button , Menu и т. д.) — а без них фабрика классов работать не может. Необходимо, чтобы иерархии виджетов имели единый набор аб- страктных интерфейсов. Только тогда удастся правильно объявить операции Create ... в интерфейсе абстрактной фабрики. Для виджетов эта проблема была решена разработкой собственных аб- страктных и конкретных изготавливаемых классов. Теперь аналогичная трудность возникает при попытке заставить Lexi работать во всех суще- ствующих оконных средах; а именно, разные среды имеют несовместимые интерфейсы программирования. Но на этот раз все сложнее, поскольку мы не можем себе позволить реализовать собственную нестандартную оконную систему. Однако спасительный выход все же есть. Как и стандарты оформления, ин- терфейсы оконных систем не так уж радикально отличаются друг от друга, ибо все они предназначены примерно для одних и тех же целей. Нам нужен унифицированный набор оконных абстракций, которым было бы возможно закрыть любую конкретную реализацию оконной системы. ИНКАПСУЛЯЦИЯ ЗАВИСИМОСТЕЙ ОТ РЕАЛИЗАЦИИ В разделе 2.2 был введен класс Window для отображения на экране глифа или структуры, состоящей из глифов. Ничего не говорилось о том, с какой оконной системой работает этот объект, поскольку в действительности он вообще не связан ни с одной системой. Класс Window инкапсулирует функ- циональность окна в любой оконной системе: операции прорисовки базовых геометрических фигур; возможность свернуть и развернуть окно; изменение собственных размеров; 80 Глава 2. Практический пример: проектирование редактора документов перерисовка своего содержимого при необходимости — например, при развертывании из значка или открытии ранее перекрытой части окна. Класс Window должен охватывать функциональность окон из разных оконных систем. Рассмотрим два крайних подхода: пересечение функциональности. Интерфейс класса Window предоставля- ет только функциональность, общую для всех оконных систем. Однако в результате мы получаем интерфейс не богаче, чем в самой слабой из рассматриваемых систем. Мы не можем воспользоваться более мощны- ми средствами, даже если их поддерживает большинство оконных си- стем (но не все); объединение функциональности. Создается интерфейс, который включа- ет возможности всех существующих систем. Здесь возникает опасность получить чрезмерно громоздкий и внутренне противоречивый интер- фейс. Кроме того, нам придется изменять его (а вместе с ним и Lexi) всякий раз, как только производитель переработает интерфейс своей оконной системы. Ни одно из крайних решений не годится, поэтому мы выберем компро- миссное. Класс Window будет предоставлять удобный интерфейс, поддер- живающий наиболее популярные возможности оконных систем. Поскольку редактор Lexi будет работать с классом Window напрямую, этот класс должен поддерживать и сущности, о которых Lexi известно — то есть глифы. Это оз- начает, что интерфейс класса Window должен включать базовый набор графи- ческих операций, позволяющий глифам отображать себя в окне. В табл. 2.3 приведена подборка операций из интерфейса класса Window Таблица 2.3. Интерфейс класса Window Обязательный Операции Управление окнами virtual void Redraw() virtual void Raise() virtual void Lower() virtual void Iconify() virtual void Deiconify() Графика virtual void DrawLine(...) virtual void DrawRect(...) virtual void DrawPolygon(...) virtual void DrawText(...) 2.6. Поддержка нескольких оконных систем 81 Window — это абстрактный класс. Его конкретные подклассы поддержи- вают различные виды окон, с которыми имеет дело пользователь. Напри- мер, окна приложений, сообщений, значки — это все окна, но свойства у них разные. Для учета таких различий мы можем определить подклассы ApplicationWindow , IconWindow и DialogWindow . Возникающая иерархия позволяет таким приложениям, как Lexi, создать унифицированную, ин- туитивно понятную абстракцию окна, не зависящую от оконной системы конкретного поставщика: Glyph Draw(Window) Window Redraw() Iconify() Lower() ... DrawLine() ... ApplicationWindow IconWindow Iconify() DialogWindow Lower() owner>Lower() glyph>Draw(this) glyph owner Итак, мы определили оконный интерфейс, с которым будет работать Lexi. Но где же в нем место для реальной платформеннозависимой оконной системы? Если мы не собираемся реализовывать собственную оконную систему, то в каком-то месте наша абстракция окна должна быть выражена в терминах целевой системы. Но где именно? Можно было бы реализовать несколько вариантов класса Window и его подклассов — по одному для каждой оконной среды. Выбор нужного варианта производится при сборке Lexi для данной платформы. Но пред- ставьте себе, с чем вы столкнетесь при сопровождении, если придется отслеживать множество разных классов с одним и тем же именем Window , но реализованных для разных оконных систем. Вместо этого можно было бы создать зависящие от реализации подклассы каждого класса в иерар- хии Window , но закончилось бы это тем же самым стремительным ростом числа классов, о котором уже говорилось при попытке добавить элементы оформления. Кроме того, оба решения не обладают достаточной гибкостью, 82 Глава 2. Практический пример: проектирование редактора документов чтобы можно было перейти на другую оконную систему уже после ком- пиляции программы. Поэтому придется поддерживать несколько разных исполняемых файлов. Ни тот, ни другой вариант не вдохновляют, но что еще можно сделать? То же самое, что мы сделали для форматирования и декорирования, — инкап- сулировать изменяющуюся сущность. В этом случае переменной частью является реализация оконной системы. Если инкапсулировать функцио- нальность оконной системы в объекте, то удастся реализовать свой класс Window и его подклассы в категориях интерфейса этого объекта. Более того, если такой интерфейс сможет поддерживать все интересующие нас оконные системы, то не придется изменять ни Window , ни его подклассы при переходе на другую систему. Чтобы настроить оконные объекты в соответствии с тре- бованиями нужной оконной системы, достаточно передать им подходящий объект, инкапсулирующий оконную систему. Это можно сделать даже во время выполнения. КЛАССЫ WINDOW И WINDOWIMP Мы определим отдельную иерархию классов WindowImp , в которой скроем знание о различных реализациях оконных систем. WindowImp — это абстракт- ный класс для объектов, инкапсулирующих системнозависимый код. Чтобы заставить Lexi работать в конкретной оконной системе, каждый оконный объект будем конфигурировать экземпляром того подкласса WindowImp , который предназначен для этой системы. На схеме ниже представлены от- ношения между иерархиями Window и WindowImp : WindowImp DeviceRaise() DeviceRect(...) ... MacWindowImp DeviceRaise() DeviceRect(...) PMWindowImp DeviceRaise() DeviceRect(...) XWindowImp DeviceRaise() DeviceRect(...) Window Raise() DrawRect(...) IconWindow ApplicationWindow DialogWindow imp 2.6. Поддержка нескольких оконных систем 83 Скрыв реализацию в классах WindowImp , мы сумели избежать «засорения» классов Window зависимостями от оконной системы. В результате иерархия Window получается сравнительно компактной и стабильной. В то же время мы можем расширить иерархию реализаций, если будет нужно поддержать новую оконную систему. ПОДКЛАССЫ WINDOWIMP Подклассы WindowImp преобразуют запросы в операции, характерные для конкретной оконной системы. Рассмотрим пример из раздела 2.2. Мы опреде- лили Rectangle::Draw в категориях DrawRect над экземпляром класса Window : void Rectangle::Draw (Window* w) { w->DrawRect(_x0, _y0, _x1, _y1); } В реализации DrawRect по умолчанию используется абстрактная операция рисования прямоугольников, объявленная в WindowImp : void Window::DrawRect ( Coord x0, Coord y0, Coord x1, Coord y1 ) { _imp->DeviceRect(x0, y0, x1, y1); } где _imp — переменная класса Window , в которой хранится указатель на объ- ект WindowImp , использованный при настройке Window . Реализация окна определяется тем экземпляром подкласса WindowImp , на который указывает _imp . Для XWindowImp (то есть подкласса WindowImp для оконной системы X Window System) реализация DeviceRect могла бы выглядеть так: void XWindowImp::DeviceRect ( Coord x0, Coord y0, Coord x1, Coord y1 ) { 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); } DeviceRect определяется именно так, поскольку XDrawRectangle (интерфейс X Window для рисования прямоугольников) определяет прямоугольник по левому нижнему углу, ширине и высоте. Реализация DeviceRect должна вы- числить эти значения по переданным ей параметрам. Сначала она находит левый нижний угол (поскольку (x0, y0) может быть любым из четырех углов прямоугольника), а затем вычисляет длину и ширину. 84 Глава 2. Практический пример: проектирование редактора документов PMWindowImp (подкласс WindowImp для Presentation Manager) определил бы DeviceRect по-другому: 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); } } Откуда такое отличие от версии для X? Дело в том, что в Presentation Manager (PM) нет явной операции для рисования прямоугольников, как в X. Вместо этого PM имеет более общий интерфейс для задания вершин фигуры, состоящей из нескольких отрезков (множество таких вершин на- зывается траекторией), и для рисования границы или заливки той области, которую эти отрезки ограничивают. Очевидно, что реализации DeviceRect для PM и X совершенно непохожи, но это не имеет никакого значения. Возможно, WindowImp скрывает различия интерфейсов оконных систем за большим, но стабильным интерфейсом. Это позволяет автору подкласса Window сосредоточиться на абстракции окна, а не на подробностях оконной системы. Также появляется возможность добавлять поддержку для новых оконных систем, не изменяя классы из иерархии Window |