Поддержка нескольких оконных систем

Как должно выглядеть приложение - это лишь один из многих вопросов, встающих при переносе приложения на новую платформу. Еще одна проблема из той же серии - оконная среда, в которой работает Lexi. Данная среда создает иллюзию наличия нескольких перекрывающихся окон на одном растровом дисплее. Она распределяет между окнами площадь экрана и направляет им события клавиатуры и мыши. Сегодня существует несколько широко распространенных и во многом не совместимых между собой оконных систем (например, Macintosh, Presentation Manager, Windows, X). Мы хотели бы, чтобы Lexi работал в любой оконной среде по тем же причинам, по которым мы поддерживаем несколько стандартов внешнего облика.

Можно ли воспользоваться абстрактной фабрикой?

На первый взгляд представляется, что перед нами еще одна возможность применить паттерн абстрактная фабрика. Но ограничения, связанные с переносом на другие оконные системы, существенно отличаются от тех, что накладывают независимость от внешнего облика.

Применяя паттерн абстрактная фабрика, мы предполагали, что удастся определить конкретный класс глифов-виджетов для каждого стандарта внешнего облика. Это означало, что можно будет произвести конкретный класс для данного стандарта (например, MotifScrollBar и MacScrollBar) от абстрактного класса (допустим, ScrollBar). Предположим, однако, что у нас уже есть несколько иерархий классов, полученных от разных поставщиков, - по одной для каждого стандарта. Маловероятно, что данные иерархии будут совместимы между собой. Поэтому отсутствуют общие абстрактные изготавливаемые классы для каждого вида виджетов (ScrollBar, Button, Menu и т.д.). А без них фабрика классов работать не может. Необходимо, чтобы иерархии виджетов имели единый набор абстрактных интерфейсов. Только тогда удастся правильно объявить операции Create... в интерфейсе абстрактной фабрики.

Для виджетов мы решили эту проблему, разработав собственные абстрактные и конкретные изготавливаемые классы. Теперь сталкиваемся с аналогичной трудностью при попытке заставить Lexi работать во всех существующих оконных средах. Именно разные среды имеют несовместимые интерфейсы программирования. Но на этот раз все сложнее, поскольку мы не можем себе позволить реализовать собственную нестандартную оконную систему.

Однако спасительный выход все же есть. Как и стандарты на внешний облик, интерфейсы оконных систем не так уж радикально отличаются друг от друга, ибо все они предназначены примерно для одних и тех же целей. Нам нужен унифицированный набор оконных абстракций, которым можно закрыть любую конкретную реализацию оконной системы.

Инкапсуляция зависимостей от реализации

Ранее мы ввели класс Window для отображения на экране глифа или структуры, состоящей из глифов. Ничего не говорилось о том, с какой оконной системой работает этот объект, поскольку в действительности он вообще не связан ни с одной системой. Класс Window инкапсулирует понятие окна в любой оконной системе:
  • операции для отрисовки базовых геометрических фигур;
  • возможность свернуть и развернуть окно;
  • возможность изменить собственные размеры;
  • возможность при необходимости отобразить свое содержимое, например при развертывании или открытии ранее перекрытой части окна.
Таблица 2.3. Интерфейс класса Window

Класс Window должен покрывать функциональность окон, которая имеется в различных оконных системах. Рассмотрим два крайних подхода:
  • пересечение функциональности. Интерфейс класса Window предоставляет только операции, общие для всех оконных систем. Однако в результате мы получаем интерфейс не богаче, чем в самой слабой из рассматриваемых систем. Мы не можем воспользоваться более развитыми средствами, даже если их поддерживает большинство оконных систем (но не все);
  • объединение функциональности. Создается интерфейс, который включает возможности всех существующих систем. Здесь нас подстерегает опасность получить чрезмерно громоздкий и внутренне не согласованный интерфейс. Кроме того, нам придется изменять его (а вместе с ним и Lexi) всякий раз, как только производитель переработает интерфейс своей оконной системы.

Ни то, ни другое решение «в чистом виде» не годятся, поэтому мы выберем компромиссное. Класс Window будет предоставлять удобный интерфейс, поддерживающий наиболее популярные возможности оконных систем. Поскольку редактор Lexi будет работать с классом Window напрямую, этот класс должен поддерживать и сущности, о которых Lexi известно, то есть глифы. Это означает, что интерфейс класса Window должен включать базовый набор графических операций, позволяющий глифам отображать себя в окне. В табл. 2.3 перечислены некоторые операции из интерфейса класса Window.

Window - это абстрактный класс. Его конкретные подклассы поддерживают различные виды окон, с которыми имеет дело пользователь. Например, окна приложений, сообщений, значки - это все окна, но свойства у них разные. Для учета таких различий мы можем определить подклассы Applicationwindow, IconWindow и DialogWindow. Возникающая иерархия позволяет таким приложениям, как Lexi, создать унифицированную, интуитивно понятную абстракцию окна, не зависящую от оконной системы конкретного поставщика:


Итак, мы определили оконный интерфейс, с которым будет работать Lexi. Но где же в нем место для реальной платформенно-зависимой оконной системы? Если мы не собираемся реализовывать собственную оконную систему, то в каком-то месте наша абстракция окна должна быть выражена в терминах целевой системы. Но где именно?

Можно было бы реализовать несколько вариантов класса Window и его подклассов - по одному для каждой оконной среды. Выбор нужного варианта производится при сборке Lexi для данной платформы. Но представьте себе, с чем вы столкнетесь при сопровождении, если придется отслеживать множество разных классов с одним и тем же именем Window, но реализованных для разных оконных систем. Вместо этого мы могли бы создать зависящие от реализации подклассы каждого класса в иерархии Window, но закончилось бы это тем же самым комбинаторным ростом числа классов, о котором уже говорилось при попытке добавить элементы оформления. Кроме того, оба решения недостаточно гибки, чтобы можно было перейти на другую оконную систему уже после компиляции программы. Поэтому придется поддерживать несколько разных исполняемых файлов.

