Рассмотрим два усовершенствования пользовательского интерфейса Lexi. Первое добавляет рамку вокруг области редактирования текста, чтобы четко обозначить страницу текста, второе - полосы прокрутки, позволяющие пользователю просматривать разные части страницы. Чтобы упростить добавление и удаление таких элементов оформления (особенно во время выполнения), мы не должны использовать наследование. Максимальной гибкости можно достичь, если другим объектам пользовательского интерфейса даже не будет известно о том, какие еще есть элементы оформления. Это позволит добавлять и удалять декорации, не изменяя других классов.
Прозрачное обрамление
В программировании оформление пользовательского интерфейса означает расширение существующего кода. Использование для этой цели наследования не дает возможности реорганизовать интерфейс во время выполнения. Не менее серьезной проблемой является комбинаторный рост числа классов в случае широкого использования наследования.
Можно было бы добавить рамку к классу Composition, породив от него новый подкласс BorderedComposition. Точно так же можно было бы добавить и интерфейс прокрутки, породив подкласс ScrollableCompositiont Если же мы хотим иметь и рамку, и полосу прокрутки, следовало бы создать подкласс BorderedScrollableComposition, и так далее. Если довести эту идею до логического завершения, то пришлось бы создавать отдельный подкласс для каждой возможной комбинации элементов оформления. Это решение быстро перестает работать, когда количество разнообразных декораций растет.
С помощью композиции объектов можно найти куда более приемлемый и гибкий механизм расширения. Но из каких объектов составлять композицию? Поскольку известно, что мы оформляем существующий глиф, то и сам элемент оформления могли бы сделать объектом (скажем, экземпляром класса Border). Следовательно, композиция может быть составлена из глифа и рамки. Следующий шаг - решить, что является агрегатом, а что - компонентом. Допустимо считать, что рамка содержит глиф, и это имеет смысл, так как рамка окружает глиф на экране. Можно принять и противоположное решение - поместить рамку внутрь глифа, но тогда пришлось бы модифицировать соответствующий подкласс класса Glyph, чтобы он «знал» о наличии рамки. Первый вариант - включение глифа в рамку - позволяет поместить весь код для отображения рамки в классе Border, оставив остальные классы без изменения.
Как выглядит класс Border? Тот факт, что у рамки есть визуальное представление, наталкивает на мысль, что она должна быть глифом, то есть подклассом класса Glyph. Но есть и более настоятельные причины поступить именно таким образом: клиентов не должно волновать, есть у глифов рамки или нет. Все глифы должны трактоваться единообразно. Когда клиент сообщает простому глифу без рамки о необходимости нарисовать себя, тот делает это, не добавляя никаких элементов оформления. Если же этот глиф заключен в рамку, то клиент не должен обрабатывать рамку как-то специально; он просто предписывает составному глифу выполнить отображение точно так же, как и простому глифу в предыдущем случае. Отсюда следует, что интерфейс класса Border должен соответствовать интерфейсу класса Glyph. Чтобы гарантировать это, мы и делаем Border подклассом Glyph.
Все это подводит нас к идее прозрачного обрамления (transparent enclosure), где комбинируются понятия о композиции с одним потомком (однокомпонентные), и о совместимых интерфейсах. В общем случае клиенту неизвестно, имеет ли он дело с компонентом или его обрамлением (то есть родителем), особенно если обрамление просто делегирует все операции своему единственному компоненту. Но обрамление может также и расширять поведение компонента, выполняя дополнительные действия либо до, либо после делегирования (а возможно, и до, и после). Обрамление может также добавить компоненту состояние. Как именно, будет показано ниже.
Моноглиф
Концепцию прозрачного обрамления можно применить ко всем глифам, оформляющим другие глифы. Чтобы конкретизировать эту идею, определим подкласс класса Glyph, называемый MonoGlyph. Он будет выступать в роли абстрактного класса для глифов-декораций вроде рамки (см. рис. 2.7).
Рис. 2.7. Отношения класса MonoGlyph с другими классами |
В классе MonoGlyph хранится ссылка на компонент, которому он и переадресует все запросы. При этом MonoGlyph по определению становится абсолютно прозрачным для клиентов. Вот как моноглиф реализует операцию Draw:
void MonoGlyph::Draw(Window *w)
{
_component->Draw(w);
}
Подклассы MonoGlyph замещают по меньшей мере одну из таких операций переадресации. Например, Border::Draw сначала вызывает операцию родительского класса MonoGlyph::Draw, чтобы компонент выполнил свою часть работы, то есть нарисовал все, кроме рамки. Затем Border::Draw рисует рамку, вызывая свою собственную закрытую операцию DrawBorder, детали которой мы опустим:
void Border::Draw(Window *w)
{
MonoGlyph::Draw(w);
DrawBorder(w);
}
Обратите внимание, что Border::Draw, по сути дела, расширяет операцию родительского класса, чтобы нарисовать рамку. Это не то же самое, что простая замена операции: в таком случае MonoGlyph::Draw не вызывалась бы.
На рис. 2.7 показан другой подкласс класса MonoGlyph. Scroller - это MonoGlyph, который рисует свои компоненты на экране в зависимости от положения двух полос прокрутки, добавляющихся в качестве элементов оформления. Когда Scroller отображает свой компонент, графическая система обрезает его по границам окна. Отсеченные части компонента, оказавшиеся за пределами видимой части окна, не появляются на экране.
Теперь у нас есть все, что необходимо для добавления рамки и прокрутки к области редактирования текста в Lexi. Мы помещаем имеющийся экземпляр класса Composition в экземпляр класса Scroller, чтобы добавить интерфейс прокрутки, а результат композиции еще раз погружаем в экземпляр класса Border. Получившийся объект показан на рис. 2.8.
Рис. 2.8. Объектная структура после добавления элементов оформления |
Обратите внимание, что мы могли изменить порядок композиции на обратный, сначала добавив рамку, а потом погрузив результат в Scroller. В таком случае рамка прокручивалась бы вместе с текстом. Может быть, это то, что вам нужно, а может, и нет. Важно лишь, что прозрачное обрамление легко позволяет экспериментировать с разными вариантами, освобождая клиента от знания деталей кода, добавляющего декорации.
Отметим, что рамка допускает композицию только с одним глифом, не более того. Этим она отличается от рассмотренных выше композиций, где родительскому объекту позволялось иметь сколько угодно потомков. Здесь же заключение чего-то в рамку предполагает, что это «что-то» имеется в единственном экземпляре. Мы могли бы приписать некоторую семантику декорации более одного объекта, но тогда пришлось бы вводить множество видов композиций с оформлением: оформление строки, колонки и т.д. Это не дает ничего нового, так как у нас уже есть классы для такого рода композиций. Поэтому для композиции лучше использовать уже существующие классы, а новые добавлять для оформления результата. Отделение декорации от других видов композиции одновременно упрощает классы, реализующие разные элементы оформления, и уменьшает их количество. Кроме того, мы избавлены от необходимости дублировать уже имеющуюся функциональность.
Паттерн декоратор
Паттерн декоратор абстрагирует отношения между классами и объектами, необходимые для поддержки оформления с помощью техники прозрачного обрамления. Термин «оформление» на самом деле применяется в более широком смысле, чем мы видели выше. В паттерне декоратор под ним понимается нечто, что возлагает на объект новые обязанности. Можно, например, представить себе оформление абстрактного дерева синтаксического разбора семантическими действиями, конечного автомата - новыми состояниями или сети, состоящей из устойчивых объектов, - тэгами атрибутов. Декоратор обобщает подход, который мы использовали в Lexi, чтобы расширить его область применения.