Э. Гамма, Р. Хелм
Скачать 6.37 Mb.
|
ГЛАВА 2 ПРАКТИЧЕСКИЙ ПРИМЕР: ПРОЕКТИРОВАНИЕ РЕДАКТОРА ДОКУМЕНТОВ В данной главе рассматривается применение паттернов на примере про- ектирования визуального редактора документов Lexi 1 , построенного по принципу «что видишь, то и получаешь» (WYSIWYG). Вы увидите, как с помощью паттернов решаются проблемы проектирования, характерные для Lexi и аналогичных приложений. К концу этой главы у вас появится практический опыт использования восьми паттернов. На рис. 2.1 изображен пользовательский интерфейс редактора Lexi. WYSIWYG-представление документа занимает большую прямоугольную область в центре. В документе могут произвольно сочетаться текст и графика с применением разных стилей форматирования. Вокруг документа — при- вычные выпадающие меню и полосы прокрутки, а также значки с номерами для перехода на нужную страницу документа. 2.1. ЗАДАЧИ ПРОЕКТИРОВАНИЯ Рассмотрим семь задач, характерных для дизайна Lexi. Структура документа. Выбор внутреннего представления документа отражается практически на всех аспектах дизайна. Для редактиро- вания, форматирования, отображения и анализа текста необходимо 1 Дизайн Lexi основан на программе Doc — текстовом редакторе, разработанном Каль- дером [CL92]. 2.1. Задачи проектирования 57 Рис. 2.1. Пользовательский интерфейс Lexi 58 Глава 2. Практический пример: проектирование редактора документов уметь перебирать составляющие этого представления. Способ органи- зации информации играет решающую роль при дальнейшем проекти- ровании. Форматирование. Как в Lexi организовано размещение текста и графи- ки в виде ряда колонок? Какие объекты отвечают за реализацию раз- личных политик форматирования? Как эти политики взаимодействуют с внутренним представлением документа? Создание привлекательного интерфейса пользователя. В состав поль- зовательского интерфейса Lexi входят полосы прокрутки, рамки и эф- фекты тени у выпадающих меню. Вполне вероятно, что эти украшения будут изменяться по мере развития интерфейса Lexi. Поэтому важно иметь возможность легко добавлять и удалять элементы оформления, не затрагивая приложение. Поддержка разных стандартов оформления программы. Lexi дол- жен без серьезной модификации адаптироваться к стандартам оформ- ления программ, например, таким как Motif или Presentation Manager (PM). Поддержка оконных систем. В разных оконных системах обычно ис- пользуются разные стандарты оформления и поведения. Дизайн Lexi должен по возможности быть независимым от оконной системы. Операции пользователя. Пользователи управляют работой Lexi с по- мощью элементов интерфейса, в том числе кнопок и выпадающих меню. Функциональность, которая вызывается из интерфейса, разбросана по многим объектам программы. Проблема в том, чтобы разработать еди- нообразный механизм для обращения к таким функциям и отмены уже выполненных операций. Проверка правописания и расстановка переносов. Поддержка в Lexi таких аналитических операций, как проверка правописания и определе- ние мест расстановки переносов. Как свести к минимуму число классов, которые придется модифицировать при добавлении новой аналитиче- ской операции? Ниже обсуждаются указанные проблемы проектирования. Для каждой из них определяются некоторые цели и ограничения на способы их достиже- ния. Прежде чем предлагать решение, мы подробно остановимся на целях и ограничениях. На примере проблемы и ее решения демонстрируется при- менение одного или нескольких паттернов проектирования. Обсуждение каждой проблемы завершается краткой характеристикой паттерна. 2.2. Структура документа 59 2.2. СТРУКТУРА ДОКУМЕНТА Документ — это всего лишь организованное некоторым способом множество базовых графических элементов: символов, линий, многоугольников и других геометрических фигур. В совокупности они образуют полную информацию о содержании документа. И все же создатель документа часто представляет себе эти элементы не в графическом виде, а в терминах физической структуры документа — строк, колонок, рисунков, таблиц и других подструктур 1 . Эти подструктуры, в свою очередь, составлены из более мелких и т. д. Пользовательский интерфейс Lexi должен позволять пользователям рабо- тать с такими подструктурами напрямую. Например, пользователю следует предоставить возможности, которые позволят ему обращаться с диаграммой как с неделимой единицей, а не как с набором отдельных графических при- митивов; с таблицей — как с единым целым, а не как с неструктурированным хранилищем текста и графики. Это делает интерфейс простым и интуитивно понятным. Чтобы реализация Lexi обладала аналогичными свойствами, мы выберем внут реннее представление, соответствующее физической структуре документа. В частности, внутреннее представление должно поддерживать: отслеживание физической структуры документа, то есть разбиение тек- ста и графики на строки, колонки, таблицы и т. д.; генерирование визуального представления документа; установление соответствия между позициями экрана и элементами вну- треннего представления. Это позволит определить, что имеет в виду пользователь, выбирая некоторый элемент визуального представления. Кроме целей, также имеются и ограничения. Во-первых, текст и графику следует трактовать единообразно. Интерфейс приложения должен позволять свободно размещать текст внутрь графики и наоборот. Не следует считать графику частным случаем текста или текст — частным случаем графики, поскольку это в конечном итоге приведет к появлению избыточных меха- низмов форматирования и манипулирования. Одного набора механизмов должно быть достаточно и для текста, и для графики. 1 Авторы часто рассматривают документы и в терминах их логической структуры: пред- ложений, абзацев, разделов, подразделов и глав. Чтобы не усложнять пример, мы не будем явно хранить во внутреннем представлении информацию о логической структу- ре. Но то проектное решение, которое мы опишем, вполне пригодно для представления и такой информации. 60 Глава 2. Практический пример: проектирование редактора документов Во-вторых, в нашей реализации не может быть различий во внутреннем представлении отдельного элемента и группы элементов. Если Lexi будет одинаково работать с простыми и составными элементами, это позволит создавать документы со структурой любой сложности. Например, деся- тым элементом в строке второй колонки может быть как один символ, так и сложно устроенная диаграмма со многими внутренними компонентами. Если вы уверены в том, что этот элемент умеет изображать себя на экране и сообщать свои размеры, его внутренняя сложность не имеет никакого от- ношения к тому, как и в каком месте страницы он отображается. Однако второе ограничение противоречит необходимости анализировать текст на предмет выявления орфографических ошибок и расстановки пере- носов. Во многих случаях нам безразлично, является ли элемент строки про- стым или сложным объектом. Но иногда анализ зависит от анализируемого объекта. Так, вряд ли имеет смысл проверять орфографию многоугольника или пытаться переносить его с одной строки на другую. При проектирова- нии внутреннего представления надо учитывать эти и другие ограничения, которые могут конфликтовать друг с другом. РЕКУРСИВНАЯ КОМПОЗИЦИЯ На практике для представления информации, имеющей иерархическую структуру, часто применяется прием, называемый рекурсивной композицией. Он позволяет строить все более сложные элементы из простых. Рекурсивная композиция дает возможность составить документ из простых графических элементов. Сначала мы можем линейно расположить множество символов и графики слева направо для формирования одной строки документа. Затем несколько строк можно объединить в колонку, несколько колонок — в стра- ницу и т. д. (рис. 2.2). Для представления физической структуры можно ввести отдельный объ- ект для каждого существенного элемента. К таковым относятся не только видимые элементы вроде символов и графики, но и структурные элементы — строки и колонки. В результате получается структура объекта, изображенная на рис. 2.3. Представляя объектом каждый символ и каждый графический элемент до- кумента, мы обеспечиваем гибкость на самых нижних уровнях дизайна Lexi. Текст и графика обрабатываются единообразно в том, что касается отобра- жения, форматирования и вложения друг в друга. Lexi можно расширить для поддержки новых наборов символов, не затрагивая никаких других функций. Объектная структура Lexi точно отражает физическую структуру документа. 2.2. Структура документа 61 G g Символы Пробел Изображение Составной объект (строка) Составной объект (столбец) Рис. 2.2. Рекурсивная композиция текста и графики Составной объект (столбец) Составной объект (строка) Составной объект (строка) G g Пробел Рис. 2.3. Структура объекта для рекурсивной композиции текста и графики У описанного подхода есть два важных следствия. Первое очевидно: для объектов нужны соответствующие классы. Второе, менее очевидное, состоит в том, что у этих классов должны быть совместимые интерфейсы, поскольку мы хотим унифицировать работу с ними. Для обеспечения совместимости интерфейсов в таком языке, как C++, применяется наследование. 62 Глава 2. Практический пример: проектирование редактора документов ГЛИФЫ Абстрактный класс Glyph (глиф) определяется для всех объектов, которые могут присутствовать в структуре документа 1 . Его подклассы определяют как примитивные графические элементы (скажем, символы и изображения), так и структурные элементы (строки и колонки). На рис. 2.4 изображена до- статочно обширная часть иерархии класса Glyph , а в табл. 2.1 более подробно представлен базовый интерфейс этого класса в синтаксисе C++ 2 Таблица 2.1. Базовый интерфейс класса Glyph Обязанность Операции Внешнее представление virtual void Draw(Window*) virtual void Bounds(Rect&) Обнаружение точки воздействия virtual bool Intersects(const Point&) Структура virtual void Insert(Glyph*, int) virtual void Remove(Glyph*) virtual Glyph* Child(int) virtual Glyph* Parent() У глифов есть три основные обязанности. Они (1) умеют рисовать себя на экране, (2) знают, сколько места они занимают, (3) располагают информа- цией о своих потомках и родителях. Подклассы класса Glyph переопределяют операцию Draw , которая пере- рисовывает текущий объект в окне. При вызове Draw ей передается ссылка на объект Window . В классе Window определены графические операции для 1 Впервые термин «глиф» в этом контексте употребил Пол Кальдер [CL90]. В боль- шинстве современных редакторов документов отдельные символы не представляются объектами — вероятно, из соображений эффективности. Кальдер продемонстрировал практическую пригодность этого подхода в своей диссертации [Cal93]. Наши глифы проще предложенных им, поскольку мы для простоты ограничились строгими иерар- хиями. Глифы Кальдера могут использоваться совместно для уменьшения потребле- ния памяти и образуют направленные ациклические графы. Для достижения того же эффекта можно воспользоваться паттерном приспособленец , но оставим это в качестве упражнения читателю. 2 Представленный здесь интерфейс намеренно сделан минимальным, чтобы не загро- мождать обсуждение техническими деталями. Полный интерфейс должен включать операции для работы с графическими атрибутами: цветами, шрифтами и преобразо- ваниями координат, а также операции для нетривиального управления потомками. 2.2. Структура документа 63 Glyph Draw(Window) Intersects(Point) Insert(Glyph, int) ... Character Rectangle Draw(Window w) Intersects(Point p) Row Draw(Window w) Intersects(Point p) Insert(Glyph g, int i) Draw(...) Intersects(...) char c Polygon Draw(...) Intersects(...) return true, если точка p лежит в пределах символа w–>DrawCharacter(c) Добавить g в позицию i Для каждого из потомков if c–>Intersects(p) return true Для каждого из потомков с убедиться в правильности позиционирования; c–>Draw(w) потомки Рис. 2.4. Частичная иерархия класса Glyph прорисовки в окне на экране текста и основных геометрических фигур. На- пример, в подклассе Rectangle операция Draw могла бы определяться так: void Rectangle::Draw (Window* w) { w->DrawRect(_x0, _y0, _x1, _y1); } Здесь _x0 , _y0 , _x1 и _y1 — переменные класса Rectangle , определяющие два противоположных угла прямоугольника, а DrawRect — операция класса Window , рисующая на экране прямоугольник. Глифу-родителю часто бывает нужно знать, сколько места на экране за- нимает глиф-потомок — например, чтобы расположить его и остальные глифы в строке без перекрытий (как показано на рис. 2.3). Операция Bounds возвращает прямоугольную область, занимаемую глифом (точнее, противо- положные углы наименьшего прямоугольника, содержащего глиф). В под- 64 Глава 2. Практический пример: проектирование редактора документов классах класса Glyph эта операция переопределена так, чтобы она возвращала прямоугольную область, в которой осуществляется прорисовка. Операция Intersects возвращает признак, показывающий, лежит ли за- данная точка в пределах глифа. Всякий раз, когда пользователь щелкает мышью где-то в документе, Lexi вызывает эту операцию, чтобы определить, какой глиф или глифовая структура оказались под указателем мыши. Класс Rectangle переопределяет эту операцию для вычисления пересечения точки с прямоугольником. Поскольку у глифов могут быть потомки, то нам необходим единый интер- фейс для добавления, удаления и обхода потомков. Например, потомками класса Row являются глифы, расположенные в данной строке. Операция Insert вставляет глиф в позицию, заданную целочисленным индексом 1 Операция Remove удаляет заданный глиф, если он действительно является потомком. Операция Child возвращает потомка с заданным индексом (если таковой существует). Глифы, у которых действительно есть потомки (такие как Row ), должны пользоваться операцией Child , а не обращаться к структуре данных потомка напрямую. В таком случае при изменении структуры данных, ска- жем, с массива на связанный список не придется модифицировать операции вроде Draw , которые перебирают всех потомков. Аналогично операция Parent предоставляет стандартный интерфейс для доступа к родителю глифа, если таковой имеется. В Lexi глифы хранят ссылку на своего родителя, а операция Parent просто возвращает эту ссылку. ПАТТЕРН COMPOSITE (КОМПОНОВЩИК) Рекурсивная композиция подходит не только для документов. Ей можно пользоваться для представления любых потенциально сложных иерархи- ческих структур. Паттерн компоновщик (196) инкапсулирует сущность рекурсивной композиции в объектно-ориентированных категориях. Сейчас самое время обратиться к разделу об этом паттерне и изучить его на примере только что рассмотренного сценария. 1 Возможно, целочисленный индекс — не лучший способ описания потомков глифа. Это зависит от структуры данных, используемой внутри глифа. Если потомки хранятся в связанном списке, то более эффективно было бы передавать указатель на элемент списка. Более удачное решение проблемы индексации будет описано в разделе 2.8, когда будем обсуждать анализ документа. 2.3. Форматирование 65 2.3. ФОРМАТИРОВАНИЕ Мы разобрались с тем, как представлять физическую структуру доку- мента. Далее нужно разобраться с тем, как сконструировать конкретную физическую структуру, соответствующую правильно отформатированному документу. Представление и форматирование — это разные аспекты про- ектирования. По описанию внутренней структуры невозможно определить, как добраться до определенной подструктуры. За это в основном отвечает Lexi. Редактор разбивает текст на строки, строки — на колонки и т. д., учи- тывая при этом пожелания пользователя. Так, пользователь может изменить ширину полей, размеры отступов и позиций табуляции, установить одиноч- ный или двойной междустрочный интервал, а также задать много других параметров форматирования 1 . Алгоритм форматирования Lexi должен все это учитывать. Кстати говоря, мы ограничим значение термина «форматирование» и будем понимать под ним лишь разбиение на строки. Будем считать термины «фор- матирование» и «разбиение на строки» взаимозаменяемыми. Все приемы, рассматриваемые ниже, в равной мере относятся и к разбиению строк на колонки, и к разбиению колонок на страницы. Таблица 2.2. Базовый интерфейс класса Compositor Обязанность Операции Что форматировать void SetComposition(Composition*) Когда форматировать virtual void Compose() ИНКАПСУЛЯЦИЯ АЛГОРИТМА ФОРМАТИРОВАНИЯ С учетом всех ограничений и многочисленных подробностей процесс фор- матирования с трудом поддается автоматизации. К этой проблеме есть много подходов, и было разработано много разных алгоритмов форматирования со 1 Пользователя в большей степени интересует логическая структура документа: предло- жения, абзацы, разделы, главы и т. д. Физическая структура в общем-то менее интерес- на. Большинству пользователей не важно, где в абзаце произошел разрыв строки, если в целом все отформатировано правильно. То же самое относится и к форматированию колонок и страниц. Таким образом, пользователи задают только высокоуровневые ограничения на физическую структуру, а Lexi берет на себя всю черновую работу по их реализации. 66 Глава 2. Практический пример: проектирование редактора документов своими сильными и слабыми сторонами. Поскольку Lexi — это WYSIWYG- редактор, важно выдержать баланс между качеством и скоростью формати- рования. В общем случае желательно, чтобы редактор реагировал достаточно быстро и при этом внешний вид документа оставался приемлемым. На до- стижение этого компромисса влияет много факторов, и не все из них удастся установить на этапе компиляции. Например, можно предположить, что пользователь смирится с некоторым замедлением реакции в обмен на лучшее качество форматирования. При таком предположении следует применять совершенно другой алгоритм форматирования. Также возможен компро- мисс между временем и памятью, в большей степени ориентированный на реализацию: кэширование в памяти большего объема информации может уменьшить время форматирования. Поскольку алгоритмы форматирования обычно оказываются весьма слож- ными, желательно, чтобы они были достаточно замкнутыми, а еще лучше — полностью независимыми от структуры документа. В оптимальном вари- анте добавление новой разновидности Glyph вовсе не затрагивает алгоритм форматирования. С другой стороны, при добавлении нового алгоритма форматирования не должно возникать необходимости в модификации су- ществующих глифов. Учитывая все вышесказанное, мы должны постараться спроектировать Lexi так, чтобы алгоритм форматирования можно было легко заменить по край- ней мере на этапе компиляции, если уж не во время выполнения. Алгоритм можно изолировать и обеспечить возможность его простой замены путем инкапсуляции в объекте. А конкретнее мы определим отдельную иерархию классов для объектов, инкапсулирующих алгоритмы форматирования. Кор- нем иерархии станет интерфейс, который поддерживает широкий спектр алгоритмов, а каждый подкласс будет реализовывать этот интерфейс в виде конкретного алгоритма форматирования. Тогда удастся ввести подкласс класса Glyph , который будет автоматически структурировать своих потомков с помощью переданного ему объекта-алгоритма. |