Ни тот, ни другой вариант не выглядят привлекательно, но что еще можно сделать? То же самое, что мы сделали для форматирования и декорирования, - инкапсулировать изменяющуюся сущность. В этом случае изменчивой частью является реализация оконной системы. Если инкапсулировать функциональность оконной системы в объекте, то удастся реализовать свой класс Window и его подклассы в терминах интерфейса этого объекта. Более того, если такой интерфейс сможет поддержать все интересующие нас оконные системы, то не придется изменять ни Window, ни его подклассы при переходе на другую систему. Мы сконфигурируем оконные объекты в соответствии с требованиями нужной оконной системы, просто передав им подходящий объект, инкапсулирующий оконную систему. Это можно сделать даже во время выполнения.

Классы Window и WindowImp

Мы определим отдельную иерархию классов WindowImp, в которой скроем знание о различных реализациях оконных систем. WindowImp - это абстрактный класс для объектов, инкапсулирующих системно-зависимый код. Чтобы заставить Lexi работать в конкретной оконной системе, каждый оконный объект будем конфигурировать экземпляром того подкласса WindowImp, который предназначен для этой системы. На диаграмме ниже представлены отношения между иерархиями Window и WindowImp:


Скрыв реализацию в классах 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 у = 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 Windows для рисования прямоугольников) определяет прямоугольник с помощью левого нижнего угла, ширины и высоты. DeviceRect должна вычислить эти значения по переданным ей параметрам. Сначала находится левый нижний угол (поскольку (х0, у0) может обозначать любой из четырех углов прямоугольника), а затем вычисляется длина и ширина.

Подкласс PMWindowImp (подкласс WindowImp для Presentation Manager) определил бы DeviceRect по-другому:


Откуда такое отличие от версии для X? Дело в том, что в Presentation Manager (РМ) нет явной операции для рисования прямоугольников, как в X. Вместо этого РМ имеет более общий интерфейс для задания вершин фигуры, состоящей из нескольких отрезков (множество таких вершин называется траекторией), и для рисования границы или заливки той области, которую эти отрезки ограничивают.

Очевидно, что реализации DeviceRect для РМ и X совершенно непохожи, но это не имеет никакого значения. Возможно, WindowImp скрывает различия интерфейсов оконных систем за большим, но стабильным интерфейсом. Это позволяет автору подкласса Window сосредоточиться на абстракции окна, а не на деталях оконной системы. Заодно мы-получаем возможность добавлять поддержку для новых оконных систем, не изменяя классы из иерархии Window.

Конфигурирование класса Window с помощью WindowImp

Важнейший вопрос, который мы еще не рассмотрели, - как сконфигурировать окно с помощью подходящего подкласса WindowImp. Другими словами, когда инициализируется переменная _imp и как узнать, какая оконная система (следовательно, и подкласс WindowImp) используется? Ведь окну необходим объект WindowImp.

Тут есть несколько возможностей, но мы остановимся на той, где используется паттерн абстрактная фабрика. Можно определить абстрактный фабричный класс WindowSystemFactory, предоставляющий интерфейс для создания различных видов системно-зависимых объектов:

class WindowSystemFactory {
public:
virtual WindowImp* CreateWindowImp() = 0;
virtual ColorImp* CreateColorImp() = 0;
virtual FontImp* CreateFontImp() = 0;
// операции "Create..." для всех видов ресурсов оконной системы
};

Далее разумно определить конкретную фабрику для каждой оконной системы:


Чтобы инициализировать член _imp указателем на объект WindowImp, соответствующий данной оконной системе, конструктор базового класса Window может использовать интерфейс WindowSystemFactory:


Переменная WindowSystemFactory - это известный программе экземпляр подкласса WindowSystemFactory. Она, аналогично переменной guiFactory, определяет внешний облик. И инициализировать WindowSystemFactory можно точно так же.

Паттерн мост

Класс WindowImp определяет интерфейс к общим средствам оконной системы, но на его дизайн накладываются иные ограничения, нежели на интерфейс класса Window. Прикладной программист не обращается к интерфейсу WindowImp непосредственно, он имеет дело только с объектами класса Window. Поэтому интерфейс WindowImp необязательно должен соответствовать представлению программиста о мире, как то было в случае с иерархией и интерфейсом класса Window. Интерфейс WindowImp может более точно отражать сущности, которые в действительности предоставляют оконные системы, со всеми их особенностями. Он может быть ближе к идее пересечения или объединения функциональности - в зависимости от требований к целевой оконной системе.

Важно понимать, что интерфейс класса Window призван обслуживать интересы прикладного программиста, тогда как интерфейс класса WindowImp в большей степени ориентирован на оконные системы. Разделение функциональности окон между иерархиями Window и WindowImp позволяет нам независимо реализовывать и специализировать их интерфейсы. Объекты из этих иерархий взаимодействуют, позволяя Lexi работать без изменений в нескольких оконных системах.

Отношение иерархий Window и WindowImp являет собой пример паттерна мост. Идея его создания заключалась в том, чтобы предоставить возможность совместной работы отдельным иерархиям классов, даже в случае их раздельного эволюционирования. Критерии разработки, которыми мы руководствовались, заставили нас создать две различные иерархии классов: одну, поддерживающую логическое понятие окон, и другую для хранения промежуточных вариантов окон. Паттерн мост позволяет нам сохранять и совершенствовать наши логические абстракции управления окнами без необходимости привлечения программно-зависимого кода и наоборот.