close

Вход

Забыли?

вход по аккаунту

?

Э.Гамма, Р.Хелм, Р.Джонсон - Приемы ООП Паттерны проектирования

код для вставкиСкачать
Э. Гамма Р. Хел м Р. Джонсо н Дж. Влиссиде с
Erich Gamma
Ralph Johnson
Richard Helm
John Vlissides
Addison-Wesle y
An imprint of Addison Wesley Longman, Inc.
Reading, Masachusetts • Harlow, England • Menlo Park, California
Berkley, California • Don Mills, Ontario • Sydney
Bonn • Amsterdam • Tokyo • Mexico City
Desig n Pattern s
Element s of Reusabl e
Object-Oriente d Softwar e
Э. Гамма Р. Хелм Р. Джонсон Дж. Влиссидес
БИБЛИОТЕКА ПРОГРАММИСТА
Санкт-Петербург
Москва • Харьков • Минск
200 1
3. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес
Приемы объектно-ориентированного проектирования
Паттерны проектирования
Серия «Библиотека программиста»
, Перевел с английского А. Слинкин
Руководитель проекта
Научный редактор
Литературный редактор
Технический редактор
Иллюстрации
Художник
Верстка
И. Захаров
Н. Шалаев
А. Петроградская
С. Прока
А. Бахарев
Н. Биржаков
Л. Пискунова
ББК 32.973.2-018
УДК 681.3.068
Гамма Э., Хелм Р., Джонсон Р., Влиссидес Дж.
П75 Приемы объектно-ориентированного проектирования. Паттерны проектирования. —
СПб: Питер, 2001. — 368 с.: ил. (Серия «Библиотека программиста»)
ISBN 5-272-00355-1
В предлагаемой книге описываются простые и изящные решения типичных задач, возникающих в
объектно-ориентированном проектировании. Паттерны появились потому, что многие разработчики иска-
ли пути повышения гибкости и степени повторного использования своих программ. Найденные решения
воплощены в краткой и легко применимой на практике форме. Авторы излагают принципы использования
паттернов проектирования и приводят их каталог. Таким образом, книга одновременно решает две задачи.
Во-первых, здесь демонстрируется роль паттернов в создании архитектуры сложных систем. Во-вторых,
применяя содержащиеся в справочнике паттерны, проектировщик сможет с легкостью разрабатывать
собственные приложения.
Издание предназначено как для профессиональных разработчиков, так и для программистов,
осваивающих объектно-ориентированное проектирование.
Original English language Edition Copyright© 1995 by Addison Wesley Longman, Inc.
© Перевод на русский язык, А. Слинкин, 2001
© Серия, оформпение, Издательский дом «Питер», 2001
Оригинал-макет подготовлен издательством «ДМК Пресс».
Права на издание получены по соглашению с Addison-Wesley Longman.
Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного
разрешения владельцев авторских прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не
менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную
точность и полноту приводимых сведений и не несет ответственность за возможные ошибки, связанные с использованием книги.
ISBN 5-272-00355-1
ISBN 0-201-63361-2 (англ.)
ЗАО «Питер Бук». 196105, Санкт-Петербург, Благодатная ул., д. 67.
Лицензия ИД № 01940 от 05.06.00.
Налоговая льгота - общероссийский классификатор продукции ОК 005-93, том 2; 953000 - книги и брошюры.
Подписано в печать 08.10.00. Формат 70х100'/|6. Усл. п. л. 29,67. Тираж 5000 экз. Заказ№ 1997.
Отпечатано с готовых диапозитивов в ГПП «Печатный двор» Министерства РФ по делам печати,
телерадиовещания и средств массовых коммуникаций.
197110, Санкт-Петербург, Чкаловский пр., 15.
Посвящается
Кэрин
- Э. Гамма
Сильвии
- Р. Хелм
Фейт
- Р. Джонсон
Дрю Энн и Мэттью
Джошуа 24:15Ь
- Дж. Влиссидес
Отзывы на книгу «Design Patterns: Elements
of Reusable Object-Oriented Software»
Одна из самых хорошо написанны х и глубоки х книг, которые мне доводилос ь читать...
эта работ а доказывае т необходимост ь паттерно в самым правильны м способом: не рассуж-
дениями, а примерами.
Стэн Липпман, The C++ Report
...Нова я книг а Гаммы, Хелма, Джонсон а и Влиссидес а окаже т важно е и продолжитель -
ное воздействи е на наук у проектировани я программног о обеспечения. Поскольк у авторы
преподнося т свой труд как относящийс я тольк о к объектно-ориентированны м программам,
боюсь, что многи е разработчики, не занимающиес я объектно й проблематикой, могу т не
обратит ь на книг у внимания. Это будет большо й ошибкой. На самом деле каждый, кто за-
нимаетс я проектирование м программ, найде т здесь мног о интересног о для себя. Все про-
ектировщик и применяю т паттерны, поэтом у более глубоко е понимани е повторн о исполь -
зуемых абстракци й лишь пойде т нам на пользу.
Том ДеМарко, IEEE Software
Полагаю, что книг а чрезвычайн о ценна, поскольк у описывае т богатейши й опыт объек-
тно-ориентированного проектирования, изложенны й в компактной, удобно й для много -
кратног о применени я форме. Безусловно, я снова и снова буду обращатьс я к идеям, пред-
ставленны м в издании, а ведь именн о в этом и состои т суть повторног о использования, не
так ли?
Санджи в Госсайн.Journa l of Object-Oriented Programming
Эта книга, столь давно ожидаемая, полность ю оправдал а все предшествовавши е ей рек-
ламные посулы. Она подобн а справочник у архитектора, в которо м приведен ы проверен -
ные временем, испытанны е на практик е прием ы и метод ы проектирования. Всего автор ы
отобрал и 23 паттерна. Подарит е экземпля р этой книг и каждом у из своих знакомы х про-
граммистов, желающем у усовершенствоватьс я в своей профессии.
Ларри О'Брайен, Software Development
Следуе т признать, что паттерн ы могу т полность ю изменит ь подход ы к инженерном у
проектировани ю программ, привнес я в эту област ь изяществ о по-настоящем у элегантног о
дизайна. Из всех имеющихс я кни г на эту тему «Паттерн ы проектирования», безусловно,
лучшая. Ее следуе т читать, изучат ь и переводит ь на други е языки. Она раз и навсегд а изме -
нит ваш взгляд на программное обеспечение.
Стив Билов, Journal of Object-Oriented Programming
«Паттерн ы проектирования » - замечательна я книга. Потрати в на ее чтени е сравни -
тельно немног о времени, большинств о программисто в на языке C++ смогу т начат ь приме -
нять паттерн ы в своей работе, что улучши т качеств о создаваемы х ими программ. Эта книг а
передае т в наше распоряжени е конкретны е инструменты, помогающи е более эффективн о
мыслит ь и выражат ь свои идеи. Она може т фундаментальн о изменит ь ваш взгля д на про-
граммирование.
Том Каргилл, The C++ Report
Предисловие 10
Глава 1. Введение в паттерны проектирования 15
1.1. Что такое паттерн проектирования 16
1.2. Паттерны проектирования в схеме MVC в языке Smalltalk 18
1.3. Описание паттернов проектирования 20
1.4. Каталог паттернов проектирования 22
1.5. Организация каталога 24
1.6. Как решать задачи проектирования с помощью паттернов 25
Поиск подходящих объектов 25
Определение степени детализации объекта 27
Специфицирование интерфейсов объекта 27
Специфицирование реализации объектов 29
Механизмы повторного использования 32
Сравнение структур времени выполнения и времени компиляции 37
Проектирование с учетом будущих изменений 38
1.7. Как выбирать паттерн проектирования 43
1.8. Как пользоваться паттерном проектирования 44
Глава 2. Проектирование редактора документов 46
2.1. Задачи проектирования 46
2.2. Структура документа 48
Рекурсивная композиция 49
Глифы 5 1
Паттерн компоновщик 53
2.3. Форматирование 53
Инкапсуляция алгоритма форматирования 54
Классы Compositor и Composition 54
Стратегия 56
2.4. Оформление пользовательского интерфейса 56
Прозрачное обрамление 57
Моноглиф 58
Паттерн декоратор 60
2.5. Поддержка нескольких стандартов внешнего облика 60
Абстрагирование создания объекта 61
Фабрики и изготовленные классы 61
Паттерн абстрактная фабрика 64
Приемы ООП
2.6. Поддержк а нескольки х оконных систем 6 4
Можно ли воспользоваться абстрактной фабрикой? 64
Инкапсуляция зависимостей от реализации 65
Классы Window и Windowlmp 67
Подклассы Windowlmp 68
Конфигурирование класса Window с помощью Windowlmp 70
Паттерн мост 70
2.7. Операции пользователя 71
Инкапсуляция запроса 72
Класс Command и его подклассы 73
Отмена операций 74
История команд 75
Паттерн команда 76
2.8. Проверка правописания и расстановка переносов 76
Доступ к распределенной информации 77
Инкапсуляция доступа и порядка обхода 77
Класс Iterator и его подклассы 78
Паттерн итератор 81
Обход и действия, выполняемые при обходе 81
Класс Visitor и его подклассы 86
Паттерн посетитель .- 87
2.9. Резюме 88
Глава 3. Порождающие паттерны 89
Паттерн Abstract Factory 93
Паттерн Builder 102
Паттерн Factory Method 111
Паттерн Prototype 121
Паттерн Singleton 130
Обсуждение порождающих паттернов 138
Глава 4. Структурные паттерны 140
Паттерн Adapter 141
Паттерн Bridge 152
Паттерн Composite 162
Паттерн Decorator 173
Паттерн Facade 183
Паттерн Flyweight 191
Паттерн Proxy 203
Обсуждение структурных паттернов 213
Адаптер и мост 213
Компоновщик, декоратор и заместитель 214
Глава 5. Паттерны поведения 216
Паттерн Chain of Responsibility 217
Паттерн Command 227
Паттерн Interpreter 236
Паттерн Iterator 24 9
Паттерн Mediator 263
Паттерн Memento 272
Паттерн Observer 28 0
Паттерн State 29 1
Паттерн Strategy 30 0
Паттерн Template Method 309
Паттерн Visitor 31 4
Обсуждение паттернов поведения 32 8
Инкапсуляция вариаций 32 8
Объекты как аргументы 32 8
Должен ли обмен информацией быть инкапсулированны м или распределенным ... 329
Разделение получателей и отправителе й 33 0
Резюме 33 2
Глава 6. Заключение 33 3
6.1. Чего ожидать от паттернов проектировани я 33 3
Единый словарь проектировани я 33 3
Помощь при документировани и и изучении 33 4
Дополнение существующих методов 33 4
Цель реорганизаци и 33 5
6.2. Краткая история 33 6
6.3. Проектировщик и паттернов 33 7
Языки паттернов Александра '. 33 8
Паттерны в программном обеспечении 33 9
6.4. Приглашение 33 9
6.5. На прощание 340
Приложение А. Глоссарий 34 1
Приложение В. Объяснение нотации 34 4
8. 1. Диаграмма классов 34 4
8.2. Диаграмма объектов 34 5
8.3. Диаграмма взаимодействи й 34 6
Приложение С. Базовые классы 34 8
C. 1. Lis t 34 8
С.2. Iterator 35 0
С.З. Listlterator 35 0
С.4. Point 35 1
С.5. Rect 35 1
Библиографи я 35 3
Алфавитный указатель : 35 9
Предислови е
Данна я книга не являетс я введение м в объектно-ориентированно е программиро -
вание или проектирование. На эти темы есть мног о других хороши х изданий.
Предполагается, что вы достаточн о хорошо владеете, по крайне й мере, одним
объектно-ориентированны м языком программировани я и имеете какой-т о опыт
объектно-ориентированног о проектирования. Безусловно, у вас не должно возни-
кать необходимост и лезть в словар ь за разъяснение м термино в «тип», «поли-
морфизм», и вам понятно, чем «наследовани е интерфейса» отличаетс я от «насле -
довани я реализации».
С другой стороны, эта книг а и не научный труд, адресованны й исключитель -
но узким специалистам. Здесь говоритс я о паттернах проектирования и описы-
ваются простые и элегантны е решени я типичны х задач, возникающи х в объект -
но-ориентированно м проектировании. Паттерн ы проектировани я не появилис ь
сразу в готовом виде; многие разработчики, искавши е возможност и повысит ь гиб-
кость и степень пригодност и к повторном у использовани ю своих программ, при-
ложили много усилий, чтобы поставленна я цель была достигнута. В паттерна х
проектировани я найденны е решени я отлит ы в кратку ю и легко примениму ю на
практик е форму.
Для использовани я паттерно в не нужны ни какие-т о особенные возможност и
языка программирования, ни хитроумны е приемы, поражающи е воображени е
друзей и начальников. Все можно реализоват ь на стандартны х объектно-ориен -
тированны х языках, хотя для этого потребуетс я приложит ь нескольк о больше
усилий, чем в случа е специализированног о решения, применимог о только в од-
ной ситуации. Но эти усили я неизменн о окупаютс я за счет больше й гибкост и
и возможност и повторног о использования.
Когда вы усвоит е работ у с паттернам и проектировани я настолько, что после
удачног о их применени я воскликнет е «Ага!», а не будете смотрет ь в сомнени и на
получившийс я результат, ваш взгляд на объектно-ориентированно е проектирова -
ние изменитс я раз и навсегда. Вы сможет е строит ь более гибкие, модульные, по-
вторно используемы е и понятные конструкции, а разве не для этого вообще суще-
ствует объектно-ориентированно е проектирование?
Нескольк о слов, чтобы предупредит ь и одновременн о подбодрит ь вас. Не
огорчайтесь, если не все будет понятно после первог о прочтени я книги. Мы и сами
не все понимали, когда начинал и писат ь ее! Помните, что эта книга не из тех, ко-
торых, однажд ы прочитав, ставят на полку. Мы надеемся, что вы будет е возвра -
щаться к ней снова и снова, черпа я идеи и ожида я вдохновения.
Книг а созревал а довольн о долго. Она повидал а четыре страны, была свидете -
лем женитьб ы трех ее авторо в и рождени я двух младенцев. В ее создани и так или
Предислови е
инач е участвовал и многи е люди. Особу ю благодарност ь мы выражае м Брюс у
Андерсон у (Bruc e Anderson), Кент у Беку (Ken t Beck ) и Андр е Вейнанд у (Andr e
Weinand ) за поддержк у и ценны е советы. Также благодари м всех рецензенто в чер-
новых варианто в рукописи: Роджер а Билефельд а (Roge r Bielefeld), Грейд и Буча
(Grad y Booch), Тома Каргилл а (Tom Cargill), Маршалл а Клайн а (Marshal l Cline),
Ральфа Хайр а (Ralp h Нуге), Брайан а Керниган а (Bria n Kernighan), Томас а Лали -
берт и (Thoma s Laliberty), Марк а Лоренц а (Mar k Lorenz), Артур а Рил я (Arthu r
Riel), Дуг а Шмидт а (Dou g Schmidt), Кловис а Тонд о (Clovi s Tondo), Стив а Винос -
ки (Stev e Vinoski ) и Ребекк у Вирфс-Бро к (Rebecc a Wirfs-Brock). Выражае м при-
знательност ь сотрудника м издательств а Addison-Wesle y за поддержк у и терпение:
Кейт у Хабиб у (Kat e Habib), Тиффан и Мур (Tiffan y Moore), Лайз е Раффаэл е (Lis a
Raffaele), Прадип у Сив а (Pradeep a Siva) и Джон у Уэйт у 0°hn Wait). Особа я бла-
годарност ь Карл у Кесслер у (Car l Kessler), Дэнн и Саббах у (Dann y Sabbah ) и Мар-
ку Вегман у (Mar k Wegman ) из исследовательског о отдел а компани и IBM за нео-
слабевающи й интере с к этой работ е и поддержку.
И наконец, не в последню ю очеред ь мы благодарн ы всем тем людям, которы е
высказывал и замечани я по повод у этой книг и через Internet, ободрял и нас и убеж-
дали, что така я работ а действительн о нужна. Вот далек о не полны й перечен ь на-
ших «незнакомы х помощников»: Ион Авотин с (Jon Avotins), Стив Берчу к (Stev e
Berczuk), Джулиа н Берди ч (Julia n Berdych), Матиа с Боле н (Matthia s Bohlen), Джон
Брант (John Brant), Ала н Клар к (Alla n Clarke), Пол Чизхол м (Paul Chisholm), Йене
Колдью и (Jens Coldewey), Дейв Коллин з (Dav e Collins), Джи м Коплие н (Jim
Coplien), Дон Двиггин с (Don Dwiggins), Габриэл ь Элиа (Gabriel e Elia), Дуг Фель т
(Doug Felt), Брайа н Фут (Bria n Foote), Дени с Форти н (Deni s Fortin), Уорд Хароль д
(War d Harold), Херма н Хуэн и (Herman n Hueni), Найи м Исла м (Nayee m Islam),
Бикрамжи т Калр а (Bikramji t Kalra), Пол Кифе р (Pau l Keefer), Тома с Кофле р
(Thoma s Kofler), Дуг Леа (Dou g Lea), Дэн Лалиберт е (Dan LaLiberte), Джейм с Лонг
(Jame s Long), Анна-Луиз а Луу (Ann Louis e Luu), Панд и Мадхава н (Pund i Mad-
havan), Брайа н Мэри к (Bria n Marick), Робер т Марти н (Rober t Martin), Дэйв Мак-
Комб (Dav e McComb), Карл МакКоннел л (Car l McConnell), Кристи н Мингин с
(Christin e Mingins), Ханспете р Мессенбе к (Hanspete r Mossenbock), Эрик Ньюто н
(Eri c Newton), Марианн а Озка н (Mariann e Ozkan), Роксан а Пайет т (Roxsa n Pay-
ette), Ларр и Подмоли к (Larr y Podmolik), Джорд ж Радин (Georg e Radin), Сит а Ра-
макришна н (Sit a Ramakrishnan), Русс Рамире с (Rus s Ramirez), Александ р Ран
(Alexande r Ran), Дир к Риэл е (Dir k Riehle), Брайа н Розенбур г (Brya n Rosenburg),
Аамо д Сейн (Aamo d Sane), Дур и Шмид т (Dur i Schmidt), Робер т Зайдл ь (Rober t
Seidl), Хин Шу (Xi n Shu) и Билл Уолке р (Bil l Walker).
Мы не считаем, что набо р отобранны х нами паттерно в поло н и неизменен,
он прост о отражае т нынешне е состояни е наши х мысле й о проектировании. Мы
приветствуе м любые замечания, будь то критик а приведенны х примеров, ссылк и
на известны е способ ы использования, которы е не упомянут ы здесь, или предложе -
ния по повод у дополнительны х паттернов. Вы может е писат ь нам на адре с изда -
тельств а Addison-Wesle y или на электронны й адре с design-patterns@cs.uiuc.edu.
Предислови е
Исходны е текст ы всех примеро в можн о получить, отправи в сообщени е «send desig n
patter n source » no адрес у design-patterns-source@cs.uiuc.edu. А тепер ь такж е ест ь
Web-страниц а http://st-www.cs.uiuc.edu/users/patterns/DPBook/DPBook.html. на
которо й размещаетс я последня я информаци я и обновлени я к книге.
Эрих Гамм а Маунти н Вью, шта т Калифорни я
Ричар д Хел м Монреаль, Квебе к
Раль ф Джонсо н Урбана, шта т Иллиной с
Джо н Влиссиде с Готорн, шта т Нью-Йор к
Август, 1994
Вступительно е слов о
Люба я хорош о структурированна я объектно-ориентированна я архитектур а
изобилуе т паттернами. На само м деле, для меня одни м из критерие в качеств а
объектно-ориентированно й систем ы являетс я то, наскольк о внимательн о разра -
ботчик и отнеслис ь к типичны м взаимодействия м межд у участвующим и в ней
объектами. Есл и таки м механизма м на этап е проектировани я систем ы уделялос ь
достаточно е внимание, то архитектур а получаетс я боле е компактной, просто й
и понятной, чем в случае, когд а наличи е паттерно в игнорировалось.
Важност ь паттерно в при создани и сложны х систе м давн о осознан а в други х
дисциплинах. Так, Кристофе р Александ р и его сотрудники, возможно, впервы е
предложил и применят ь язык паттерно в для архитектурног о проектировани я зда-
ний и городов. Эти идеи, развитые зате м другим и исследователями, нын е глубок о
укоренилис ь в объектно-ориентированно м проектировании. В дву х слова х кон-
цепци я паттерн а проектировани я в программировани и - это ключ к использова -
нию разработчикам и опыт а высококвалифицированны х коллег.
В данно й работ е излагаютс я принцип ы применени я паттерно в проектирова -
ния и приводитс я катало г таки х паттернов. Тем самым книг а решае т сраз у две
задачи. Во-первых, она демонстрируе т роль паттерно в в проектировани и архитек -
туры сложны х систем. Во-вторых, содержи т практичны й справочни к удачн ы
паттернов, которы е разработчи к може т применит ь в собственны х приложени я
Мне выпал а чест ь работат ь вмест е с некоторым и авторам и книг и над пробле-
мами архитектурног о дизайна. Я многом у научилс я у этих люде й и полагаю, что
то же само е можн о буде т сказат ь и о вас, посл е тог о как вы прочтет е эту книгу.
Грейд и Буч,
Главны й научны й сотрудни к
Rationa l Softwar e Corporatio n
Предисловие
Совет ы читател ю
Книга состоит из двух частей. В главах 1 и 2 рассказывается, что такое паттер-
ны проектировани я и как с их помощью можно разрабатыват ь объектно-ориенти -
рованные программы. Практическо е применение паттернов проектировани я де-
монстрируетс я на примерах. Главы 3,4 и 5 - это каталог паттернов проектирования.
Каталог занимает большую часть книги. Три главы отражают деление паттер-
нов на категории: порождающи е паттерны, структурные паттерны и паттерны по-
ведения. Каталогом можно пользоватьс я по-разному: читать его с начала и до кон-
ца или переходит ь от одного паттерна к другому. Удачен и другой путь: тщательно
изучить любую главу, чтобы понять, чем отличаютс я тесно связанные между со-
бой паттерны.
Ссылки между паттернами дают возможност ь сориентироватьс я в каталоге.
Это также позволит уяснить, как различные паттерны связаны друг с другом, как
их можно сочетать между собой и какие паттерны могут хорошо работать совмест-
но. На рис. 1.1 связи между паттернами изображены графически.
Можно читать каталог, ориентируяс ь на конкретну ю задачу. В разделе 1.6 вы
найдете описание некоторых типичных задач, возникающих при проектировани и
повторно используемых объектно-ориентированны х программ. После этого пере-
ходите к изучению паттернов, предназначенны х для решения интересующе й вас
задачи. Некоторые предпочитают сначала ознакомитьс я со всем каталогом, а по-
том применит ь те или иные паттерны.
Если ваш опыт объектно-ориентированног о проектировани я невелик, начни-
те изучать самые простые и распространенны е паттерны:
а абстрактная фабрика;
а адаптер;
а компоновщик;
а декоратор;
а фабричный метод;
а наблюдатель;
а стратегия;
а шаблонный метод.
Трудно найти объектно-ориентированну ю систему, в которой не используютс я
хотя бы некоторые из указанных паттернов, а уж в больших системах встречаютс я
чуть ли не все. Разобравшис ь с этим подмножеством, вы получите представлени е
как о некоторых определенных паттернах, так и об объектно-ориентированно м про-
ектировании в целом.
Принятые в книге обозначения
Для облегчения работы с книгой издательством «ДМК Пресс» приняты следу-
ющие соглашения:
а коды программ, важные операторы, классы, объекты, методы и свой-
ства обозначены в книге специальным шрифтом (Courier), что позволяет
легко найти их в тексте;
а смысловые акценты, определения, термины, встретившиеся впервые, обозна-
чены курсивом;
а команды, пункты меню, кнопки, клавиши, функции выделены полужирным
шрифтом.
Глава 1. Введени е в паттерн ы
проектирования
Проектировани е объектно-ориентированны х програм м - нелегко е дело, а если их
нужно использовать повторно, то все становитс я еще сложнее. Необходим о подо-
брать подходящи е объекты, отнест и их к различны м классам, соблюда я разумну ю
степень детализации, определит ь интерфейс ы классо в и иерархи ю наследовани я
и установит ь существенны е отношени я межд у классами. Дизай н должен,
с одной стороны, соответствоват ь решаемо й задаче, с другой - быть общим, что-
бы удалос ь учесть все требования, которые могут возникнут ь в будущем. Хотелос ь
бы также избежат ь вовсе или, по крайне й мере, свести к минимум у необходимост ь
перепроектирования. Поднаторевши е в объектно-ориентированно м проектиро -
вании разработчик и скажу т вам, что обеспечит ь «правильный», то есть в доста-
точной мере гибкий и пригодны й для повторног о использовани я дизайн, с первог о
раза очень трудно, если вообще возможно. Прежде чем считат ь цель достигнутой,
они обычно пытаютс я опробоват ь найденно е решение на нескольки х задачах, и каж-
дый раз модифицирую т его.
И все же опытным проектировщика м удаетс я создат ь хороши й дизайн систе-
мы. В то же время новичк и испытываю т шок от количеств а возможны х варианто в
и нередко возвращаютс я к привычны м не объектно-ориентированны м методикам.
Проходи т немало времени перед тем, как становитс я понятно, что же такое удач-
ный объектно-ориентированны й дизайн. Опытны е проектировщики, очевидно,
знают какие-т о тонкости, ускользающи е от новичков. Так что же это?
Прежде всего, опытном у разработчик у понятно, что не нужно решат ь кажду ю
новую задачу с нуля. Вмест о этого он стараетс я повторн о воспользоватьс я теми
решениями, которые оказалис ь удачным и в прошлом. Отыска в хороше е решени е
один раз, он будет прибегат ь к нему снова и снова. Именн о благодар я накоплен -
ному опыту проектировщи к и становитс я эксрерто м в своей области. Во многих
объектно-ориентированны х система х вы встретит е повторяющиес я паттерны, со-
стоящие из классо в и взаимодействующи х объектов. С их помощь ю решаютс я
конкретны е задачи проектирования, в результат е чего объектно-ориентирован -
ный дизайн становитс я более гибким, элегантным, и им можно воспользоватьс я
повторно. Проектировщик, знакомы й с паттернами, может сразу же применят ь их
к решени ю новой задачи, не пытаяс ь каждый раз изобретат ь велосипед.
Поясним нашу мысль через аналогию. Писател и редко выдумываю т совершен -
но новые сюжеты. Вместо этого они берут за основу уже отработанны е в мирово й
литератур е схемы, жанр ы и образы. Например, трагически й герой - Макбет,
Введение в паттерны проектирования
Гамле т и т.д., моти в убийств а - деньги, месть, ревност ь и т.п. Точн о так же
в объектно-ориентированно м проектировани и используютс я такие паттерны, как
«представлени е состояни я с помощь ю объектов » или «декорировани е объектов,
чтобы было проще добавлят ь и удалят ь их свойства».
Все мы знае м о ценност и опыта. Скольк о раз при проектировани и вы испыты -
вали дежавю, чувствуя, что уже когда-т о решал и таку ю же задачу, тольк о ника к
не сообразить, когд а и где? Если бы удалос ь вспомнит ь детал и старо й задач и и ее
решения, то не пришлос ь бы придумыват ь все заново. Увы, у нас нет привычк и
записыват ь свой опыт на благ о други м людя м да и себе тоже.
Цель этой книг и состои т как раз в том, чтоб ы документироват ь опыт разра -
ботки объектно-ориентированны х програм м в виде паттернов проектирования.
Каждом у паттерн у мы присвои м имя, объясни м его назначени е и роль в проекти -
ровани и объектно-ориентированны х систем. Некоторы е из наиболе е распростра -
ненны х паттерно в формализован ы и сведен ы в едины й каталог.
Паттерн ы проектировани я упрощаю т повторно е использовани е удачны х про-
ектны х и архитектурны х решений. Представлени е прошедши х проверк у време -
нем методи к в виде паттерно в проектировани я облегчае т досту п к ним со сторо -
ны разработчико в новых систем. С помощь ю паттерно в можн о улучшит ь качеств о
документаци и и сопровождени я существующи х систем, позволя я явно описат ь
взаимодействи я классо в и объектов, а также причины, по которы м систем а была
построен а так, а не иначе. Проще говоря, паттерн ы проектировани я дают разра -
ботчик у возможност ь быстре е найт и «правильный » путь.
Как уже было сказано, в книг у включен ы тольк о такие паттерны, которы е неод-
нократн о применялис ь в разны х системах. По больше й част и они никогд а ране е
не документировалис ь и либо известн ы самым квалифицированны м специалис -
там по объектно-ориентированном у проектированию, либо были часть ю какой -
то удачно й системы.
Хотя книг а получилас ь довольн о объемной, паттерн ы проектировани я - лишь
мала я част ь того, что необходим о знат ь специалист у в этой области. В издани е не
включен о описани е паттернов, имеющи х отношени е к параллельности, распреде -
ленном у программировани ю и программировани ю систе м реальног о времени. От-
сутствую т и сведени я о паттернах, специфичны х для конкретны х предметны х об-
ластей. Из этой книг и вы не узнаете, как строит ь интерфейс ы пользователя, как
писат ь драйвер ы устройст в и как работат ь с объектно-ориентированным и базами
данных. В каждо й из этих областе й есть свои собственны е паттерны, и, може т быть,
кто-то их и систематизирует.
1.1. Чт о тако е паттер н проектировани я
По слова м Кристофер а Александра, «любо й паттер н описывае т задачу, котора я
снова и снов а возникае т в наше й работе, а также принци п ее решения, приче м та-
ким образом, что это решени е можн о пото м использоват ь миллио н раз, ничег о не
изобрета я заново » [AIS+77]. Хот я Александ р имел в виду паттерны, возникающи е
при проектировани и здани й и городов, но его слов а верны и в отношени и паттер -
нов объектно-ориентированног о проектирования. Наши решени я выражаютс я
Что такое паттерн проектирования
в терминах объектов и интерфейсов, а не стен и дверей, но в обоих случаях смысл
паттерна - предложить решение определенной задачи в конкретном контексте.
В общем случае паттерн состоит из четырех основных элементов:
1. Имя. Сославшись на него, мы можем сразу описать проблему проектирова-
ния; ее решения и их последствия. Присваивание паттернам имен позволяет
проектировать на более высоком уровне абстракции. С помощью словаря
паттернов можно вести обсуждение с коллегами, упоминать паттерны в до-
кументации, в тонкостях представлять дизайн системы. Нахождение хоро-
ших имен было одной из самых трудных задач при составлении каталога.
2. Задача. Описание того, когда следует применять паттерн. Необходимо сфор-
мулировать задачу и ее контекст. Может описываться конкретная проблема
проектирования, например способ представления алгоритмов в виде объек-
тов. Иногда отмечается, какие структуры классов или объектов свидетель-
ствуют о негибком дизайне. Также может включаться перечень условий, при
выполнении которых имеет смысл применять данный паттерн.
3. Решение. Описание элементов дизайна, отношений между ними, функций
каждого элемента. Конкретный дизайн или реализация не имеются в виду,
поскольку паттерн - это шаблон, применимый в самых разных ситуациях.
Просто дается абстрактное описание задачи проектирования и того, как она
может быть решена с помощью некоего весьма обобщенного сочетания эле-
ментов (в нашем случае классов и объектов).
4. Результаты - это следствия применения паттерна и разного рода компро-
миссы. Хотя при описании проектных решений о последствиях часто не упо-
минают, знать о них необходимо, чтобы можно было выбрать между различ-
ными вариантами и оценить преимущества и недостатки данного паттерна.
Здесь речь идет и о выборе языка и реализации. Поскольку в объектно-ори-
ентированном проектировании повторное использование зачастую является
важным фактором, то к результатам следует относить и влияние на степень
гибкости, расширяемости и переносимости системы. Перечисление всех по-
следствий поможет вам понять и оценить их роль.
То, что один воспринимает как паттерн, для другого просто строительный
блок. В этой книге мы рассматриваем паттерны на определенном уровне абстрак-
ции. Паттерны проектирования - это не то же самое, что связанные списки или
хэш-таблицы, которые можно реализовать в виде класса и повторно использовать
без каких бы то ни было модификаций. Но это и не сложные, предметно-ориен-
тированные решения для целого приложения или подсистемы. Здесь под паттерна-
ми проектирования понимается описание взаимодействия объектов и классов, адап-
тированных для решения общей задачи проектирования в конкретном контексте.
Паттерн проектирования именует, абстрагирует и идентифицирует ключевые
аспекты структуры общего решения, которые и позволяют применить его для со-
здания повторно используемого дизайна. Он вычленяет участвующие классы
и экземпляры, их роль и отношения, а также функции. При описании каждого пат-
терна внимание акцентируется на конкретной задаче объектно-ориентированно-
го проектирования. Анализируется,- когда следует применять паттерн, можно ли
Введение в паттерны проектировани я
его использоват ь с учетом других проектных ограничений, каковы будут послед-
ствия применени я метода. Поскольк у любой проект в конечном итоге предстоит
реализовывать, в состав паттерна включаетс я пример кода на языке C++ (иногда
на Smalltalk), иллюстрирующег о реализацию.
Хотя, строго говоря, паттерны используютс я в проектировании, они основа-
ны на практически х решениях, реализованны х на основных языках объектно-ори -
ентированног о программировани я типа Smalltal k и C++, а не на процедурны х
(Pascal, С, Ada и т.п.) или объектно-ориентированны х языках с динамическо й ти-
пизацие й (CLOS, Dylan, Self). Мы выбрали Smalltal k и C++ из прагматически х
соображений, поскольк у чаще всего работаем с ними и поскольк у они завоевыва -
ют все большую популярность.
Выбор языка программировани я безусловн о важен. В наших паттерна х под-
разумеваетс я использовани е возможносте й Smalltal k и C++, и от этого зависит,
что реализоват ь легко, а что - трудно. Если бы мы ориентировалис ь на проце-
дурные языки, то включил и бы паттерны наследование, инкапсуляци я и по-
лиморфизм. Некоторые из наших паттерно в напряму ю поддерживаютс я менее
распространенным и языками. Так, в языке CLOS есть мультиметоды, которые де-
лают ненужным паттерн посетитель. Собственно, даже между Smalltal k и C++
есть много различий, из-за чего некоторые паттерны проще выражаютс я на одном
языке, чем на другом (см., например, паттерн итератор).
1.2. Паттерн ы проектировани я в схем е MV C
в язык е Smalltal k
В Smalltalk-8 0 для построени я интерфейсо в пользовател я применяетс я трой-
ка классов модель/вид/контролле р (Model/View/Controlle r - MVC) [KP88].
Знакомств о с паттернами проектирования, встречающимис я в схеме MVC, помо-
жет вам разобратьс я в том, что мы понимае м под словом «паттерн».
MVC состоит из объекто в трех видов. Модель - это объект приложения,
а вид - экранно е представление. Контроллер описывает, как интерфей с реагиру -
ет на управляющи е воздействи я пользователя. До появлени я схемы MVC эти
объекты в пользовательски х интерфейса х смешивались. MVC отделяе т их друг
от друга, за счет чего повышаетс я гибкост ь и улучшаютс я возможност и повтор-
ного использования.
М VC отделяе т вид от модели, устанавлива я между ними протокол взаимодей -
ствия «подписка/оповещение». Вид должен гарантировать, что внешне е представ -
ление отражае т состояние модели. При каждом изменени и внутренни х данных
модель оповещае т все зависящи е от нее виды, в результат е чего вид обновляе т
себя. Такой подход позволяе т присоединит ь к одной модели нескольк о видов,
обеспечив тем самым различные представления. Можно создать новый вид, не
переписыва я модель.
На рисунке ниже показана одна модель и три вида. (Для простот ы мы опусти-
ли контроллеры.) Модель содержит некоторые данные, которые могут быть пред-
ставлены в виде электронно й таблицы, гистограмм ы и кругово й диаграммы.
Паттерн ы проектировани я в схеме MVC
Виды
Модель
Модел ь оповещае т свои виды при каждо м изменени и значени й данных, а виды
обращаютс я к модел и для получени я новых значений.
На первы й взгляд, в этом пример е продемонстрирова н прост о дизайн, отделя -
ющий вид от модели. Но тот же принци п примени м и к боле е обще й задаче: разде -
ление объекто в таки м образом, что изменени е одног о отражаетс я сраз у на не-
скольки х других, приче м изменившийс я объек т не имее т информаци и о деталя х
реализаци и объектов, на которы е он оказа л воздействие. Этот боле е общи й под-
ход описываетс я паттерно м проектировани я наблюдатель.
Еще одно свойств о MVC заключаетс я в том, что виды могу т быть вложенными.
Например, панел ь управления, состоящу ю из кнопок, допустим о представит ь как
составно й вид, содержащи й вложенные, - по одной кнопк е на каждый. Пользователь -
ский интерфей с инспектор а объекто в може т состоят ь из вложенны х видов, исполь -
зуемых также и в отладчике. MVC поддерживае т вложенны е виды с помощь ю класс а
CompositeView, являющегос я подклассо м View. Объект ы класс а CompositeVie w
ведут себя так же, как объект ы класс а View, поэтом у могу т использоватьс я всюду, где
и виды. Но еще они могу т содержат ь вложенны е виды и управлят ь ими.
Здесь можн о было бы считать, что этот дизай н позволяе т обращатьс я с состав -
ным видом, как с любым из его компонентов. Но тот же дизай н примени м и в ситу -
ации, когд а мы хотим имет ь возможност ь группироват ь объект ы и рассматриват ь
групп у как отдельны й объект. Такой подхо д описываетс я паттерно м компоновщик.
Он позволяе т создават ь иерархи ю классов, в которо й некоторы е подкласс ы опре -
деляют примитивны е объект ы (например, Butto n - кнопка), а други е - составны е
объект ы (CompositeView), группирующи е примитив ы в более сложны е структуры.
MVC позволяе т также изменят ь реакци ю вида на действи я пользователя. При
этом визуально е представлени е остаетс я прежним. Например, можн о изменит ь
реакци ю на нажати е клавиш и или использоват ь всплывающи е меню вмест о ко-
мандны х клавиш. MVC инкапсулируе т механиз м определени я реакци и в объект е
Controller. Существуе т иерархи я классо в контроллеров, и это позволяе т без
труда создат ь новый контролле р как.вариан т уже существующего.
Введение в паттерны проектировани я
Вид пользуется экземпляром класса, производного от Controller, для реа-
лизации конкретной стратегии реагирования. Чтобы реализоват ь иную страте-
гию, нужно просто подставит ь другой контроллер. Можно даже заменить кон-
троллер вида во время выполнени я программы, изменив тем самым реакцию на
действия пользователя. Например, вид можно деактивировать, так что он вообще
не будет ни на что реагировать, если передать ему контроллер, игнорирующи й
события ввода.
Отношение вид-контролле р - это пример паттерна проектировани я страте-
гия. Стратегия - это объект для представлени я алгоритма. Он полезен, когда вы
хотите статически или динамическ и подменит ь один алгоритм другим, если су-
ществует много вариантов одного алгоритма или когда с алгоритмо м связаны
сложные структуры данных, которые хотелось бы инкапсулировать.
В МУС используютс я и другие паттерны проектирования, например фабрич-
ный метод, позволяющий задать для вида класс контроллера по умолчанию, и де-
коратор для добавления к виду возможност и прокрутки. Но основные отношения
в схеме МУС описываютс я паттернами наблюдатель, компоновщи к и стратегия.
1.3. Описание паттернов проектирования
Как мы будем описыват ь паттерны проектирования? Графических обозначе-
ний недостаточно. Они просто символизируют конечный продукт процесса про-
ектирования в виде отношений между классами и объектами. Чтобы повторно
воспользоватьс я дизайном, нам необходимо документироват ь решения, альтерна-
тивные варианты и компромиссы, которые привели к нему. Важны также конкрет-
ные примеры, поскольку они позволяют увидеть применение паттерна.
При описании паттернов проектировани и мы будем придерживатьс я единого
принципа. Описание каждого паттерна разбито на разделы, перечисленные ниже.
Такой подход позволяет единообразно представит ь информацию, облегчает изу-
чение, сравнение и применение паттернов.
Название и классификация паттерна
Название паттерна должно четко отражать его назначение. Классификаци я пат-
тернов проводится в соответствии со схемой, которая изложена в разделе 1.5.
Назначение
Лаконичный ответ на следующие вопросы: каковы функции паттерна, его обос-
нование и назначение, какую конкретну ю задачу проектировани я можно решить
с его помощью.
Известен также под именем
Другие распространенны е названия паттерна, если таковые имеются.
Мотивация
Сценарий, иллюстрирующи й задачу проектировани я и то, как она решается
данной структурой класса или объекта. Благодаря мотивации можно лучше по-
нять последующее, более абстрактное описание паттерна.
Описание паттернов проектирования
Применимость
Описание ситуаций, в которых можно применять данный паттерн. Примеры
проектирования, которые можно улучшить с его помощью. Распознавание таких
ситуаций.
Структура
Графическое представление классов в паттерне с использование м нотации,
основанной на методике Object Modeling Technique (OMT) [RBP+91]. Мы поль-
зуемся также диаграммами взаимодействий [JCJO92, Воо94] для иллюстрации
последовательносте й запросов и отношений между объектами. В приложении В эта
нотация описывается подробно.
Участники
Классы или объекты, задействованные в данном паттерне проектирования,
и их функции.
Отношения
Взаимодействие участников для выполнения своих функций.
Результаты
Насколько паттерн удовлетворяе т поставленным требованиям? Результаты
применения, компромиссы, на которые приходится идти. Какие аспекты поведе-
ния системы можно независимо изменять, используя данный паттерн?
Реализация
Сложности и так называемые подводные камни при реализации паттерна.
Советы и рекомендуемые приемы. Есть ли у данного паттерна зависимость от язы-
ка программирования?
Пример кода
Фрагмент кода, иллюстрирующий вероятную реализацию на языках C++ или
Smalltalk.
Известные применения
Возможности применения паттерна в реальных системах. Даются, по меньшей
мере, два примера из различных областей.
Родственные паттерны
Связь других паттернов проектирования с данным. Важные различия. Ис-
пользование данного паттерна в сочетании с другими.
В приложениях содержится информация, которая поможет вам лучше понять
паттерны и связанные с ними вопросы. Приложение А представляет собой глос-
сарий употребляемых нами терминов. В уже упомянутом приложении В дано опи-
сание разнообразных нотаций. Некоторые аспекты применяемой нотации мы по-
ясняем по мере ее появления в тексте книги. Наконец, в приложении С приведен
исходный код базовых классов, встречающихс я в примерах.
Введение в паттерны проектировани я
1.4. Катало г паттерно в проектировани я
Каталог содержит 23 паттерна. Ниже для удобства перечислен ы их имена и на-
значение. В скобках после названия каждог о паттерна указан номер страницы,
откуда начинаетс я его подробное описание.
Abstract Factory (абстрактная фабрика) (93)
Предоставляе т интерфейс для создания семейств, связанных между собой,
или независимых объектов, конкретные классы которых неизвестны.
Adapter (адаптер) (141)
Преобразуе т интерфейс класса в некоторый другой интерфейс, ожида-
емый клиентами. Обеспечивае т совместну ю работу классов, котора я
была бы невозможн а без данног о паттерна из-за несовместимост и ин-
терфейсов.
Bridge (мост) (152)
Отделяет абстракцию от реализации, благодаря чему появляетс я возмож-
ность независимо изменят ь то и другое.
Builder (строитель) (103)
Отделяет конструировани е сложног о объекта от его представления, позво-
ляя использоват ь один и тот же процесс конструировани я для создания
различных представлений.
Chain of Responsibility (цепочка обязанностей) (217)
Можно избежат ь жесткой зависимост и отправител я запроса от его полу-
чателя, при этом запросом начинае т обрабатыватьс я один из нескольки х
объектов. Объекты-получател и связываютс я в цепочку, и запрос переда-
ется по цепочке, пока какой-т о объект его не обработает.
Command (команда) (226)
Инкапсулируе т запрос в виде объекта, позволяя тем самым параметризо -
вывать клиентов типом запроса, устанавливат ь очередност ь запросов, про-
токолироват ь их и поддерживат ь отмену выполнени я операций.
Composite (компоновщик) (162)
Группируе т объекты в древовидны е структур ы для представлени я иерар-
хий типа «часть-целое». Позволяе т клиента м работать с единичным и объ-
ектами так же, как с группами объектов.
Decorator (декоратор) (173)
Динамическ и возлагае т на объект новые функции. Декоратор ы применя-
ются для расширени я имеющейс я функциональност и и являютс я гибкой
альтернативо й порождени ю подклассов.
Facade (фасад) (183)
Предоставляе т унифицированны й интерфейс к множеств у интерфейсо в
в некоторо й подсистеме. Определяе т интерфейс более высоког о уровня,
облегчающи й работу с подсистемой.
Каталог паттернов проектирования
Factory Method (фабричный метод) (111)
Определяет интерфейс для создания объектов, при этом выбранный класс
инстанцируется подклассами.
Flyweight (приспособленец) (191)
Использует разделение для эффективной поддержки большого числа мел-
ких объектов.
Interpreter (интерпретатор) (236)
Для заданного языка определяет представление его грамматики, а также
интерпретатор предложений языка, использующий это представление.
Iterator (итератор) (173)
Дает возможность последовательно обойти все элементы составного
объекта, не раскрывая его внутреннего представления.
Mediator (посредник) (263)
Определяет объект, в котором инкапсулировано знание о том, как взаимо-
действуют объекты из некоторого множества. Способствует уменьшению
числа связей между объектами, позволяя им работать без явных ссылок
друг на друга. Это, в свою очередь, дает возможность независимо изменять
схему взаимодействия.
Memento (хранитель) (272)
Позволяет, не нарушая инкапсуляции, получить и сохранить во внешней
памяти внутреннее состояние объекта, чтобы позже объект можно было
восстановить точно в таком же состоянии.
Observer (наблюдатель) (281)
Определяет между объектами зависимость типа один-ко-многим, так что
при изменении состоянии одного объекта все зависящие от него получают
извещение и автоматически обновляются.
Prototype (прототип) (121)
Описывает виды создаваемых объектов с помощью прототипа и создает
новые объекты путем его копирования.
Proxy (заместитель) (203)
Подменяет другой объект для контроля доступа к нему.
Singleton (одиночка) (130)
Гарантирует, что некоторый класс может иметь только один экземпляр,
и предоставляет глобальную точку доступа к нему.
State (состояние)(291)
Позволяет объекту варьировать свое поведение при изменении внутренне-
го состояния. При этом создается впечатление, что поменялся класс объекта.
Strategy (стратегия) (300)
Определяет семейство алгоритмов, инкапсулируя их все и позволяя под-
ставлять один вместо другого. Можно менять алгоритм независимо от
клиента, который им пользуется.
введение в паттерны проектирования
Template Method (шаблонный метод) (309)
Определяет скелет алгоритма, перекладывая ответственность за некото-
рые его шаги на подклассы. Позволяет подклассам переопределять шаги
алгоритма, не меняя его общей структуры.
Visitor (посетитель) (314)
Представляет операцию, которую надо выполнить над элементами объек-
та. Позволяет определить новую операцию, не меняя классы элементов,
к которым он применяется.
1.5. Организаци я каталог а
Паттерны проектирования различаются степенью детализации и уровнем аб-
стракции и должны быть каким-то образом организованы. В данном разделе опи-
сывается классификация, позволяющая ссылаться на семейства взаимосвязанных
паттернов. Она поможет быстрее освоить паттерны, описываемые в каталоге,
и укажет направление поиска новых.
Мы будем классифицироват ь паттерны по двум критериям (табл. 1.1). Пер-
вый - цель - отражает назначение паттерна. В связи с этим выделяются порожда-
ющие паттерны, структурные паттерны и паттерны поведения. Первые связаны
с процессом создания объектов. Вторые имеют отношение к композиции объек-
тов и классов. Паттерны поведения характеризуют то, как классы или объекты
взаимодействуют между собой.
Таблица 1.1. Пространство паттернов проектирования
Порождающие
паттерны
Структурные
паттерны
Паттерны
поведения
Класс
Фабричный метод
Адаптер (класса)
Интерпретатор
Шаблонный метод
Объект
Абстрактная фабрика
Одиночка
Прототип
Строитель
Адаптер (объекта)
Декоратор
Заместитель
Компоновщик
Мост
Приспособленец
Фасад
Итератор
Команда
Наблюдатель
Посетитель
Посредник
Состояние
Стратегия
Хранитель
Цепочка обязанностей
Второй критерий - уровень - говорит о том, к чему обычно применяется пат-
терн: к объектам или классам. Паттерны уровня классов описывают отношения
между классами и их подклассами. Такие отношения выражаются с помощью на-
следования, поэтому они статичны, то есть зафиксированы на этапе компиляции.
Паттерны уровня объектов описывают отношения между объектами, которые
могут изменяться во время выполнения и потому более динамичны. Почти все
паттерны в какой-то мере используют наследование. Поэтому к категории «пат-
терны классов» отнесены только те, что сфокусированы лишь на отношениях меж-
ду классами. Обратите внимание: большинство паттернов действуют на уровне
объектов.
Порождающие паттерны классов частично делегируют ответственность за соз-
дание объектов своим подклассам, тогда как порождающие паттерны объектов
передают ответственность другому объекту. Структурные паттерны классов ис-
пользуют наследование для составления классов, в то время как структурные пат-
терны объектов описывают способы сборки объектов из частей. Поведенческие
паттерны классов используют наследование для описания алгоритмов и потока
управления, а поведенческие паттерны объектов описывают, как объекты, принад-
лежащие некоторой группе, совместно функционируют и выполняют задачу, ко-
торая ни одному отдельному объекту не под силу.
Существуют и другие способы классификации паттернов. Некоторые паттер-
ны часто используются вместе. Например, компоновщик применяется с итера-
тором или посетителем. Некоторыми паттернами предлагаются альтернативные
решения. Так, прототип нередко можно использовать вместо абстрактной фаб-
рики. Применение части паттернов приводит к схожему дизайну, хотя изначаль-
но их назначение различно. Например, структурные диаграммы компоновщика
и декоратора похожи.
Классифицировать паттерны можно и по их ссылкам (см. разделы «Родствен-
ные паттерны»). На рис. 1.1 такие отношения изображены графически.
Ясно, что организовать паттерны проектирования допустимо многими спосо-
бами. Оценивая паттерны с разных точек зрения, вы глубже поймете, как они функ-
ционируют, как их сравнивать и когда применять тот или другой.
1.6. Ка к решат ь задач и проектировани я
с помощь ю паттерно в
Паттерны проектирования позволяют разными способами решать многие за-
дачи, с которыми постоянно сталкиваются проектировщики объектно-ориенти-
рованных приложений. Поясним эту мысль примерами.
Поиск подходящих объектов
Объектно-ориентированные программы состоят из объектов. Объект сочета-
ет данные и процедуры для их обработки. Такие процедуры обычно называют ме-
тодами или операциями. Объект выполняет операцию, когда получает запрос (или
сообщение) от клиента.
Посылка запроса - это единственный способ заставить объект выполнить опе-
рацию. А выполнение операции - единственный способ изменить внутреннее со-
стояние объекта. Имея в виду два эти ограничения, говорят, что внутреннее со-
стояние объекта инкапсулировано: к нему нельзя получить непосредственный
доступ, то есть представление объекта закрыто от внешней программы.
Введение в паттерны проектировани я
Самая трудная задача в объектно-ориентированно м проектировани и - разло-
жить систему на объекты. При решении приходитс я учитыват ь множество факто-
ров: инкапсуляцию, глубину детализации, наличие зависимостей, гибкость, про-
изводительность, развитие, повторное использовани е и т.д. и т.п. Все это влияет
на декомпозицию, причем часто противоречивы м образом.
Методики объектно-ориентированног о проектировани я отражают разные под-
ходы. Вы можете сформулироват ь задачу письменно, выделит ь из получившейс я
Как решат ь задач и проектировани я
фразы существительны е и глаголы, после чего создат ь соответствующи е класс ы
и операции. Друго й пут ь - сосредоточитьс я на отношения х и разделени и обязан -
носте й в системе. Можн о построит ь модел ь реальног о мира или перенест и выяв -
ленные при анализ е объект ы на свой дизайн. Согласи е по повод у того, какой под-
ход самый лучший, никогд а не будет достигнуто.
Многи е объект ы возникаю т в проект е из построенно й в ходе анализ а модели.
Но нередк о появляютс я и классы, у которых нет прототипо в в реально м мире. Это
могут быть класс ы как низког о уровня, наприме р массивы, так и высокого. Пат-
терн компоновщи к вводи т таку ю абстракци ю для единообразно й трактовк и объ-
ектов, у которо й нет физическог о аналога. Если придерживатьс я строгог о моде-
лировани я и ориентироватьс я тольк о на реальны й мир, то получитс я система,
отражающа я сегодняшни е потребности, но, возможно, не учитывающа я будущег о
развития. Абстракции, возникающи е в ходе проектирования, - ключ к гибком у
дизайну.
Паттерн ы проектировани я помогаю т выявит ь не вполн е очевидны е абстрак -
ции и объекты, которые могут их использовать. Например, объектов, представля -
ющих процес с или алгоритм, в действительност и нет, но они являютс я неотъем -
лемым и составляющим и гибког о дизайна. Паттер н стратеги я описывае т спосо б
реализаци и взаимозаменяемы х семейст в алгоритмов. Паттер н состояни е позво -
ляет представит ь состояни е некоторо й сущност и в виде объекта. Эти объект ы ред-
ко появляютс я во время анализ а и даже на ранни х стадия х проектирования. Ра-
бота с ними начинаетс я позже, при попытка х сделат ь дизай н боле е гибки м
и пригодны м для повторног о использования.
Определение степени детализации объекта
Размер ы и числ о объекто в могу т сильн о варьироваться. С их помощь ю може т
быть представлен о все, начина я с уровн я аппаратур ы и до законченны х приложе -
ний. Как же решить, что долже н представлят ь собой объект?
Здесь и потребуютс я паттерн ы проектирования. Паттер н фаса д показывает,
как представит ь в виде объект а целые подсистемы, а паттер н приспособлене ц -
как поддержат ь большо е число объекто в при высоко й степен и детализации. Дру-
гие паттерн ы указываю т пут ь к разложени ю объект а на меньши е подобъекты.
Абстрактна я срабрик а и строител ь описываю т объекты, единственно й цель ю ко-
торых являетс я создани е други х объектов, а посетител ь и команд а - объекты,
отвечающи е за реализаци ю запрос а к другом у объект у или группе.
Специфицирование интерфейсов объекта
При объявлени и объекто м любо й операци и должн ы быть заданы: имя опера -
ции, объекты, передаваемы е в качеств е параметров, и значение, возвращаемо е опе-
рацией. Эту триад у называю т сигнатурой операции. Множеств о сигнату р всех
определенны х для объект а операци й называетс я интерфейсом этог о объекта. Ин-
терфей с описывае т все множеств о запросов, которы е можн о отправит ь объекту.
Любой запрос, сигнатур а которог о соответствуе т интерфейс у объекта, може т быть
ему послан.
Тип — это имя, используемое для обозначения конкретног о интерфейса. Гово-
рят, что объект имеет тип Window, если он готов принимать запросы на выполне-
ние любых операций, определенных в интерфейсе с именем Window. У одного
объекта может быть много типов. Напротив, сильно отличающиеся объекты мо-
гут разделять общий тип. Часть интерфейса объекта может быть охарактеризова -
на одним типом, а часть - другим. Два объекта одного и того же типа должны раз-
делять только часть своих интерфейсов. Интерфейсы могут содержать другие
интерфейсы в качестве подмножеств. Мы говорим, что один тип является подти-
пом другого, если интерфейс первого содержит интерфейс второго. В этом случае
второй тип называется супертипом для первого. Часто говорят также, что подтип
наследует интерфейс своего супертцпа.
В объектно-ориентированны х системах интерфейсы фундаментальны. Об
объектах известно только то, что они сообщают о себе через свои интерфейсы.
Никакого способа получить информацию об объекте или заставить его что-то сде-
лать в обход интерфейса не существует. Интерфейс объекта ничего не говорит
о его реализации; разные объекты вправе реализовыват ь сходные запросы совер-
шенно по-разному. Это означает, что два объекта с различными реализациями
могут иметь одинаковые интерфейсы.
Когда объекту посылается запрос, то операция, которую он будет выполнять,
зависит как от запроса, так и от объекта-адресата. Разные объекты, поддерживаю-
щие одинаковые интерфейсы, могут выполнять в ответ на такие запросы разные
операции. Ассоциация запроса с объектом и одной из его операций во время вы-
полнения называется динамическим связыванием.
Динамическое связывание означает, что отправка некоторого запроса не опре-
деляет никакой конкретной реализации до момента выполнения. Следовательно,
допустимо написать программу, которая ожидает объект с конкретным интерфей-
сом, точно зная, что любой объект с подходящим интерфейсом сможет принять
этот запрос. Более того, динамическое связывание позволяет во время выполне-
ния подставить вместо одного объекта другой, если он имеет точно такой же ин-
терфейс. Такая взаимозаменяемост ь называется полиморфизмом и является важ-
нейшей особенностью объектно-ориентированны х систем. Она позволяет клиенту
не делать почти никаких предположений об объектах, кроме того, что они поддер-
живают определенный интерфейс. Полиморфиз м упрощает определение клиен-
тов, позволяет отделить объекты друг от друга и дает объектам возможность из-
менять взаимоотношения во время выполнения.
Паттерны проектирования позволяют определять интерфейсы, задавая их ос-
новные элементы и то, какие данные можно передавать через интерфейс. Паттерн
может также «сказать», что не должно проходить через интерфейс. Хорошим при-
мером в этом отношении является хранитель. Он описывает, как инкапсулироват ь
и сохранить внутреннее состояние объекта таким образом, чтобы в будущем его
можно было восстановить точно в таком же состоянии. Объекты, удовлетворяю-
щие требованиям паттерна хранитель, должны определить два интерфейса: один
ограниченный, который позволяет клиентам держать у себя и копировать храните-
ли, а другой привилегированный, которым может пользоваться только сам объект
для сохранения и извлечения информации о состоянии их хранителя.
Как решать задачи проектирования
Паттерны проектирования специфицируют также отношения между интер-
фейсами. В частности, нередко они содержат требование, что некоторые классы
должны иметь схожие интерфейсы, а иногда налагают ограничения на интерфей-
сы классов. Так, декоратор и заместитель требуют, чтобы интерфейсы объектов
этих паттернов были идентичны интерфейсам декорируемых и замещаемых объ-
ектов соответственно. Интерфейс объекта, принадлежащег о паттерну посети-
тель, должен отражать все классы объектов, с которыми он будет работать.
Специфицирование реализации объектов
До сих пор мы почти ничего не сказали о том, как же в действительност и опре-
деляется объект. Реализация объекта определяется его классом. Класс специфи-
цирует внутренние данные объекта и его представление, а также операции, кото-
рые объект может выполнять.
В нашей нотации, основанной на ОМТ (см. приложение В), класс изобража-
ется в виде прямоугольника, внутри которого жирным шрифтом написано имя
класса. Ниже обычным шрифтом перечислены операции. Любые данные, которые
определены для класса, следуют после операций. Имя класса, операции и данные
разделяются горизонтальными линиями.
Типы возвращаемог о значения и переменных экземпляра необязательны,
поскольку мы не ограничивае м себя языками программировани я с сильной ти-
пизацией.
Объекты создаются с помощью инстанцирования клас-
са. Говорят, что объект является экземпляром класса. В про-
цессе инстанцирования выделяется память для переменных
экземпляра (внутренних данных объекта), и с этими данны-
ми ассоциируютс я операции. С помощью инстанцирования
одного класса можно создать много разных объектов-эк-
земпляров.
Пунктирная линия со стрелкой обозначает класс, который инстанцируе т
объекты другого класса. Стрелка направлена в сторону класса инстанцированно -
го объекта.
Новые классы можно определить в терминах существую-
щих с помощью наследования классов. Если подкласс наследу-
ет родительскому классу, то он включает определения всех
данных и операций, определенных в родительском классе.
Объекты, являющиеся экземплярами подкласса, будут со-
держать все данные, определенные как в самом подклассе, так
и во всех его родительских классах. Такой объект сможет
выполнять все операции, определенные в подклассе и его предках. Отношение «яв-
ляется подклассом» обозначается вертикальной линией с треугольником.
Класс называется абстрактным, если его единственное назначение - опреде-
лить общий интерфейс для всех своих подклассов. Абстрактный класс делегирует
Введени е в паттерн ы проектировани я
реализацию всех или части своих операции подклассам, поэтому у него не может
быть экземпляров. Операции, объявленные, но не реализованные в абстрактном
классе, называются абстрактными. Класс, не являющийся абстрактным, называ-
ется конкретным.
Подклассы могут уточнять или переопределять поведение своих предков. Точ-
нее, класс может заместить операцию, определенную в родительском классе. За-
мещение дает подклассам возможность обрабатывать запросы, адресованные ро-
дительским классам. Наследование позволяет определять новые классы, просто
расширяя возможности старых. Тем самым можно без труда определять семей-
ства объектов со схожей функциональностью.
Имена абстрактных классов оформлены курсивом, чтобы отличать их от кон-
кретных. Курсив используется также для обозначения абстрактных операций. На
диаграмме может изображаться псевдокод, описывающий реализацию операции;
в таком случае код представлен в прямоугольнике с загнутым уголком, соединен-
ном пунктирной линией с операцией, которую он реализует.
Подмешанным (mixin class) называется класс, назначение которого - предоста-
вить дополнительный интерфейс или функциональность другим классам. Он род-
ственен абстрактным классам в том смысле, что не предполагает непосредственного
инстанцирования. Для работы с подмешанными классами необходимо множествен-
ное наследование.
Наследование класса и наследование интерфейса
Важно понимать различие между классом объекта и его типом.
Класс объекта определяет, как объект реализован, то есть внутреннее состояние
и реализацию операций объекта. Напротив, тип относится только к интерфейсу
Как решат ь задач и проектировани я
объект а - множеств у запросов, на которы е объек т отвечает. У объект а може т быть
мног о типов, и объект ы разных классо в могу т имет ь один и тот же тип.
Разумеется, межд у классо м и типо м есть тесна я связь. Поскольк у класс опре-
деляет, какие операци и може т выполнят ь объект, то заодно он определяе т и его
тип. Когд а мы говори м «объек т являетс я экземпляро м класса», то подразумеваем,
что он поддерживае т интерфейс, определяемы й этим классом.
В языка х вроде C++ и Eiffe l класс ы используютс я для специфицирования,
типа и реализаци и объекта. В программа х на языке Smalltal k типы переменны х не
объявляются, поэтом у компилято р не проверяет, что тип объекта, присваиваемо -
го переменной, являетс я подтипо м типа переменной. При отправк е сообщени я
необходим о проверять, что клас с получател я реализуе т реакци ю на сообщение,
но проверк а того, что получател ь являетс я экземпляро м определенног о класса,
не нужна.
Важно также понимат ь различи е межд у наследование м класс а и наследовани -
ем интерфейс а (или порождение м подтипов). В случа е наследовани я класс а реа-
лизаци я объект а определяетс я в термина х реализаци и другог о объекта. Проще
говоря, это механиз м разделени я кода и представления. Напротив, наследовани е
интерфейс а (порождени е подтипов ) описывает, когда один объек т можно исполь -
зовать вместо другого.
Две эти концепци и легко спутать, поскольк у во многи х языка х явное разли-
чие отсутствует. В таких языках, как C++ и Eiffel, под наследование м понимаетс я
одновременн о наследовани е интерфейс а и реализации. Стандартны й спосо б реа-
лизаци и наследовани я интерфейс а в C++ - это открыто е наследовани е классу,
в которо м есть исключительн о виртуальны е функции. Истинно е наследовани е
интерфейс а можн о аппроксимироват ь в C++ с помощь ю открытог о наследовани я
абстрактном у классу. Истинно е наследовани е реализаци и или класс а аппроксими -
руется с помощь ю закрытог о наследования. В Smalltal k под наследование м пони-
мается тольк о наследовани е реализации. Переменно й можно присвоит ь экземпля -
ры любог о класс а при условии, что они поддерживаю т операции, выполняемы е над
значение м этой переменной.
Хотя в большинств е языко в программировани я различи е межд у наследова -
нием интерфейс а и реализаци и не поддерживается, на практик е оно существует.
Программист ы на Smalltal k обычн о предпочитаю т считать, что подкласс ы - это
подтип ы (хот я имеютс я и хорошо известны е исключени я [Соо92]). Программис -
ты на C++ манипулирую т объектам и чере з типы, определяемы е абстрактным и
классами.
Многи е паттерн ы проектировани я завися т от этог о различия. Например, объ-
екты, построенны е в соответстви и с паттерно м цепочк а обязанностей, должн ы
имет ь общий тип, но их реализаци я обычн о различна. В паттерн е компоновщи к
отдельный объект (компонент) определяет общий интерфейс, но реализацию ча-
сто определяе т составно й объек т (композиция). Паттерн ы команда, наблюда -
тель, состояние и стратегия часто реализуются абстрактными классами с исклю-
чительн о виртуальным и функциями.
Программирование в соответствии с интерфейсом, а не с реализацией
Наследование классов - это не что иное, как механизм расширения функцио-
нальности приложения путем повторного использования функциональности ро-
дительских классов. Оно позволяет быстро определить новый вид объектов в тер-
минах уже имеющегося. Новую реализацию вы можете получить посредством
наследования большей части необходимого кода из ранее написанных классов.
Однако не менее важно, что наследование позволяет определять семейства
объектов с идентичными интерфейсами (обычно за счет наследования от абстракт-
ных классов). Почему? Потому что от этого зависит полиморфизм.
Если пользоваться наследованием осторожно (некоторые сказали бы правиль-
но), то все классы, производные от некоторого абстрактного класса, будут обладать
его интерфейсом. Отсюда следует, что подкласс добавляет новые или замещает ста-
рые операции и не скрывает операций, определенных в родительском классе. Все
подклассы могут отвечать на запросы, соответствующие интерфейсу абстрактного
класса, поэтому они являются подтипами этого абстрактного класса.
У манипулирования объектами строго через интерфейс абстрактного класса
есть два преимущества:
а клиенту не нужно иметь информации о конкретных типах объектов, кото-
рыми он пользуется, при условии, что все они имеют ожидаемый клиентом
интерфейс;
а клиенту необязательно «знать» о классах, с помощью которых реализованы
объекты. Клиенту известно только об абстрактном классе (или классах), опре-
деляющих интерфейс.
Данные преимущества настолько существенно уменьшают число зависимос-
тей между подсистемами, что можно даже сформулировать принцип объектно-
ориентированного проектирования для повторного использования: программи-
руйте в соответствии с интерфейсом, а не с реализацией.
Не объявляйте переменные как экземпляры конкретных классов. Вместо это-
го придерживайтесь интерфейса, определенного абстрактным классом. Это одна
из наших ключевых идей.
Конечно, где-то в системе вам придется инстанцировать конкретные классы,
то есть определить конкретную реализацию. Как раз это и позволяют сделать по-
рождающие паттерны: абстрактная фабрика, строитель, фабричный метод,
прототип и одиночка. Абстрагируя процесс создания объекта, эти паттерны пре-
доставляют вам разные способы прозрачно ассоциировать интерфейс с его реали-
зацией в момент инстанцирования. Использование порождающих паттернов га-
рантирует, что система написана в терминах интерфейсов, а не реализации.
Механизмы повторного использования
Большинству проектировщиков известны концепции объектов, интерфейсов,
классов и наследования. Трудность в том, чтобы применить эти знания для по-
строения гибких, повторно используемых программ. С помощью паттернов про-
ектирования вы сможете сделать это проще.
Как решат ь задачи проектировани я
Наследование и композиция
Два наиболее распространенных приема повторного использования функци-
ональности в объектно-ориентированных системах - это наследование класса
и композиция объектов. Как мы уже объясняли, наследование класса позволяет
определить реализацию одного класса в терминах другого. Повторное использо-
вание за счет порождения подкласса называют еще прозрачным ящиком (white-
box reuse). Такой термин подчеркивает, что внутреннее устройство родительских
классов видимо подклассам.
Композиция объектов - это альтернатива наследованию класса. В этом случае
новую, более сложную функциональность мы получаем путем объединения или
композиции объектов. Для композиции требуется, чтобы объединяемые объекты
имели четко определенные интерфейсы. Такой способ повторного использования
называют черным ящиком (black-box reuse), поскольку детали внутреннего устрой-
ства объектов остаются скрытыми.
И у наследования, и у композиции есть достоинства и недостатки. Наследова-
ние класса определяется статически на этапе компиляции, его проще использо-
вать, поскольку оно напрямую поддержано языком программирования. В случае
наследования классов упрощается также задача модификации существующей ре-
ализации. Если подкласс замещает лишь некоторые операции, то могут оказаться
затронутыми и остальные унаследованные операции, поскольку не исключено,
что они вызывают замещенные.
Но у наследования класса есть и минусы. Во-первых, нельзя изменить унасле-
дованную от родителя реализацию во время выполнения программы, поскольку
само наследование фиксировано на этапе компиляции. Во-вторых, родительский
класс нередко хотя бы частично определяет физическое представление своих под-
классов. Поскольку подклассу доступны детали реализации родительского класса,
то часто говорят, что наследование нарушает инкапсуляцию [Sny86]. Реализации
подкласса и родительского класса настолько тесно связаны, что любые измене-
ния последней требуют изменять и реализацию подкласса.
Зависимость от реализации может повлечь за собой проблемы при попытке
повторного использования подкласса. Если хотя бы один аспект унаследованной
реализации непригоден для новой предметной области, то приходится переписы-
вать родительский класс или заменять его чем-то более подходящим. Такая зави-
симость ограничивает гибкость и возможности повторного использования. С пробле-
мой можно справиться, если наследовать только абстрактным классам, поскольку
в них обычно совсем нет реализации или она минимальна.
Композиция объектов определяется динамически во время выполнения за
счет того, что объекты получают ссылки на другие объекты. Композицию можно
применить, если объекты соблюдают интерфейсы друг друга. Для этого, в свою
очередь, требуется тщательно проектировать интерфейсы, так чтобы один объект
можно было использовать вместе с широким спектром других. Но и выигрыш ве-
лик. Поскольку доступ к объектам осуществляется только через их интерфейсы,
мы не нарушаем инкапсуляцию. Во время выполнения программы любой объект
можно заменить другим, лишь бы он имел тот же тип. Более того, поскольку при
введение в паттерны проектирования
реализаци и объект а кодируютс я прежд е всег о его интерфейсы, то зависимост ь от
реализаци и резко снижается.
Композици я объекто в влияе т на дизай н систем ы и еще в одно м аспекте. Отда -
вая предпочтени е композици и объектов, а не наследовани ю классов, вы инкапсу -
лирует е кажды й клас с и дает е ему возможност ь выполнят ь тольк о свою задачу.
Класс ы и их иерархи и остаютс я небольшими, и вероятност ь их разрастани я до
неуправляемы х размеро в невелика. С друго й стороны, дизайн, основанны й на
композиции, буде т содержат ь больше объекто в (хот я числ о классов, возможно,
уменьшится), и поведени е систем ы начне т зависет ь от их взаимодействия, тогд а
как при друго м подход е оно было бы определен о в одном классе.
Это подводи т нас ко втором у правил у объектно-ориентированног о проекти -
рования: предпочитайте композицию наследованию класса.
В идеале, чтоб ы добитьс я повторног о использования, вообще не следовал о
бы создават ь новые компоненты. Хорош о бы, чтоб ы можн о был о получит ь всю
нужну ю функциональность, прост о собира я вмест е уже существующи е компо -
ненты. На практике, однако, так получаетс я редко, поскольк у набо р имеющихс я
компоненто в все же недостаточн о широк. Повторно е использовани е за счет на-
следовани я упрощае т создани е новы х компонентов, которы е можн о был о бы
применят ь со старыми. Поэтом у наследовани е и композици я част о используют -
ся вместе.
Тем не менее, наш опыт показывает, что проектировщик и злоупотребляю т на-
следованием. Нередк о дизай н мог бы стат ь лучше и проще, если бы автор больше
полагалс я на композици ю объектов.
Делегирование
С помощь ю делегирования композици ю можн о сделат ь стол ь же мощны м ин-
струменто м повторног о использования, скол ь и наследовани е [Lie86, JZ91]. При
делегировани и в процес с обработк и запрос а вовлечен о два объекта: получател ь
поручае т выполнени е операци й другом у объект у - уполномоченному. Примерн о
так же подклас с делегируе т ответственност ь своем у родительском у классу. Но
унаследованна я операци я всегд а може т обратитьс я к объекту-получател ю чере з
переменную-чле н (в C++) или переменну ю self (в Smalltalk). Чтоб ы достич ь
того же эффект а для делегирования, получател ь передае т указател ь на самог о себя
соответствующем у объекту, дабы при выполнени и делегированно й операци и послед -
ний мог обратитьс я к непосредственном у адресат у запроса.
Например, вмест о того чтоб ы делат ь клас с Windo w (окно ) подклассо м класс а
Rectangl e (прямоугольник ) - ведь окно являетс я прямоугольником, - мы мо-
жем воспользоватьс я внутри Window поведением класса Rectangle, поместив
в клас с Windo w переменну ю экземпляр а типа Rectangl e и делегиру я ей опера -
ции, специфичны е для прямоугольников. Другим и словами, окно не является
прямоугольником, а содержит его. Тепер ь класс Windo w може т явно перенаправ -
лят ь запрос ы своем у член у Rectangle, а не наследоват ь его операции.
На диаграмм е ниже изображе н клас с Window, которы й делегируе т операци ю
Area () над своей внутренней областью переменной экземпляра Rectangle.
Как решать задачи проектирования
Сплошная линия со стрелкой обозначает, что класс содержит ссылку на экзем-
пляр другого класса. Эта ссылка может иметь необязательное имя, в данном слу-
чае прямоугольник.
Главное достоинство делегирования в том, что оно упрощает композицию
поведений во время выполнения. При этом способ комбинирования поведений
можно изменять. Внутреннюю область окна разрешается сделать круговой во вре-
мя выполнения, просто подставив вместо экземпляра класса Rectangl e экземп-
ляр класса Circle; предполагается, конечно, что оба эти класса имеют одина-
ковый тип.
У делегирования есть и недостаток, свойственный и другим подходам, приме-
няемым для повышения гибкости за счет композиции объектов. Заключается он
в том, что динамическую, в высокой степени параметризованну ю программу труд-
нее понять, нежели статическую. Есть, конечно, и некоторая потеря машинной
производительности, но неэффективност ь работы проектировщик а гораздо более
существенна. Делегирование можно считать хорошим выбором только тогда, ког-
да оно позволяет достичь упрощения, а не усложнения дизайна. Нелегко сформу-
лировать правила, ясно говорящие, когда следует пользоваться делегированием,
поскольку эффективност ь его зависит от контекста и вашего личного опыта. Луч-
ше всего делегирование работает при использовании в составе привычных идиом,
то есть в стандартных паттернах.
Делегирование используется в нескольких паттернах проектирования: состо-
яние, стратегия, посетитель. В первом получатель делегирует запрос объекту,
представляющему его текущее состояние. В паттерне стратегия обработка за-
проса делегируется объекту, который представляет стратегию его исполнения.
У объекта может быть только одно состояние, но много стратегий для исполне-
ния различных запросов. Назначение обоих паттернов - изменить поведение
объекта за счет замены объектов, которым делегируются запросы. В паттерне по-
сетитель операция, которая должна быть выполнена над каждым элементом со-
ставного объекта, всегда делегируется посетителю.
В других паттернах делегирование используется не так интенсивно. Паттерн
посредник вводит объект, осуществляющий посредничество при взаимодействии
других объектов. Иногда объект-посредник реализует операции, переадресуя их
другим объектам; в других случаях он передает ссылку на самого себя, исполь-
зуя тем самым делегирование как таковое. Паттерн цепочка обязанносте й
Введени е в паттерн ы проектировани я
обрабатывает запросы, перенаправляя их от одного объекта другому по цепочке.
Иногда вместе с запросом передается ссылка на исходный объект, получивший
запрос, и в этом случае мы снова сталкиваемся с делегированием. Паттерн мост
отделяет абстракцию от ее реализации. Если между абстракцией и конкретной
реализацией имеется существенное сходство, то абстракция может просто деле-
гировать операции своей реализации.
Делегирование показывает, что наследование как механизм повторного ис-
пользования всегда можно заменить композицией.
Наследование и параметризованные типы
Еще один (хотя и не в точности объектно-ориентированный) метод повтор-
ного использования имеющейся функциональности - это применение парамет-
ризованных типов, известных также как обобщенные типы (Ada, Eiffel) или шаб-
лоны (C++). Данная техника позволяет определить тип, не задавая типы, которые
он использует. Неспецифицированные типы передаются в виде параметров в точ-
ке использования. Например, класс List (список) можно параметризовать типом
помещаемых в список элементов. Чтобы объявить список целых чисел, вы пере-
даете тип integer в качестве параметра параметризованному типу List. Если
же надо объявить список строк, то в качестве параметра передается тип String.
Для каждого типа элементов компилятор языка создаст отдельный вариант шаб-
лона класса List.
Параметризованные типы дают в наше распоряжение третий (после наследо-
вания класса и композиции объектов) способ комбинировать поведение в объект-
но-ориентированных системах. Многие задачи можно решить с помощью любого
из этих трех методов. Чтобы параметризовать процедуру сортировки операцией
сравнения элементов, мы могли бы сделать сравнение:
а операцией, реализуемой подклассами (применение паттерна шаблонный
метод);
а функцией объекта, передаваемого процедуре сортировки (стратегия);
а аргументом шаблона в C++ или обобщенного типа в Ada, который задает
имя функции, вызываемой для сравнения элементов.
Но между тремя данными подходами есть важные различия. Композиция
объектов позволяет изменять поведение во время выполнения, но для этого тре-
буются косвенные вызовы, что снижает эффективность. Наследование разрешает
предоставить реализацию по умолчанию, которую можно замещать в подклассах.
С помощью параметризованных типов допустимо изменять типы, используемые
классом. Но ни наследование, ни параметризованные типы не подлежат модифи-
кации во время выполнения. Выбор того или иного подхода зависит от проекта
и ограничений на реализацию.
Ни в одном из паттернов, описанных в этой книге, параметризованные типы
не используются, хотя изредка мы прибегаем к ним для реализации паттернов
в C++. В языке вроде Smalltalk, где нет проверки типов во время компиляции, па-
раметризованные типы не нужны вовсе.
Как решать задачи проектирования
Сравнение структур времени выполнения
и времени компиляции
Структура объектно-ориентированно й программы на этапе выполнения час-
то имеет мало общего со структурой ее исходного кода. Последняя фиксируется
на этапе компиляции; код состоит из классов, отношения наследования между
которыми неизменны. На этапе же выполнения структура программы - быстро
изменяющаяся сеть из взаимодействующи х объектов. Две эти структуры почти
независимы.
Рассмотрим различие между агрегированием и осведомленностью (acquaintance)
объектов и его проявления на этапах компиляции и выполнения. Агрегирование
подразумевает, что один объект владеет другим или несет за него ответственность.
В общем случае мы говорим, что объект содержит другой объект или является его
частью. Агрегирование означает, что агрегат и его составляющие имеют одинако-
вое время жизни.
Говоря же об осведомленности, мы имеем в виду, что объекту известно о дру-
гом объекте. Иногда осведомленност ь называют ассоциацией или отношением
«использует». Осведомленные объекты могут запрашиват ь друг у друга операции,
но они не несут никакой ответственност и друг за друга. Осведомленност ь - это
более слабое отношение, чем агрегирование; оно предполагает гораздо менее тес-
ную связь между объектами.
На наших диаграммах осведомленност ь будет обозначаться сплошной линией
со стрелкой. Линия со стрелкой и ромбиком вначале обозначает агрегирование.
Агрегирование и осведомленност ь легко спутать, поскольку они часто реали-
зуются одинаково. В языке Smalltal k все переменные являются ссылками на
объекты, здесь нет различия между агрегированием и осведомленностью. В C++
агрегирование можно реализовать путем определения переменных-членов, кото-
рые являются экземплярами, но чаще их определяют как указатели или ссылки.
Осведомленност ь также реализуется с помощью указателей и ссылок.
Различие между осведомленность ю и агрегирование м определяется, скорее,
предполагаемым использованием, а не языковыми механизмами. В структуре, су-
ществующей на этапе компиляции, увидеть различие нелегко, но тем не менее оно
существенно. Обычно отношений агрегирования меньше, чем отношений осве-
домленности, и они более постоянны. Напротив, отношения осведомленност и
возникают и исчезают чаще и иногда длятся лишь во время исполнения некото-
рой операции. Отношения осведомленности, кроме того, более динамичны, что
затрудняет их выявление в исходном тексте программы.
Коль скоро несоответствие между структурой программы на этапах компиля-
ции и выполнения столь велико, ясно, что изучение исходного кода может ска-
зать о работе системы совсем немного. Поведение системы во время выполнения
должно определяться проектировщиком, а не языком. Соотношения между объек-
тами и их типами нужно проектироват ь очень аккуратно, поскольку именно от
Введение в паттерны проектировани я
них зависит, насколько удачной или неудачной окажется структура во время вы-
полнения.
Многие паттерны проектировани я (особенно уровня объектов) явно подчер-
кивают различие между структурами на этапах компиляци и и выполнения. Пат-
терны компоновщи к и декорато р полезны для построения сложных структур
времени выполнения. Наблюдател ь порождае т структуры времени выполнения,
которые часто трудно понять, не зная паттерна. Паттерн цепочка обязанносте й
также приводит к таким схемам взаимодействия, в которых наследовани е неоче-
видно. В общем можно утверждать, что разобратьс я в структура х времени выпол-
нения невозможно, если не понимаешь специфики паттернов.
Проектирование с учетом будущих изменений
Системы необходимо проектироват ь с учетом их дальнейшег о развития. Для
проектировани я системы, устойчиво й к таким изменениям, следует предполо-
жить, как она будет изменятьс я на протяжении отведенног о ей времени жизни.
Если при проектировани и системы не принималас ь во внимание возможност ь
изменений, то есть вероятность, что в будущем ее придется полностью перепроек-
тировать. Это может повлечь за собой переопределени е и новую реализацию клас-
сов, модификаци ю клиентов и повторный цикл тестирования. Перепроектирова -
ние отражаетс я на многих частях системы, поэтому непредвиденны е изменения
всегда оказываютс я дорогостоящими.
Благодаря паттерна м систему всегда можно модифицироват ь определенным
образом. Каждый паттерн позволяе т изменят ь некоторый аспект системы незави-
симо от всех прочих, таким образом, она менее подвержена влиянию изменений
конкретног о вида.
Вот некоторые типичные причины перепроектирования, а также паттерны,
которые позволяют этого избежать:
а при создании объекта явно указывается класс. Задание имени класса привя-
зывает вас к конкретно й реализации, а не к конкретному интерфейсу. Это
может осложнит ь изменение объекта в будущем. Чтобы уйти от такой про-
блемы, создавайте объекты косвенно.
Паттерны проектирования: абстрактная фабрика, фабричный метод,
прототип;
а зависимость от конкретных операций. Задавая конкретну ю операцию, вы
ограничивает е себя единственным способом выполнени я запроса. Если же
не включат ь запросы в код, то будет проще изменит ь способ удовлетворе -
ния запроса как на этапе компиляции, так и на этапе выполнения.
Паттерны проектирования: цепочка обязанностей, команда;
а зависимость от аппаратной и программной платформ. Внешние интерфей-
сы операционно й системы и интерфейс ы прикладных программ (API) раз-
личны на разных программных и аппаратных платформах. Если программа
зависит от конкретно й платформы, ее будет труднее перенест и на другие.
Даже на «родной» платформе такую программу трудно поддерживать.
Как решат ь задач и проектировани я
Поэтом у при проектировани и систе м так важн о ограничиват ь платформен -
ные зависимости.
Паттерны проектирования: абстрактная фабрика, мост;
а зависимость от представления или реализации объекта. Есл и клиен т «зна -
ет», как объек т представлен, хранитс я или реализован, то при изменени и
объект а може т оказатьс я необходимы м изменит ь и клиента. Сокрыти е этой
информаци и от клиенто в поможе т уберечьс я от каскад а изменений.
Паттерн ы проектирования: абстрактна я фабрика, мост, хранитель, замес -
титель;
а зависимость от алгоритмов. Во время разработк и и последующег о исполь -
зовани я алгоритм ы част о расширяются, оптимизируютс я и заменяются. За-
висящи е от алгоритмо в объект ы придетс я переписыват ь при каждо м изме -
нени и алгоритма. Поэтом у алгоритмы, вероятност ь изменени я которы х
высока, следуе т изолировать.
Паттерн ы проектирования: мост, итератор, стратегия, шаблонны й метод,
посетитель;
а сильная связанность. Сильн о связанны е межд у собо й класс ы трудн о исполь -
зоват ь порознь, так как они завися т дру г от друга. Сильна я связанност ь при-
водит к появлени ю монолитны х систем, в которы х нельз я ни изменить, ни
удалит ь клас с без знани я детале й и модификаци и други х классов. Таку ю
систем у трудн о изучать, переносит ь на други е платформ ы и сопровождать.
Слаба я связанност ь повышае т вероятност ь того, что клас с можн о буде т по-
вторн о использоват ь сам по себе. При этом изучение, перенос, модифика -
ция и сопровождени е систем ы намног о упрощаются. Для поддержк и слаб о
связанны х систе м в паттерна х проектировани я применяютс я таки е методы,
как абстрактны е связ и и разбиени е на слои.
Паттерн ы проектирования: абстрактна я фабрика, мост, цепочк а обязан -
ностей, команда, фасад, посредник, наблюдатель;
а расширение функциональности за счет порождения подклассов. Специали -
зация объект а путе м создани я подкласс а част о оказываетс я непросты м делом.
С кажды м новы м подклассо м связан ы фиксированны е издержк и реализаци и
(инициализация, очистк а и т.д.). Для определени я подкласс а необходим о так-
же ясно представлят ь себе устройств о родительског о класса. Например, для
замещени я одно й операци и може т потребоватьс я заместит ь и другие. За-
мещени е операци и може т оказатьс я необходимы м для того, чтоб ы можн о
был о вызват ь унаследованну ю операцию. Кром е того, порождени е под -
классо в веде т к комбинаторном у рост у числ а классов, поскольк у даже для
реализаци и простог о расширени я може т понадобитьс я мног о новы х под -
классов.
Композици я объекто в и делегировани е - гибки е альтернатив ы наследовани ю
для комбинировани я поведений. Приложени ю можн о добавит ь нову ю функ -
циональность, меня я спосо б композици и объектов, а не определяя новые под-
класс ы уже имеющихс я классов. С друго й стороны, при интенсивно м исполь -
зовани и композици и объекто в проек т може т оказатьс я трудны м для понимания.
С помощь ю многи х паттерно в проектировани я удаетс я построит ь тако е
Введени е в паттерн ы проектировани я
решение, где специализаци я достигаетс я за счет определени я одног о подклас -
са и комбинировани я его экземпляро в с уже существующими.
Паттерн ы проектирования: мост, цепочк а обязанностей, компоновщик,
декоратор, наблюдатель, стратегия;
а неудобства при изменении классов. Иногд а нужн о модифицироват ь класс, но
делат ь это неудобно. Допустим, вам нуже н исходны й код, а его нет (так об-
стоит дело с коммерческим и библиотекам и классов). Или любо е изменени е
тяне т за собо й модификаци и множеств а существующи х подклассов. Бла -
годар я паттерна м проектировани я можн о модифицироват ь класс ы и при
таких условиях.
Паттерны проектирования: адаптер, декоратор, посетитель.
Приведенны е пример ы демонстрирую т ту гибкост ь работы, которо й можн о
добиться, использу я паттерн ы при проектировани и приложений. Наскольк о эта
гибкост ь необходима, зависит, конечно, от особенносте й ваше й программы. Да-
вайт е посмотрим, каку ю рол ь играю т паттерн ы при разработк е прикладны х про-
грамм, инструментальны х библиоте к и каркасо в приложений.
Прикладные программы
Если вы проектирует е прикладну ю программу, наприме р редакто р докумен -
тов или электронну ю таблицу, то наивысши й приорите т имею т внутреннее по-
вторно е использование, удобств о сопровождени я и расширяемость. Перво е под -
разумевает, чт о вы не проектирует е и не реализует е больше, чем необходимо.
Повысит ь степен ь внутреннег о повторног о использовани я помогу т паттерны,
уменьшающи е числ о зависимостей. Ослаблени е связанност и увеличивае т веро -
ятност ь того, что некоторы й клас с объекто в сможе т совместн о работат ь с другими.
Например, устраня я зависимост и от конкретны х операци й путе м изолировани я
и инкапсуляци и каждо й операции, вы упрощает е задач у повторног о использова -
ния любо й операци и в друго м контексте. К тому же результат у приводи т устране -
ние зависимосте й от алгоритм а и представления.
Паттерн ы проектировани я такж е позволяю т упростит ь сопровождени е при -
ложения, если использоват ь их для ограничени я платформенны х зависимосте й
и разбиени я систем ы на отдельные слои. Они способствую т и наращивани ю функ-.
ций системы, показывая, как расширят ь иерархи и классо в и когд а применят ь ком-
позици ю объектов. Уменьшени е степен и связанност и такж е увеличивае т возмож -
ност ь развити я системы. Расширени е класс а становитс я проще, если он не зависи т
от множеств а других.
Инструментальные библиотеки
Част о приложени е включае т класс ы из одно й или нескольки х библиоте к пре -
допределенны х классов. Таки е библиотек и называютс я инструментальными.
Инструментальна я библиотек а - это набо р взаимосвязанных, повторн о исполь -
зуемы х классов, спроектированны й с цель ю предоставлени я полезны х функци й
общег о назначения. Пример ы инструментально й библиотек и - набо р контейнер -
ных классо в для списков, ассоциативны х массивов, стеко в и т.д., библиотек а по-
токовог о ввода/вывод а в C++. Инструментальны е библиотек и не определяю т
Как решат ь задач и проектировани я
какой-то конкретный дизайн приложения, а просто предоставляют средства, бла-
годаря которым в приложениях проще решать поставленные задачи, позволяют раз-
работчику не изобретать заново повсеместно применяемые функции. Таким обра-
зом, в инструментальных библиотеках упор сделан на повторном использовании
кода. Это объектно-ориентированные эквиваленты библиотек подпрограмм.
Можно утверждать, что проектирование инструментальной библиотеки слож-
нее, чем проектирование приложения, поскольку библиотеки должны использо-
ваться во многих приложениях, иначе они бесполезны. К тому же автор библио-
теки не знает заранее, какие специфические требования будут предъявляться
конкретными приложениями. Поэтому ему необходимо избегать любых предпо-
ложений и зависимостей, способных ограничить гибкость библиотеки, следова-
тельно, сферу ее применения и эффективность.
Каркасы приложений
Каркас - это набор взаимодействующих классов, составляющих повторно ис-
пользуемый дизайн для конкретного класса программ [Deu89, JF88]. Например,
можно создать каркас для разработки графических редакторов в разных облас-
тях: рисовании, сочинении музыки или САПР [VL90, Joh92]. Другим каркасом
рекомендуется пользоваться при создании компиляторов для разных языков про-
граммирования и целевых машин [JML92]. Третий упростит построение приложе-
ний для финансового моделирования [ВЕ93]. Каркас можно подстроить под кон-
кретное приложение путем порождения специализированных подклассов от
входящих в него абстрактных классов.
Каркас диктует определенную архитектуру приложения. Он определяет об-
щую структуру, ее разделение на классы и объекты, основные функции тех и дру-
гих, методы взаимодействия объектов и классов и потоки управления. Данные
параметры проектирования задаются каркасом, а прикладные проектировщики
или разработчики могут сконцентрироваться на специфике приложения. В кар-
касе аккумулированы проектные решения, общие для данной предметной облас-
ти. Акцент в каркасе делается на повторном использовании дизайна, а не кода, хотя
обычно он включает и конкретные подклассы, которые можно применять непо-
средственно.
Повторное использование на данном уровне меняет направление связей
между приложением и программным обеспечением, лежащим в его основе, на
противоположное. При использовании инструментальной библиотеки (или,
если хотите, обычной библиотеки подпрограмм) вы пишете тело приложения
и вызываете из него код, который планируете использовать повторно. При ра-
боте с каркасом вы, наоборот, повторно используете тело и пишете код, который
оно вызывает. Вам приходится кодировать операции с предопределенными име-
нами и параметрами вызова, но зато число принимаемых вами проектных реше-
ний сокращается.
В результате приложение создается быстрее. Более того, все приложения име-
ют схожую структуру. Их проще сопровождать, и пользователям они представля-
ются более знакомыми. С другой стороны, вы в какой-то мере жертвуете свобо-
дой творчества, поскольку многие проектные решения уже приняты за вас.
Введение в паттерны проектирования
Если проектировать приложения нелегко, инструментальные библиотеки -
еще сложнее, то проектирование каркасов - задача самая трудная. Проектиров-
щик каркаса рассчитывает, что единая архитектура будет пригодна для всех при-
ложений в данной предметной области. Любое независимое изменение дизайна
каркаса приведет к утрате его преимуществ, поскольку основной «вклад» каркаса
в приложение - это определяемая им архитектура. Поэтому каркас должен быть
максимально гибким и расширяемым.
Поскольку приложения так сильно зависят от каркаса, они особенно чувстви-
тельны к изменениям его интерфейсов. По мере усложнения каркаса приложения
должны эволюционировать вместе с ним. В результате существенно возрастает
значение слабой связанности, в противном случае малейшее изменение каркаса
приведет к целой волне модификаций.
Рассмотренные выше проблемы проектирования актуальны именно для кар-
касов. Каркас, в котором они решены путем применения паттернов, может лучше
обеспечить высокий уровень проектирования и повторного использования кода,
чем тот, где паттерны не применялись. В отработанных каркасах обычно можно
обнаружить несколько разных паттернов проектирования. Паттерны помогают
адаптировать архитектуру каркаса ко многим приложениям без повторного про-
ектирования.
Дополнительное преимущество появляется потому, что вместе с каркасом до-
кументируются те паттерны, которые в нем использованы [BJ94]. Тот, кто знает
паттерны, способен быстрее разобраться в тонкостях каркаса. Но даже не работа-
ющие с паттернами увидят их преимущества, поскольку паттерны помогают удоб-
но структурировать документацию по каркасу. Повышение качества документи-
рования важно для всех типов программного обеспечения, но для каркасов этот
аспект важен вдвойне. Для освоения работы с каркасами надо потратить немало
усилий, и только после этого они начнут приносить реальную пользу. Паттерны
могут существенно упростить задачу, явно выделив ключевые элементы дизайна
каркаса.
Поскольку между паттернами и каркасами много общего, часто возникает во-
прос, в чем же различия между ними и есть ли они вообще. Так вот, существует
три основных различия:
а паттерны проектирования более абстрактны, чем каркасы. В код могут
быть включены целые каркасы, но только экземпляры паттернов. Каркасы
можно писать на разных языках программирования и не только изучать, но
и непосредственно исполнять и повторно использовать. В противополож-
ность этому паттерны проектирования, описанные в данной книге, необхо-
димо реализовывать всякий раз, когда в них возникает необходимость. Пат-
терны объясняют намерения проектировщика, компромиссы и последствия
выбранного дизайна;
а как архитектурные элементы, паттерны проектирования мельче, чем кар-
касы. Типичный каркас содержит несколько паттернов. Обратное утверж-
дение неверно;
Как выбират ь паттер н проектировани я
а паттерны проектирования менее специализированы, чем каркасы. Карка с
всегд а создаетс я для конкретно й предметно й области. В принцип е карка с
графическог о редактор а можн о использоват ь для моделировани я работ ы
фабрики, но его никогд а не спутаеш ь с каркасом, предназначенны м специ -
ально для моделирования. Напротив, включенны е в наш катало г паттерн ы
разрешаетс я использоват ь в приложения х почт и любог о вида. Хотя, безуслов -
но, существую т и боле е специализированны е паттерн ы (скажем, паттерн ы
для распределенны х систе м или параллельног о программирования), но
даже они не диктую т выбо р архитектур ы в той же мере, что и каркасы.
Значени е каркасо в возрастает. Именн о с их помощь ю объектно-ориентиро -
ванны е систем ы можн о использоват ь повторн о в максимально й степени. Круп -
ные объектно-ориентированны е приложени я составляютс я из слое в взаимодей -
ствующи х дру г с друго м каркасов. Дизай н и код приложени я в значительно й мере
определяютс я теми каркасами, которы е применялис ь при его создании.
1.7. Как выбират ь паттер н проектировани я
Если в распоряжени е проектировщик а предоставле н катало г из боле е чем 20 пат-
тернов, трудн о решать, како й паттер н лучше всег о подходи т для решени я кон-
кретно й задач и проектирования. Ниж е представлен ы разны е подход ы к выбор у
подходящег о паттерна:
а подумайте, как паттерны решают проблемы проектирования. В раздел е 1.6
обсуждаетс я то, как с помощь ю паттерно в можн о найт и подходящи е объек -
ты, определит ь нужну ю степен ь их детализации, специфицироват ь их ин-
терфейсы. Здес ь же говоритс я и о некоторы х иных подхода х к решени ю за-
дач с помощь ю паттернов;
а пролистайте разделы каталога, описывающие назначение паттернов. В раз -
деле 1.4,перечислен ы назначени я всех представленны х паттернов. Ознакомь -
тесь с цель ю каждог о паттерна, когд а будет е искат ь тот, что в наибольше й сте-
пени относитс я к ваше й проблеме. Чтоб ы сузит ь поиск, воспользуйтес ь
схемо й в таблиц е 1.1;
а изучите взаимосвязи паттернов. На рис. 1.1 графическ и изображен ы соот -
ношени я межд у различным и паттернам и проектирования. Данна я инфор -
маци я поможе т вам найт и нужны й паттер н или групп ы паттернов;
а проанализируйте паттерны со сходными целями. Катало г состои т из тре х
частей: порождающи е паттерны, структурны е паттерн ы и паттерн ы поведе -
ния. Кажда я част ь начинаетс я со вступительны х замечани й о паттерна х со-
ответствующег о вид а и заканчиваетс я разделом, где они сравниваютс я дру г
с другом;
а разберитесь в причинах, вызывающих перепроектирование. Взглянит е на пе-
речен ь причин, приведенны й выше. Быт ь может, в нем упомянут а ваша про-
блема? Зате м обратитес ь к изучени ю паттернов, помогающи х устранит ь эту
причину;
Введение в паттерны проектирования
а посмотрите, что в вашем дизайне должно быть изменяющимся. Такой подход
противоположен исследованию причин, вызвавших необходимость перепро-
ектирования. Вместо этого подумайте, что могло бы заставить изменить ди-
зайн, а также о том, что бы вы хотели изменять без перепроектирования.
Акцент здесь делается на инкапсуляции сущностей, подверженных измене-
ниям, а это предмет многих паттернов. В таблице 1.2 перечислены те аспек-
ты дизайна, которые разные паттерны позволяют варьировать независимо,
устраняя тем самым необходимост ь в перепроектировании.
1.8. Ка к пользоватьс я паттерно м проектировани я
Как пользоваться паттерном проектирования, который вы выбрали для изу-
чения и работы? Вот перечень шагов, которые помогут вам эффективно приме-
нить паттерн:
1. Прочитайте описание паттерна, чтобы получить о нем общее представле-
ние. Особое внимание обратите на разделы «Применимость » и «Результа-
ты» - убедитесь, что выбранный вами паттерн действительно подходит для
решения ваших задач.
2. Вернитесь назад и изучите разделы «Структура», «Участники» и «Отноше-
ния». Убедитесь, что понимаете упоминаемые в паттерне классы и объекты
и то, как они взаимодействуют друг с другом.
3. Посмотрите на раздел «Пример кода», где приведен конкретный пример ис-
пользования паттерна в программе. Изучение кода поможет понять, как нуж-
но реализовыват ь паттерн.
4. Выберите для участников паттерна подходящие имена. Имена участников
паттерна обычно слишком абстрактны, чтобы употреблять их непосредствен-
но в коде. Тем не менее бывает полезно включить имя участника как имя
в программе. Это помогает сделать паттерн более очевидным при реализа-
ции. Например, если вы пользуетесь паттерном стратегия в алгоритме раз-
мещения текста, то классы могли бы называться SimpleLayoutStrateg y
или TeXLayoutStrategy.
5. Определите классы. Объявите их интерфейсы, установите отношения насле-
дования и определите переменные экземпляра, которыми будут представлены
данные объекты и ссылки на другие объекты. Выявите имеющиеся в вашем
приложении классы, на которые паттерн оказывает влияние, и соответствую-
щим образом модифицируйт е их.
6. Определите имена операций, встречающихся в паттерне. Здесь, как и в пре-
дыдущем случае, имена обычно зависят от приложения. Руководствуйтес ь
теми функциями и взаимодействиями, которые ассоциированы с каждой
операцией. Кроме того, будьте последовательны при выборе имен. Например,
для обозначения фабричног о метода можно было бы всюду использоват ь
префикс Create-.
7. Реализуйте операции, которые выполняют обязанности и отвечают за от-
ношения, определенные в паттерне. Советы о том, как это лучше сделать, вы
найдете в разделе «Реализация». Поможет и «Пример кода».
Как пользоваться паттерном проектирования
Все вышесказанное - обычные рекомендации. Со временем вы выработаете
собственный подход к работе с паттернами проектирования.
Таблица 1.2. Изменяемые паттернами элементы дизайна
Назначени е
Паттерн
проектировани я
Аспекты, которые можно
изменят ь
Порождающие
паттерны
Паттерны
поведения
Абстрактная фабрика
Одиночка
Прототип
Строитель
Фабричный метод
Семейства порождаемых объектов
Единственный экземпляр класса
Класс, из которого инстанцируется объект
Способ создания составного объекта
Инстанцируемый подкласс объекта
Структурные Адаптер Интерфейс к объекту
паттерны Декоратор Обязанности объекта без порождения подкласса
Заместитель Способ доступа к объекту, его местоположение
Компоновщик Структура и состав объекта
Мост Реализация объекта
Приспособленец Накладные расходы на хранение объектов
Фасад Интерфейс к подсистеме
Интерпретатор
Итератор
Команда
Наблюдатель
Посетитель
Посредник
Состояние
Стратегия
Хранитель
Цепочка обязанностей
Шаблонный метод
Грамматика и интерпретация языка
Способ обхода элементов агрегата
Время и способ выполнения запроса
Множество объектов, зависящих от другого
объекта; способ, которым зависимые объекты
поддерживают себя в актуальном состоянии
Операции, которые можно применить к объекту
или объектам, не меняя класса
Объекты, взаимодействующие между собой,
и способ их коопераций
Состояние объекта
Алгоритм
Закрытая информация, хранящаяся вне объекта,
и время ее сохранения
Объект, выполняющий запрос
Шаги алгоритма
Никакое обсуждение того, как пользоваться паттернами проектирования,
нельзя считать полным, если не сказать о том, как не надо их применять. Нередко
за гибкость и простоту изменения, которые дают паттерны, приходится платить
усложнением дизайна и ухудшением производительности. Паттерн проектирова-
ния стоит применять, только когда дополнительная гибкость действительно не-
обходима. Для оценки достоинств и недостатков паттерна большую помощь мо-
гут оказать разделы каталога «Результаты».
Глава 2. Проектирование
редактора документов
В данной главе рассматриваетс я применени е паттерно в на пример е проектирова -
ния визуальног о редактор а документо в Lexi 1, построенног о по принцип у «что ви-
дишь, то и получаешь » (WYSIWYG). Мы увидим, как с помощь ю паттерно в можно
решать проблемы проектирования, характерны е для Lexi и аналогичны х прило-
жений. Здесь описываетс я опыт работы с восемь ю паттернами.
На рис. 2.1 изображе н пользовательски й интерфей с редактор а Lexi. WYSIWYG -
представлени е документ а занимае т большу ю прямоугольну ю област ь в центре.
В документ е могут произвольн о сочетатьс я текст и графика, отформатированны е
разными способами. Вокру г документ а - привычны е выпадающи е меню и поло-
сы прокрутки, а также значк и с номерам и для переход а на нужну ю страниц у до-
кумента.
2.1. Задач и проектировани я
Рассмотри м семь задач, характерны х для дизайн а Lexi:
а структура документа. Выбор внутреннег о представлени я документ а отра-
жается практическ и на всех аспекта х дизайна. Для редактирования, форма -
тирования, отображени я и анализ а текст а необходим о умет ь обходит ь это
представление. Способ организаци и информаци и играе т решающу ю роль
при дальнейше м проектировании;
а форматирование. Как в Lexi организован ы текст и график а в виде строк
и колонок? Какие объект ы отвечают за реализаци ю стратеги й форматиро -
вания? Взаимодействи е данных стратеги й с внутренни м представление м
документа;
а создание привлекательного интерфейса пользователя. В состав пользователь -
ского интерфейс а Lexi входят полосы прокрутки, рамки и оттененны е выпада-
ющие меню. Вполне вероятно, что количеств о и состав элементо в интерфейс а
будут изменятьс я по мере его развития. Поэтому важно иметь возможност ь
легко добавлят ь и удалят ь элемент ы оформления, не затрагива я приложение;
а поддержка стандартов внешнего облика программы. Lexi долже н без серьез -
ной модификаци и адаптироватьс я к стандарта м внешнег о облика программ,
например, таким как Moti f или Presentatio n Manage r (PM);
Дизайн Lexi основа н на программ е Doc - текстовог о редактора, разработанног о Кальдеро м [CL92].
Задачи проектирования
Рис. 2.1. Пользовательский интерфейс Lex/
Проектировани е редактор а документ е
а поддержка оконных систем. В оконных системах стандарты внешнего обль
ка обычно различаются. По возможности дизайн Lexi должен быть незавк
симым от оконной системы;
а операции пользователя. Пользователи управляют работой Lexi с помощы
элементов интерфейса, в том числе кнопок и выпадающих меню. Функции
которые вызываются из интерфейса, разбросаны по всей программе. Разра
ботать единообразный механизм для доступа к таким «рассеянным» функ
циям и для отмены уже выполненных операций довольно трудно;
а проверка правописания и расстановка переносов. Поддержка в Lexi таки:
аналитических операций, как проверка правописания и определение мес
переноса. Как минимизировать число классов, которые придется модифи
цировать при добавлении новой аналитической операции?
Ниже обсуждаются указанные проблемы проектирования. Для каждой из ню
определены некоторые цели и ограничения на способы их достижения. Прежде
чем предлагать решение, мы подробно остановимся на целях и ограничениях. Не
примере проблемы и. ее решения демонстрируется применение одного или не-
скольких паттернов проектирования.. Обсуждение каждой проблемы завершает-
ся краткой характеристикой паттерна.
2.2. Структур а документ а
Документ - это всего лишь организованное некоторым способом множество
базовых графических элементов: символов, линий, многоугольников и других гео-
метрических фигур. Все они несут в себе полную информацию о содержании до-
кумента. И все же автор часто представляет себе эти элементы не в графическом
виде, а в терминах физической структуры документа - строк, колонок, рисунков,
таблиц и других подструктур.1 Эти подструктуры, в свою очередь, составлены из
более мелких и т.д.
Пользовательский интерфейс Lexi должен позволять пользователям непо-
средственно манипулировать такими подструктурами. Например, пользователю
следует дать возможность обращаться с диаграммой как с неделимой единицей,
а не как с набором отдельных графических примитивов, предоставить средства
ссылаться на таблицу как на единое целое, а не как на неструктурированное хра-
нилище текста и графики. Это делает интерфейс простым и интуитивно понят-
ным. Чтобы придать реализации Lexi аналогичные свойства, мы выберем такое
внутреннее представление, которое в точности соответствует физической струк-
туре документа.
В частности, внутреннее представление должно поддерживать:
а отслеживание физической структуры документа, то есть разбиение текста
и графики на строки, колонки, таблицы и т.д.;
Авторы часто рассматривают документы и в терминах их логической структуры: предложений, абза-
цев, разделов, подразделов и глав. Чтобы не слишком усложнять пример, мы не будем явно хранить
во внутреннем представлении информацию о логической структуре. Но то проектное решение, кото-
рое мы опишем, вполне пригодно для представления и такой информации.
Структур а документ а
а генерирование визуальног о представления документа;
а отображение позиций экрана на элементы внутреннег о представления. Это
позволит определить, что имел в виду пользователь, когда указал на что-то
в визуальном представлении.
Помимо данных целей имеются и ограничения. Во-первых, текст и графику
следует трактовать единообразно. Интерфейс приложения должен позволять сво-
бодно помещать текст внутрь графики и наоборот. Не следует считать графику
частным случаем текста или текст - частным случаем графики, поскольку это
в конечном итоге привело бы к появлению избыточных механизмов форматиро-
вания и манипулирования. Одного набора механизмов должно хватить и для текс-
та, и для графики.
Во-вторых, в нашей реализации не может быть различий во внутреннем пред-
ставлении отдельного элемента и группы элементов. При одинаковой работе Lexi
с простыми и сложными элементами можно будет создавать документы со струк-
турой любой сложности. Например, десятым элементом на пересечении пятой
строки и второй колонки мог бы быть как один символ, так и сложно устроенная
диаграмма со многими внутренними компонентами. Но, коль скоро мы уверены,
что этот элемент имеет возможность изображать себя на экране и сообщать свои раз-
меры, его внутренняя сложность не имеет никакого отношения к тому, как и в ка-
ком месте страницы он появляется.
Однако второе ограничение противоречит необходимости анализировать текст
на предмет выявления орфографических ошибок и расстановки переносов. Во мно-
гих случаях нам безразлично, является ли элемент строки простым или сложным
объектом. Но иногда вид анализа зависит от анализируемог о объекта. Так, вряд ли
имеет смысл проверять орфографию многоугольника или пытаться переносить
его с одной строки на другую. При проектировании внутреннего представлении
надо учитывать эти и другие потенциально конфликтующие ограничения.
Рекурсивная композиция
На практике для представления иерархически структурированно й информа-
ции часто применяется прием, называемый рекурсивной композицией. Он позво-
ляет строить все более сложные элементы из простых. Рекурсивная композиция
дает нам способ составить документ из простых графических элементов. Сначала
мы можем линейно расположить множество символов и графики слева направо
для формирования одной строки документа. Затем несколько строк можно объе-
динить в колонку, несколько колонок - в страницу и т.д. (см. рис. 2.2.).
Данную физическую структуру можно представить, введя отдельный объект
для каждого существенног о элемента. К таковым относятся не только видимые
элементы вроде символов и графики, но и структурные элементы - строки и ко-
лонки. В результате получается структура объекта, изображенная на рис. 2.3.
Представляя объектом каждый символ и графический элемент документа, мы
обеспечиваем гибкость на самых нижних уровнях дизайна Lexi. С точки зрения
отображения, форматирования и вкладывания друг в друга единообразно тракту-
ются текст и графика. Мы сможем расширить Lexi для поддержки новых наборов
Проектировани е редактор а документо в
Составно й объек т
(колонка )
Рис. 2.2. Рекурсивная композиция текста и графики
Рис. 2.3. Структура объекта для рекурсивной композиции текста и графики
символов, не затрагивая никаких других функций. Объектная структура Lexi точ-
но отражает физическую структуру документа.
У описанного подхода есть два важных следствия. Первое очевидно: для объек-
тов нужны соответствующие классы. Второе, менее очевидное, состоит в том, что
у этих классов должны быть совместимые интерфейсы, поскольку мы хотим уни-
фицировать работу с ними. Для обеспечения совместимости интерфейсов в таком
языке, как C++, применяется наследование.
Структур а документ а
Глифы
Абстрактны й клас с Glyp h (глиф ) определяетс я для все х объектов, которы е
могу т присутствоват ь в структур е документа 1. Его подкласс ы определяю т как при-
митивны е графически е элемент ы (скажем, символ ы и изображения), так и струк -
турны е элемент ы (строк и и колонки). На рис. 2.4 изображен а достаточн о обшир -
ная част ь иерархи и класс а Glyph, а в таблиц е 2.1 боле е подробн о представле н
базовы й интерфей с этог о класс а в нотаци и C++2.
Таблица 2. 1. Базовый интерфейс класса Glyph
Обязанность Операции
внешне е представлени е virtual void Dra w (Window* )
virtual void Bound s (Rect& )
обнаружени е точк и воздействия virtual bool intersects (const Point& )
структур а virtual void Insert (Glyph * , int)
virtual void Remov e (Glyph* )
virtual Glyph* Child (int)
virtual Glyph* Parent ( )
У глифо в ест ь три основны е функции. Они имею т информаци ю о свои х пред -
ках и потомках, а такж е о том, как нарисоват ь себя на экран е и скольк о места они
занимают.
Подкласс ы класс а Glyp h переопределяю т операци ю Draw, выполняющу ю пе-
рерисовк у себя в окне. При вызов е Dra w ей передаетс я ссылк а на объек т Window.
В класс е Windo w определен ы графически е операци и для прорисовк и в окне на
экран е текст а и основны х геометрически х фигур. Например, в подкласс е Rectangl e
операци я Dra w могл а определятьс я так:
void Rectangle: : Draw (Window * w) {
w->DrawRec t (_xO,
где _xO, _yO, _x l и _y l - это данные-член ы класс а Rectangle, определяющи е
два противоположны х угл а прямоугольника, a DrawRec t - операци я из класс а
Window, рисующа я на экран е прямоугольник.
Впервы е терми н «глиф» в этом контекст е употреби л Пол Кальде р [CL90]. В большинств е современны х
редакторо в документо в отдельны е символ ы не представляютс я объектами, скоре е всего, из соображени й
эффективности. Кальде р продемонстрирова л практическу ю пригодност ь этог о подход а в свое й диссерта -
ции [Са193]. Наш и глиф ы прощ е предложенны х им, поскольк у мы дл я простот ы ограничилис ь строги -
ми иерархиями. Глиф ы Кальдер а могу т использоватьс я совместн о дл я уменьшени я потреблени я памят и
и, стал о быть, образую т направленны е ациклически е графы. Дл я достижени я тог о же эффект а мы може м
воспользоватьс я паттерно м Приспособленец, но остави м эт о в качеств е упражнени я читателю.
Представленны й здес ь интерфей с намеренн о сдела н минимальным, чтоб ы не загромождат ь обсужде -
ние техническим и деталями. Полны й интерфей с долже н бы включат ь операци и для работ ы с графи -
ческим и атрибутами: цветами, шрифтам и и, преобразованиям и координат, а такж е операци и дл я боле е
развитог о управлени я потомками.
Проектировани е редактор а документо в
Рис. 2.4. Частичная иерархия класса Glyph
Глифу-родител ю часто бывает нужно «знать», скольк о места на экране зани-
мает глиф-потомок, чтобы расположит ь его и остальные глифы в строке без пере-
крытий (как показан о на рис. 2.2). Операци я Bounds возвращае т прямоугольну ю
область, занимаему ю глифом, точнее, противоположны е углы наименьшег о прямо-
угольника, содержащег о глиф. В подкласса х класса Glyph эта операци я переопре -
делена в соответстви и с природо й конкретног о элемента.
Операци я Intersects возвращае т признак, показывающий, лежит ли задан-
ная точка в предела х глифа. Всякий раз, когда пользовател ь щелкае т мышь ю где-
то в документе, Lexi вызывае т эту операцию, чтобы определить, какой глиф или
глифова я структур а оказалас ь под курсоро м мыши. Класс Rectangl e переопре -
деляе т эту операци ю для вычислени я пересечени я точки с прямоугольником.
Поскольк у у глифов могут быть потомки, то нам необходи м единый интерфей с
для добавления, удалени я и обхода потомков. Например, потомки класса Row - это
глифы, расположенны е в данной строке. Операци я Insert вставляе т глиф в по-
зицию, заданну ю целочисленны м индексом.1 Операци я Remov e удаляе т заданный
глиф, если он действительн о являетс я потомком.
Возможно, целочисленны й индекс - не лучши й способ описани я потомко в глифа. Это зависи т от
структур ы данных, используемо й внутр и глифа. Если потомк и хранятс я в связанно м списке, то бо-
лее эффективн о было бы передават ь указател ь на элемент списка. Мы еще увидим более удачно е ре-
шение проблем ы индексаци и в разделе 2.8, когда будем обсуждат ь анализ документа.
Операция Child возвращает потомка с заданным индексом (если таковой су-
ществует). Глифы типа Row, у которых действительно есть потомки, должны
пользоваться операцией Child, а не обращаться к структуре данных потомка на-
прямую. В таком случае при изменении структуры данных, скажем, с массива на
связанный список не придется модифицировать операции вроде Draw, которые
обходят всех потомков. Аналогично операция Parent предоставляет стандартный
интерфейс для доступа к родителю глифа, если таковой имеется. В Lexi глифы
хранят ссылку на своего родителя, a Parent просто возвращает эту ссылку.
Паттерн компоновщик
Рекурсивная композиция пригодна не только для документов. Мы можем вос-
пользоваться ей для представления любых потенциально сложных иерархичес-
ких структур. Паттерн компоновщик инкапсулирует сущность рекурсивной ком-
позиции объектно-ориентированным способом. Сейчас самое время обратиться
к разделу об этом паттерне и изучить его, имея в виду, только что рассмотренный
сценарий.
2.3. Форматировани е
Мы решили, как представлять физическую структуру документа. Далее нужно
разобраться с тем, как сконструировать конкретную физическую структуру, соответ-
ствующую правильно отформатированному документу. Представление и формати-
рование - это разные аспекты проектирования. Описание внутренней структуры не
дает возможности добраться до определенной подструктуры. Ответственность за
это лежит в основном на Lexi. Редактор разбивает текст на строки, строки - на ко-
лонки и т.д., учитывая при этом пожелания пользователя. Так, пользователь может
изменить ширину полей, размер отступа и положение точек табуляции, установить
одиночный или двойной междустрочный интервал, а также задать много других
параметров форматирования.1 В процессе форматирования это учитывается.
Кстати говоря, мы сузим значение термина «форматирование», понимая под
этим лишь разбиение на строки. Будем считать слова «форматирование» и «раз-
биение на строки взаимозаменяемыми. Обсуждаемая техника в равной мере при-
менима и к разбиению строк на колонки, и к разбиению колонок на страницы.
Таблица 2.2. Базовый интерфейс класса Compositor
Обязанность Операции
что форматировать void SetComposition(Composition*)
когда форматировать vi rtual void Compose ()
У пользователя найдется что сказать и по поводу логической структуры документа: предложений, аб-
зацев, разделов, глав и т.д. Физическая структура в общем-то менее интересна. Большинству людей
не важно, где в абзаце произошел разрыв строки, если в целом все отформатировано правильно. То
же самое относится и к форматированию колонок и страниц. Таким образом, пользователи задают
только высокоуровневые ограничения на физическую структуру, a Lexi выполняет их.
Проектировани е редактор а документо в
Инкапсуляция алгоритма форматирования
С учето м всех ограничени й и детале й процес с форматировани я с трудо м под-
даетс я автоматизации. К этой проблем е есть мног о подходов, и у разны х алгорит -
мов форматировани я имеютс я свои сильны е и слабые стороны. Поскольк у Lexi -
это WYSIWYG-редактор, важно соблюдат ь компромис с межд у качество м и ско-
рость ю форматирования. В обще м случа е желательно, чтоб ы редакто р реагирова л
достаточн о быстр о и при этом внешни й вид документ а оставалс я приемлемым.
На достижени е этог о компромисс а влияе т мног о факторов, и не все из них удаст -
ся установит ь на этапе компиляции. Например, можн о предположить, что пользо -
ватель готов смиритьс я с замедленно й реакцие й в обмен на лучше е качеств о форма -
тирования. При таком предположени и нужно было бы применят ь совершенн о друго й
алгорит м форматирования. Есть также компромис с межд у времене м и памятью,
скорее, имеющи й отношени е к реализации: время форматировани я можн о умень -
шить, если хранит ь в памят и больше информации.
Поскольк у алгоритм ы форматировани я обычн о оказываютс я весьма сложны -
ми, желательно, чтоб ы они был и достаточн о замкнутыми, а еще лучше - полно -
стью независимым и от структур ы документа. Оптимальны й вариан т - добавле -
ние новог о вида глифа вовс е не затрагивае т алгорит м форматирования. С друго й
стороны, при добавлени и новог о алгоритм а форматировани я не должн о возникат ь
необходимост и в модификаци и существующи х глифов.
Учитыва я все вышесказанное, мы должн ы постаратьс я спроектироват ь Lexi
так, чтоб ы алгорит м форматировани я легк о можн о был о заменить, по крайне й
мере, на этапе компиляции, если уж не во время выполнения. Допустим о изоли -
роват ь алгорит м и одновременн о сделат ь его легк о замещаемы м путе м инкапсу -
лировани я в объекте. Точнее, мы определи м отдельну ю иерархи ю классо в для
объектов, инкапсулирующи х алгоритм ы форматирования. Для корневог о класс а
иерархи и определи м интерфейс, которы й поддерживае т широки й спект р алгорит -
мов, а кажды й подклас с буде т реализовыват ь этот интерфей с в виде конкретног о
алгоритм а форматирования. Тогд а удастс я ввест и подклас с класс а Glyph, кото -
рый буде т автоматическ и структурироват ь свои х потомко в с помощь ю передан -
ного ему объекта-алгоритма.
Классы Compositor и Composition
Мы определим класс Compositor (композитор) для объектов, которые мо-
гут инкапсулироват ь алгорит м форматирования. Интерфей с (см. табл. 2.2) по-
зволяе т объект у этого класс а узнать, какие глифы надо форматироват ь и когда. Фор-
матируемы е композиторо м глифы являютс я потомкам и специальног о подкласс а
класс а Glyph, которы й называетс я Compositio n (композиция). Композици я при
создани и получае т объек т некоторог о подкласс а Composito r (специализирован -
ный для конкретног о алгоритм а разбиени я на строки ) и в нужны е момент ы пред -
писывае т композитор у форматироват ь глифы, по мере того как пользовател ь изме -
няет документ. На рис. 2.5 изображен ы отношени я межд у классам и Compositio n
и Compositor.
Рис. 2.5. Отношения классов Composition и Compositor
Неформатированны й объект Compositio n содержит только видимые глифы,
составляющи е основное содержание документа. В нем нет глифов, определяющи х
физическу ю структуру документа, наприме р Row и Column. В таком состоянии
композиция находитс я сразу после создания и инициализаци и глифами, которые
должна отформатировать. Во время форматировани я композици я вызывает опе-
рацию Compos e своего объекта Compositor. Композито р обходит всех потомков
композиции и вставляет новые глифы Row и Column в соответствии со своим ал-
горитмом разбиения на строки.1 На рис. 2.6 показана получающаяс я объектная
структура. Глифы, созданные и вставленные в эту структуру композитором, за-
крашены на рисунке серым цветом.
Каждый подкласс класса Composito r может реализовыват ь свой собствен-
ный алгорит м форматирования. Например, класс SimpleComposito r мог бы
осуществлят ь быстрый проход, не обращая внимани я на такую экзотику, как
«цвет» документа. Под «хорошим цветом» понимаетс я равномерно е распределе -
ние текста и пустого пространства. Класс TeXCompositor мог бы реализовывать
полный алгоритм TjX[Knu84], учитывающи й наряду со многими другими веща-
ми и цвет, но за счет увеличения времени форматирования.
Наличие классов Compositor и Composition обеспечивает четкое отде-
ление кода, поддерживающег о физическу ю структур у документа, от кода раз-
личных алгоритмо в форматирования. Мы можем добавит ь новые подкласс ы
к классу Compositor, не трогая классов глифов, и наоборот. Фактическ и допус-
тимо подменит ь алгоритм разбиения на строки во время выполнения, добавив
одну-единственну ю операцию SetComposito r к базовому интерфейс у класса
Composition.
1 Композито р должен получить коды символов глифов Character, чтобы вычислит ь места разбиения
на строки. В разделе 2.8 мы увидим, как можно получит ь информаци ю полиморфно, не добавляя спе-
цифичной для символов операции к интерфейс у класса Glyph.
Рис. 2.6. Объектная структура, отражающая алгоритм разбиения на строки,
выбираемый композитором
Стратегия
Инкапсуляци я алгоритма в объект - это назначение паттерна стратегия. Ос-
новными участниками паттерна являютс я объекты-стратегии, инкапсулирующи е
различные алгоритмы, и контекст, в котором они работают. Композитор ы пред-
ставляют вариант ы стратегий; они инкапсулирую т алгоритмы форматирования.
Композиция - это контекст для стратегии композитора.
Ключ к применени ю паттерна стратегия - спроектироват ь интерфейс ы стра-
тегии и контекста, достаточно общие для поддержки широког о диапазона алго-
ритмов. Не должно возникат ь необходимост и изменят ь интерфейс стратегии или
контекста для поддержки нового алгоритма. В нашем примере поддержка досту-
па к потомкам, их вставки и удаления, предоставляема я базовыми интерфейсами
класса Glyph, достаточно общая, чтобы подклассы класса Composito r могли из-
менять физическу ю структуру документ а независимо от того, с помощью каких
алгоритмов это делается. Аналогично интерфейс класса Composito r дает компо-
зициям все, что им необходимо для инициализаци и форматирования.
2.4. Оформлени е пользовательског о интерфейс а
Рассмотрим два усовершенствовани я пользовательског о интерфейс а Lexi.
Первое добавляет рамку вокруг области редактировани я текста, чтобы четко обо-
значить страницу текста, второе - полосы прокрутки, позволяющи е пользовате -
лю просматриват ь разные части страницы. Чтобы упростит ь добавление и удале-
ние таких элементов оформления (особенно во время выполнения), мы не должны
использоват ь наследование. Максимально й гибкости можно достичь, если другим
объектам пользовательског о интерфейс а даже не будет известно о том, какие еще
есть элемент ы оформления. Это позволит добавлят ь и удалять декорации, не из-
меняя других классов.
Оформлени е пользовательског о интерфейс а
Прозрачное обрамление
В программировани и оформление пользовательског о интерфейса означает
расширение существующег о кода. Использование для этой цели наследования не
дает возможности реорганизоват ь интерфейс во время выполнения. Не менее се-
рьезной проблемой является комбинаторный рост числа классов в случае широ-
кого использования наследования.
Можно было бы добавить рамку к классу Composition, породив от него но-
вый подкласс BorderedComposition. Точно так же можно было бы добавить
и интерфейс прокрутки, породив подкласс ScrollableComposition t Если же
мы хотим иметь и рамку, и полосу прокрутки, следовало бы создать подкласс
BorderedScrollableComposition, и так далее. Если довести эту идею до ло-
гического завершения, то пришлось бы создавать отдельный подкласс для каж-
дой возможной комбинации элементов оформления. Это решение быстро пере-
стает работать, когда количество разнообразных декораций растет.
С помощью композиции объектов можно найти куда более приемлемый и гиб-
кий механизм расширения. Но из каких объектов составлять композицию? По-
скольку известно, что мы оформляем существующий глиф, то и сам элемент оформ-
ления могли бы сделать объектом (скажем, экземпляром класса Border).
Следовательно, композиция может быть составлена из глифа и рамки. Следую-
щий шаг - решить, что является агрегатом, а что - компонентом. Допустимо счи-
тать, что рамка содержит глиф, и это имеет смысл, так как рамка окружает глиф
на экране. Можно принять и противоположно е решение - поместить рамку
внутрь глифа, но тогда пришлось бы модифицироват ь соответствующий подкласс
класса Glyph, чтобы он «знал» о наличии рамки. Первый вариант - включение
глифа в рамку - позволяет поместить весь код для отображения рамки в классе
Border, оставив остальные классы без изменения.
Как выглядит класс Border? Тот факт, что у рамки есть визуальное представ-
ление, наталкивает на мысль, что она должна быть глифом, то есть подклассом клас-
са Glyph. Но есть и более настоятельные причины поступить именно таким обра-
зом: клиентов не должно волновать, есть у глифов рамки или нет. Все глифы
должны трактоваться единообразно. Когда клиент сообщает простому глифу без
рамки о необходимости нарисовать себя, тот делает это, не добавляя никаких эле-
ментов оформления. Если же этот глиф заключен в рамку, то клиент не должен об-
рабатывать рамку как-то специально; он просто предписывает составному глифу
выполнить отображение точно так же, как и простому глифу в предыдущем случае.
Отсюда следует, что интерфейс класса Border должен соответствоват ь интерфей-
су класса Glyph. Чтобы гарантировать это, мы и делаем Border подклассом Glyph.
Все это подводит нас к идее прозрачного обрамления (transparent enclosure), где
комбинируютс я понятия о композиции с одним потомком (однокомпонентные),
и о совместимых интерфейсах. В общем случае клиенту неизвестно, имеет ли он дело
с компонентом или его обрамлением (то есть родителем), особенно если обрамление
просто делегирует все операции своему единственному компоненту. Но обрамление
может также и расширять поведение компонента, выполняя дополнительные дей-
ствия либо до, либо после делегирования (а возможно, и до, и после). Обрамление
может также добавить компоненту состояние. Как именно, будет показано ниже.
Проектировани е редактор а документо в
Моноглиф
Концепцию прозрачного обрамления можно применить ко всем глифам, оформ-
ляющим другие глифы. Чтобы конкретизировать эту идею, определим подкласс
класса Glyph, называемый MonoGlyph. Он будет выступать в роли абстрактного
класса для глифов-декораций вроде рамки (см. рис. 2.7). В классе MonoGlyph хра-
нится ссылка на компонент, которому он и переадресует все запросы. При этом
MonoGlyph по определению становится абсолютно прозрачным для клиентов.
Вот как моноглиф реализует операцию Draw:
void MonoGlyph::Dra w (Window * w) {
_component->Draw(w);
}
Подклассы MonoGlyph замещают по меньшей мере одну из таких операций
переадресации. Например, Border: :Draw сначала вызывает операцию роди-
тельского класса MonoGlyph : : Draw, чтобы компонент выполнил свою часть ра-
боты, то есть нарисовал все, кроме рамки. Затем Border : : Draw рисует рамку,
вызывая свою собственную закрытую операцию DrawBorder, детали которой
мы опустим:
voi d Border::Dra w (Window * w) {
MonoGlyph::Draw(w);
DrawBorder(w);
}
Обратите внимание, что Border : : Draw, по сути дела, расширяет операцию
родительского класса, чтобы нарисовать рамку. Это не то же самое, что простая
замена операции: в таком случае MonoGlyph : : Draw не вызывалась бы.
На рис. 2.7 показан другой подкласс класса MonoGlyph. Scroller - это
MonoGlyph, который рисует свои компоненты на экране в зависимости от
Рис. 2.7. Отношения класса MonoGlyph с другими классами
Оформлени е пользовательског о интерфейс а
положени я дву х поло с прокрутки, добавляющихс я в качеств е элементо в оформ -
ления. Когд а Scroller отображае т свой компонент, графическа я систем а обре -
зает его по граница м окна. Отсеченны е част и компонента, оказавшиес я за преде -
лами видимо й част и окна, не появляютс я на экране.
Тепер ь у нас есть все, что необходим о для добавлени я рамк и и прокрутк и
к област и редактировани я текст а в Lexi. Мы помещае м имеющийс я экземпля р
класс а Composition в экземпля р класс а Scroller, чтоб ы добавит ь интерфей с
прокрутки, а результа т композици и еще раз погружае м в экземпля р класс а
Border. Получившийс я объект показан на рис. 2.8.
Рис. 2.8. Объектная структура после добавления элементов оформления
Обратит е внимание, что мы могл и изменит ь порядо к композици и на обрат -
ный, сначал а добави в рамку, а пото м погрузи в результат в Scroller. В таком слу-
чае рамк а прокручивалас ь бы вмест е с текстом. Може т быть, это то, что вам нужно,
а может, и нет. Важн о лишь, что прозрачно е обрамлени е легк о позволяе т экс-
периментироват ь с разным и вариантами, освобожда я клиент а от знани я детале й
кода, добавляющег о декорации.
Отметим, что рамк а допускае т композици ю тольк о с одним глифом, не более
того. Этим она отличаетс я от рассмотренны х выше композиций, где родительском у
объект у позволялос ь имет ь скольк о угодн о потомков. Здесь же заключени е чего-т о
в рамку предполагает, что это «что-то» имеется в единственно м экземпляре. Мы
могли бы приписат ь некотору ю семантику декорации более одного объекта, но
тогда пришлось бы вводить множеств о видов композици й с оформлением: офор-
мление строки, колонки и т.д. Это не дает ничего нового, так как у нас уже есть
классы для такого рода композиций. Поэтому для композици и лучше использо-
вать уже существующи е классы, а новые добавлят ь для оформления результата.
Отделение декорации от других видов композици и одновременн о упрощает клас-
сы, реализующи е разные элементы оформления, и уменьшае т их количество. Кро-
ме того, мы избавлены от необходимост и дублироват ь уже имеющуюс я функцио-
нальность.
Паттерн декоратор
Паттерн декоратор абстрагируе т отношения между классами и объектами,
необходимые для поддержки оформлени я с помощь ю техники прозрачног о об-
рамления. Термин «оформление » на самом деле применяетс я в более широком
смысле, чем мы видели выше. В паттерне декоратор под ним понимаетс я нечто,
что возлагает на объект новые обязанности. Можно, например, представит ь себе
оформление абстрактног о дерева синтаксическог о разбора семантическим и дей-
ствиями, конечног о автомата - новыми состояниями или сети, состоящей из устой-
чивых объектов, - тэгами атрибутов. Декоратор обобщает подход, который мы
использовал и в Lexi, чтобы расширит ь его область применения.
2.5. Поддержк а нескольки х стандарто в
внешнег о облик а
При проектировани и системы приходитс я сталкиватьс я с серьезной пробле-
мой: как добиться переносимост и между различными программно-аппаратным и
платформами. Перенос Lexi на другую платформу не должен требовать капиталь -
ного перепроектирования, иначе не стоит за него и браться. Он должен быть мак-
симально прост.
Одним из препятствий для переноса является разнообрази е стандартов внеш-
него облика, призванных унифицироват ь работу с приложениям и на данной плат-
форме. Эти стандарт ы определяют, как приложени я должны выглядет ь и реа-
гировать на действия пользователя. Хотя существующи е стандарт ы не так уж
сильно отличаютс я друг от друга, ни один пользовател ь не спутает один стандарт
с другим - приложения, написанные для Motif, выглядят не совсем так, как ана-
логичные приложени я на других платформах, и наоборот. Программа, работаю-
щая более чем на одной платформе, должна всюду соответствоват ь принятой сти-
листике пользовательског о интерфейса.
Наша проектна я цель - сделать так, чтобы Lexi поддержива л разные стандар-
ты внешнег о облика и чтобы легко можно было добавить поддержк у нового стан-
дарта, как только таковой появится (а это неизбежно произойдет). Хотелось бы
также, чтобы наш дизайн решал и другую задачу: изменение внешнег о облика Lexi
во время выполнения.
Поддержка нескольких стандартов
Абстрагирование создания объекта
Все, что мы видим и с чем можем взаимодействоват ь в пользовательско м ин-
терфейсе Lexi, - это визуальные глифы, скомпонованны е в другие, уже невидимые
глифы вроде строки (Row) и колонки (Column). Невидимые глифы содержат види-
мые - скажем, кнопку (Button) или символ (Character) - и правильно распола-
гают их на экране. В руководства х по стилистическом у оформлению много гово-
рится о внешнем облике и поведении так называемых «виджетов » (widgets); это
просто другое название таких видимых глифов, как кнопки, полосы прокрутки
и меню, выполняющи х в пользовательско м интерфейс е функции элементов управ-
ления. Для представлени я данных виджеты могут пользоватьс я более простыми
глифами: символами, окружностями, прямоугольникам и и многоугольниками.
Мы будем предполагать, что имеется два набора классов глифов-виджетов,
с помощью которых реализуютс я стандарт ы внешнег о облика:
а набор абстрактных подклассов класса Glyph для каждой категории видже-
тов. Например, абстрактный класс ScrollBar будет дополнят ь интерфейс
глифа с целью получения операций прокрутки общего вида, a Button - это
абстрактный класс, добавляющи й операции с кнопками;
а набор конкретных подклассов для каждого абстрактног о подкласса, в кото-
рых реализованы стандарты внешнег о облика. Так, у Scrol IBar могут быть
подклассы MotifScrollBar и PMScrollBar, реализующие полосы про-
крутки в стиле Moti f и Presentatio n Manager соответственно.
Lexi должен различать глифы-виджет ы для разных стилей внешнего оформле-
ния. Например, когда необходимо поместить в интерфейс кнопку, редактор должен
инстанцироват ь подкласс класса Glyph для нужног о стиля кнопки (Mot i f Button,
PMButton, MacButton и т.д.).
Ясно, что в реализации Lexi это нельзя сделать непосредственно, например,
вызвав конструктор, если речь идет о языке C++. При этом была бы жестко зако-
дирована кнопка одного конкретног о стиля, значит, выбрать нужный стиль во
время выполнени я оказалось бы невозможно. Кроме того, мы были бы вынужде -
ны отслеживат ь и изменять каждый такой вызов конструктор а при переносе Lexi
на другую платформу. А ведь кнопки - это лишь один элемент пользовательског о
интерфейса Lexi. Загромождени е кода вызовами конструкторо в для разных клас-
сов внешнег о облика вызывае т существенные неудобств а при сопровождении.
Стоит что-нибудь пропустит ь - и в приложении, ориентированно м на платформу
Мае, появится меню в стиле Motif.
Lexi необходим какой-то способ определит ь нужный стандарт внешнег о об-
лика для создания подходящи х виджетов. При этом надо не только постаратьс я
избежать явных вызовов конструкторов, но и уметь без труда заменять весь набор
виджетов. Этого можно добиться путем абстрагирования процесса создания объек-
та. Далее на примере мы продемонстрируем, что имеется в виду.
Фабрики и изготовленные классы
В Moti f для создания экземпляр а глифа полосы прокрутки обычно было до-
статочно написать следующий код.на C++:
Проектировани е редактор а документо в
ScrollBar* sb = new MotifScrollBar;
Но такого кода надо избегать, если мы хотим минимизироват ь зависимость
Lexi от стандарта внешнего облика. Предположим, однако, что sb инициализиру -
ется так:
ScrollBar* sb = guiFactory->CreateScrollBar();
где guiFactory - CreateScrollBar объект класса Mot if Factory. Операция
возвращает новый экземпляр подходящего подкласса ScrollBar, который соот-
ветствует желательному варианту внешнего облика, в нашем случае - Motif.
С точки зрения клиентов результат тот же самый, что и при прямом обращении
к конструктору Motif ScrollBar. Но есть и существенное отличие: нигде в коде 4
больше не упоминается имя Motif. Объект guiFactory абстрагирует процесс
создания не только полос прокрутки для Motif, но и любых других. Более того,
guiFactor y не ограничен изготовление м только полос прокрутки. Его можно
применять для производства любых виджетов, включая кнопки, поля ввода, меню
и т.д.
Все это стало возможным, поскольку Mot if Factor y является подклассом
GUIFactor y - абстрактног о класса, который определяет общий интерфейс для
создания глифов-виджетов. В нем есть такие операции, как CreateScrollBar
и CreateButton, для инстанцировани я различных видов виджетов. Подклассы
GUIFactory реализуют эти операции, возвращая глифы вроде Mot if ScrollBar
и PMButton, которые имеют нужный внешний облик и поведение. На рис. 2.9 по-
казана иерархия классов для объектов guiFactory.
Мы говорим, что фабрики изготавливают объекты. Продукты, изготовленные
фабриками, связаны друг с другом; в нашем случае все такие продукты - это вид-
жеты, имеющие один и тот же внешний облик. На рис. 2.10 показаны некоторые клас-
сы, необходимые для того, чтобы фабрика могла изготавливать глифы-виджеты.
Экземпляр класса GUIFactor y может быть создан любым способом. Перемен-
ная guiFactor y может быть глобальной или статическим членом хорошо извест-
ного класса или даже локальной, если весь пользовательский интерфейс создается
внутри одного класса или функции. Существует специальный паттерн проектиро-
вания одиночка, предназначенный для работы с такого рода объектами, существу-
ющими в единственном экземпляре. Важно, однако, чтобы фабрика guiFactor y
была инициализирован а до того, как начнет использоватьс я для производства
объектов, но после того, как стало известно, какой внешний облик нужен.
Когда вариант внешнего облика известен на этапе компиляции, то guiFactor y
можно инициализироват ь простым присваивание м в начале программы:
GUIFactory* guiFactory = new MotifFactory;
Если же пользователю разрешается задавать внешний облик с помощью стро-
ки-параметра при запуске, то код создания фабрики мог бы выглядеть так:
GUIFactory* guiFactory;
const char* styleName = getenv("LOOK_AND_FEEL");
// получаем это от пользователя или среды при запуске
Поддержк а нескольки х стандарто в
Рис. 2.9. Иерархия классов GUIFactory
Рис. 2.10. Абстрактные классы-продукты и их конкретные подклассы
i f (strcmp(styleName, "Mot i f") = = 0 ) {
guiFactory = new MotifFactory;
} else if (strcmp(styleName, "Presentation_Manager") = = 0 ) {
guiFactor y = ne w PMFactory;
} els e {
guiFactor y = new DefaultGUIFactory;
}
Проектировани е редактор а документо в
Есть и другие способы выбора фабрики во время выполнения. Например,
можно было бы вести реестр, в котором символьные строки отображаются на
объекты фабрик. Это позволяет зарегистрировать экземпляр новой фабрики, не
меняя существующий код, как то требуется при предыдущем подходе. И нет нуж-
ды связывать с приложением код фабрик для всех платформ. Это существенно,
поскольку связать код для Moti f Factory с приложением, работающим на плат-
форме, где Motif не поддерживается, может оказаться невозможным.
Важно, впрочем, лишь то, что после конфигурации приложения для работы с кон-
кретной фабрикой объектов, мы получаем нужный внешний облик. Если впослед-
ствии мы изменим решение, то сможем инициализировать guiFactory по-другому,
чтобы изменить внешний облик, а затем динамически перестроим интерфейс.
Паттерн абстрактная фабрика
Фабрики и их продукция - вот ключевые участники паттерна абстрактная
фабрика. Этот паттерн может создавать семейства объектов, не инстанцируя
классы явно. Применять его лучше всего, когда число и общий вид изготавливае-
мых объектов остаются постоянными, но между конкретными семействами про-
дуктов имеются различия. Выбор того или иного семейства осуществляется путем
инстанцирования конкретной фабрики, после чего она используется для созда-
ния всех объектов. Подставив вместо одной фабрики другую, мы можем заменить
все семейство объектов целиком. В паттерне абстрактная фабрика акцент дела-
ется на создании семейств объектов, и это отличает его от других порождающих
паттернов, создающих только один какой-то вид объектов.
2.6. Поддержк а нескольки х оконны х систе м
Как должно выглядеть приложение - это лишь один из многих вопросов, вста-
ющих при переносе приложения на новую платформу. Еще одна проблема из той
же серии - оконная среда, в которой работает Lexi. Данная среда создает иллю-
зию наличия нескольких перекрывающихся окон на одном растровом дисплее.
Она распределяет между окнами площадь экрана и направляет им события кла-
виатуры и мыши. Сегодня существует несколько широко распространенных и во
многом не совместимых между собой оконных систем (например, Macintosh, Pre-
sentation Manager, Windows, X). Мы хотели бы, чтобы Lexi работал в любой окон-
ной среде по тем же причинам, по которым мы поддерживаем несколько стандар-
тов внешнего облика.
Можно ли воспользоваться абстрактной фабрикой?
На первый взгляд представляется, что перед нами еще одна возможность при-
менить паттерн абстрактная фабрика. Но ограничения, связанные с переносом
на другие оконные системы, существенно отличаются от тех, что накладывают не-
зависимость от внешнего облика.
Применяя паттерн абстрактная фабрика, мы предполагали, что удастся опре-
делить конкретный класс глифов-виджетов для каждого стандарта внешнего об-
лика. Это означало, что можно будет произвести конкретный класс для данного
Поддержк а нескольки х оконны х систе м
стандарта (например, Mot if ScrollBar иMacScrollBar) от абстрактного клас-
са (допустим, ScrollBar). Предположим, однако, что у нас уже есть несколько
иерархий классов, полученных от разных поставщиков, - по одной для каждого
стандарта. Маловероятно, что данные иерархии будут совместимы между собой.
Поэтому отсутствуют общие абстрактные изготавливаемые классы для каждого
вида виджетов (ScrollBar, Button, Menu и т.д.). А без них фабрика классов
работать не может. Необходимо, чтобы иерархии виджетов имели единый набор
абстрактных интерфейсов. Только тогда удастся правильно объявить операции
Create. . . в интерфейсе абстрактной фабрики.
Для виджетов мы решили эту проблему, разработав собственные абстрактные
и конкретные изготавливаемые классы. Теперь сталкиваемся с аналогичной труд-
ностью при попытке заставить Lexi работать во всех существующих оконных сре-
дах. Именно разные среды имеют несовместимые интерфейсы программирования.
Но на этот раз все сложнее, поскольку мы не можем себе позволить реализовать
собственную нестандартную оконную систему.
Однако спасительный выход все же есть. Как и стандарты на внешний облик,
интерфейсы оконных систем не так уж радикально отличаются друг от друга, ибо
все они предназначены примерно для одних и тех же целей. Нам нужен унифици-
рованный набор оконных абстракций, которым можно закрыть любую конкрет-
ную реализацию оконной системы.
Инкапсуляция зависимостей от реализации
В разделе 2.2 мы ввели класс Window для отображения на экране глифа или
структуры, состоящей из глифов. Ничего не говорилось о том, с какой оконной
системой работает этот объект, поскольку в действительности он вообще не свя-
зан ни с одной системой. Класс Window инкапсулирует понятие окна в любой
оконной системе:
а операции для отрисовки базовых геометрических фигур;
а возможность свернуть и развернуть окно;
Таблица 2.3. Интерфейс класса Window
Обязанность
Операции
управлени е окнам и
virtual void Redraw()
virtual void Raise()
virtual void Lower()
virtual void IconifyO
virtual void DeiconifyO
...
график а
virtual void DrawLine(...)
virtual void DrawRect(...)
virtual void DrawPolygon(... )
virtual void DrawText(...)
...
Проектировани е редактор а документо в
а возможност ь изменит ь собственны е размеры;
а возможност ь при необходимост и отобразит ь свое содержимое, наприме р
при развертывани и или открыти и ране е перекрыто й част и окна.
Клас с Windo w долже н покрыват ь функциональност ь окон, котора я имеетс я
в различны х оконны х системах. Рассмотри м два крайни х подхода:
а пересечение функциональности. Интерфей с класс а Windo w предоставляе т
тольк о операции, общи е для всех оконны х систем. Однак о в результат е мы
получае м интерфей с не богаче, чем в самой'слабо й из рассматриваемы х сис-
тем. Мы не може м воспользоватьс я боле е развитым и средствами, даже если
их поддерживае т большинств о оконны х систе м (но не все);
а объединение функциональности. Создаетс я интерфейс, которы й включае т
возможност и всех существующи х систем. Здес ь нас подстерегае т опасност ь
получит ь чрезмерн о громоздки й и внутренн е не согласованны й интерфейс.
Кроме того, нам придетс я изменят ь его (а вмест е с ним и Lexi ) всяки й раз,
как тольк о производител ь переработае т интерфей с свое й оконно й системы.
Ни то, ни друго е решени е «в чисто м виде » не годятся, поэтом у мы выбере м
компромиссное. Клас с Windo w буде т предоставлят ь удобны й интерфейс, поддер -
живающи й наиболе е популярны е возможност и оконны х систем. Поскольк у ре-
дакто р Lexi буде т работат ь с классо м Windo w напрямую, этот клас с долже н под-
держиват ь и сущности, о которы х Lexi известно, то есть глифы. Это означает, что
интерфей с класс а Windo w долже н включат ь базовы й набо р графически х опера -
ций, позволяющи й глифа м отображат ь себя в окне. В табл. 2.3 перечислен ы неко -
торые операци и из интерфейс а класс а Window.
Windo w - это абстрактны й класс. Его конкретны е подкласс ы поддерживаю т
различны е виды окон, с которым и имеет дело пользователь. Например, окна прило -
жений, сообщений, значк и - это все окна, но свойств а у них разные. Для учет а таких
различи й мы може м определит ь подкласс ы Applicationwindow, IconWindo w
и DialogWindow. Возникающа я иерархи я позволяе т таки м приложениям, как
Поддержк а нескольки х оконны х систем
Lexi, создать унифицированную, интуитивно понятную абстракцию окна, не за-
висящую от оконной системы конкретног о поставщика.
Итак, мы определили оконный интерфейс, с которым будет работать Lexi. Но
где же в нем место для реальной платформенно-зависимо й оконной системы?
Если мы не собираемся реализовыват ь собственную оконную систему, то в каком-
то месте наша абстракция окна должна быть выражена в терминах целевой систе-
мы. Но где именно?
Можно было бы реализовать несколько вариантов класса Window и его под-
классов - по одному для каждой оконной среды. Выбор нужного варианта про-
изводится при сборке Lexi для данной платформы. Но представьте себе, с чем
вы столкнетесь при сопровождении, если придется отслеживать множество раз-
ных классов с одним и тем же именем Window, но реализованных для разных
оконных систем. Вместо этого мы могли бы создать зависящие от реализации
подклассы каждого класса в иерархии Window, но закончилось бы это тем же
самым комбинаторным ростом числа классов, о котором уже говорилось при
попытке добавить элементы оформления. Кроме того, оба решения недостаточ-
но гибки, чтобы можно было перейти на другую оконную систему уже после
компиляции программы. Поэтому придется поддерживат ь несколько разных
исполняемых файлов.
Ни тот, ни другой вариант не выглядят привлекательно, но что еще можно сде-
лать? То же самое, что мы сделали для форматирования и декорирования, - ин-
капсулировать изменяющуюся сущность. В этом случае изменчивой частью явля-
ется реализация оконной системы. Если инкапсулироват ь функциональност ь
оконной системы в объекте, то удастся реализовать свой класс Window и его под-
классы в терминах интерфейса этого объекта. Более того, если такой интерфейс
сможет поддержать все интересующие нас оконные системы, то не придется из-
менять ни Window, ни его подклассы при переходе на другую систему. Мы скон-
фигурируем оконные объекты в соответствии с требованиями нужной оконной
системы, просто передав им подходящий объект, инкапсулирующи й оконную сис-
тему. Это можно сделать даже во время выполнения.
Классы Window и Windowlmp
Мы определим отдельную иерархию классов Windowlmp, в которой скроем
знание о различных реализациях оконных систем. Windowlmp - это абстрактный
класс для объектов, инкапсулирующи х системно-зависимый код. Чтобы заставить
Lexi работать в конкретной оконной системе, каждый оконный объект будем кон-
фигурировать экземпляром того подкласса Windowlmp, который предназначен
для этой системы. На диаграмме ниже представлены отношения между иерархи-
ями Window и Windowlmp:
Скрыв реализацию в классах Windowlmp, мы сумели избежать «засорения»
классов Window зависимостями от оконной системы. В результате иерархия
Window получается сравнительно компактной и стабильной. В то же время мы
можем расширить иерархию реализаций, если будет нужно поддержать новую
оконную систему.
Проектирование редактора документов
Подклассы Windowlmp
Подклассы Windowlmp преобразуют запросы в операции, характерные для
конкретной оконной системы. Рассмотрим пример из раздела 2.2. Мы определи-
ли Rectangle: : Draw в терминах операции DrawRect над экземпляром класса
Window:
voi d Rectangle::Dra w (Window * w) {
w->DrawRect(_xO, _yO, _xl, _yl);
}
В реализации DrawRect по умолчанию используется абстрактная операция
рисования прямоугольников, объявленная в Windowlmp:
void Window: : DrawRect (
Coord xO, Coord yO, Coord xl, Coord yl
) {
_imp->DeviceRect (xO , yO , xl , yl) ;
}
где _imp - переменная-член класса Window, в которой хранится указатель на
объект Windowlmp, использованный при конфигурировании Window. Реализация
окна определяется тем экземпляром подкласса Windowlmp, на который указыва-
ет _imp. Для XWindowImp (то есть подкласса Windowlmp для оконной системы
X Window System) реализация DeviceRect могла бы выглядеть так:
void XWindowImp::DeviceRec t (
Coor d xO, Coor d yO', Coor d xl, Coor d yl
) {
int x = round(mi n(xO, xl ) );
int у = round(mi n(yO, y l ) );
int w = round(abs(x O - x l ) );
int h = round(abs(y O - yl ) );
XDrawRectangle(_dpy, _winid, _gc, x, y, w, h) ;
}
Поддержк а нескольки х оконны х систе м
DeviceRect определено именно так, поскольку XDrawRectangle (функция
X Windows для рисования прямоугольников) определяет прямоугольник с по-
мощью левого нижнего угла, ширины и высоты. DeviceRect должна вычислить
эти значения по переданным ей параметрам. Сначала находится левый нижний
угол (поскольку (хО, уО) может обозначать любой из четырех углов прямоуголь-
ника), а затем вычисляется длина и ширина.
Подкласс PMWindowImp (подкласс Wi ndowl mp для Presentation Manager)
определил бы DeviceRect по-другому:
voi d PMWindowImp::DeviceRec t (
Coor d xO, Coor d yO, Coor d xl, Coor d y l
Coor d l eft = mi n(xO, xl );
Coor d right = max(xO, xl ) ;
Coor d botto m = mi n(yO, yl ) ;
Coor d to p = max(yO, yl );
PPOINT L poi nt [4];
poi nt [ 0].x = l ef t; poi nt [0].y = top;
poi nt[1].x = right; poi nt[1].у = top;
poi nt [2].x = right; poi nt [2].у = bottom;
poi nt [3].x = l eft; poi nt[3].у = bottom;
if (
(GpiBeginPath(_hps, 1L) == false ) ||
(GpiSetCurrentPosition(_hps, &point[3] ) == false ) ||
(GpiPolyLine(_hps, 4L, point ) == GPI_ERROR ) ||
(GpiEndPath(Jips ) == false )
) {
// сообщит ь об ошибк е
} els e {
GpiStrokePath(_hps, 1L, OL);
}
}
Откуда такое отличие от версии для X? Дело в том, что в Presentation Manager
(РМ) нет явной операции для рисования прямоугольников, как в X. Вместо этого
РМ имеет более общий интерфейс для задания вершин фигуры, состоящей из
нескольких отрезков (множество таких вершин называется траекторией), и для
рисования границы или заливки той области, которую эти отрезки ограничивают.
Очевидно, что реализации DeviceRect для РМ и X совершенно непохожи,
но это не имеет никакого значения. Возможно, Windowlmp скрывает различия ин-
терфейсов оконных систем за большим, но стабильным интерфейсом. Это позво-
ляет автору подкласса Window сосредоточиться на абстракции окна, а не на дета-
лях оконной системы. Заодно мы-получаем возможность добавлять поддержку
для новых оконных систем, не изменяя классы из иерархии Window.
Проектирование редактора документов
Конфигурирование класса Window с помощью Windowlmp
Важнейший вопрос, который мы еще не рассмотрели, - как сконфигуриро -
вать окно с помощью подходящег о подкласса Windowlmp. Другими словами, ког-
да инициализируетс я переменна я _imp и как узнать, какая оконная система (сле-
довательно, и подкласс Windowlmp ) используется? Ведь окну необходим объект
Windowlmp.
Тут есть несколько возможностей, но мы остановимся на той, где использует -
ся паттерн абстрактна я фабрика. Можно определит ь абстрактный фабричный
класс WindowSystemFactory, предоставляющи й интерфейс для создания раз-
личных видов системно-зависимы х объектов:
class WindowSystemFactory {
public:
virtual Windowlmp* CreateWindowImp( ) = 0;
virtual Colorlmp* CreateColorlmp() = 0;
virtual Fontlmp* CreateFontImp( ) = 0;
// операции "Create..." для всех видов ресурсов оконной системы
};
Далее разумно определить конкретную фабрику для каждой оконной системы:
Чтобы инициализироват ь член _imp указателем на объект Windowlmp, соот-
ветствующий данной оконной системе, конструкто р базового класса Window мо-
жет использоват ь интерфейс WindowSystemFactory:
Переменна я WindowSystemFactor y - это известный программе экземпляр
подкласса WindowSystemFactory. Она, аналогично переменной guiFactory,
определяет внешний облик. И инициализироват ь WindowSystemFactor y мож-
но точно так же.
Паттерн мост
Класс Windowlmp определяе т интерфейс к общим средствам оконной систе-
мы, но на его дизайн накладываютс я иные ограничения, нежели на интерфейс
Операции пользователя
класса Window. Прикладной программист не обращается к интерфейсу Windowlmp
непосредственно, он имеет дело только с объектами класса Window. Поэтому ин-
терфейс Windowlmp необязательно должен соответствоват ь представлению про-
граммиста о мире, как то было в случае с иерархией и интерфейсом класса Window.
Интерфейс Windowlmp может более точно отражать сущности, которые в дей-
ствительности предоставляют оконные системы, со всеми их особенностями. Он
может быть ближе к идее пересечения или объединения функциональност и -
в зависимости от требований к целевой оконной системе.
Важно понимать, что интерфейс класса Window призван обслуживать интере-
сы прикладного программиста, тогда как интерфейс класса Windowlmp в большей
степени ориентирован на оконные системы. Разделение функциональност и окон
между иерархиями Window и Windowlmp позволяет нам независимо реализовы-
вать и специализироват ь их интерфейсы. Объекты из этих иерархий взаимодей-
ствуют, позволяя Lexi работать без изменений в нескольких оконных системах.
Отношение иерархий Window и Windowlmp являет собой пример паттерна
мост. Идея его создания заключалась в том, чтобы предоставить возможность со-
вместной работы отдельным иерархиям классов, даже в случае их раздельного
эволюционирования. Критерии разработки, которыми мы руководствовались, за-
ставили нас создать две различные иерархии классов: одну, поддерживающу ю
логическое понятие окон, и другую для хранения промежуточных вариантов окон.
Паттерн мост позволяет нам сохранять и совершенствоват ь наши логические аб-
стракции управления окнами без необходимост и привлечения программно-зави-
симого кода и наоборот.
2.7. Операци и пользовател я
Часть функциональност и Lexi доступна через WYSIWYG-представлени е до-
кумента. Вы вводите и удаляете текст, перемещаете точку вставки и выбираете
участки текста, просто указывая и щелкая мышью или нажимая клавиши. Другая
часть функциональност и доступна через выпадающие меню, кнопки и клавиши-
ускорители. К этой категории относятся такие операции:
а создание нового документа;
а открытие, сохранение и печать существующег о документа;
а вырезание выбранной части документа и вставка ее в другое место;
а изменение шрифта и стиля выбранного текста;
а изменение форматирования текста, например, установка режима выравни-
вания;
а завершение приложения и др.
Lexi предоставляет для этих операций различные пользовательские интерфей-
сы. Но мы не хотим ассоциировать конкретную операцию с определенным пользо-
вательским интерфейсом, поскольку для выполнения одной и той же операции
желательно иметь несколько интерфейсов (например, листать страницы можно
с помощью кнопки или выбора из меню). Кроме того, в будущем может понадо-
биться изменить интерфейс.
Проектировани е редактор а документо в
Далее. Операци и реализован ы в разны х классах. Нам как разработчика м хоте -
лось бы получат ь досту п к функциональност и классов, не создава я зависимосте й
межд у классами, отвечающим и за реализаци ю и за пользовательски й интерфейс.
В противно м случа е получитс я сильн о связанны й код, которы й буде т трудн о по-
нять, модернизироват ь и сопровождать.
Ситуаци я осложняетс я еще и тем, что Lex i долже н поддерживат ь операци и
отмен ы и повтора 1 большинства, но не всех функций. Точнее, желательн о умет ь
отменят ь операци и модификаци и документ а (скажем, удаление), которы е из-з а
оплошност и пользовател я могу т привест и к уничтожени ю большог о объем а дан-
ных. Но не следуе т пытатьс я отменит ь таку ю операцию, как сохранени е чертеж а
или завершени е приложения. Мы также не хотел и бы налагат ь произвольны е огра -
ничени я на числ о уровне й отмен ы и повтора.
Ясно, что поддержк а пользовательски х операци й распределен а по всему при -
ложению. Задач а в том, чтоб ы найт и просто й и расширяемы й механизм, удовлет -
воряющи й всем вышеизложенны м требованиям.
Инкапсуляция запроса
С точк и зрени я проектировщик а выпадающе е меню - это прост о еще один вид
вложенны х глифов. От други х глифов, имеющи х потомков, его отличае т то, что
большинств о содержащихс я в меню глифо в каким-т о образо м реагируе т на отпус -
кани е кнопк и мыши.
Предположим, что таки е реагирующи е глиф ы являютс я экземплярам и под -
класс а Men u It em класс а Glyp h и что свою работ у они выполняю т в отве т на за-
прос клиента 2. Для выполнени я запрос а може т потребоватьс я вызват ь одну операци ю
одног о объект а или мног о операци й разны х объектов. Возможн ы и промежуточны е
варианты.
Мы могл и бы определит ь подклас с класс а Men u It em для каждо й операци и
пользователя, а зате м жестк о закодироват ь кажды й подклас с для выполнени я со-
ответствующег о запроса. Но вря д ли это правильно: нам не нуже н подклас с
Men u It em для каждог о запроса, точн о так же, как не нуже н отдельны й подклас с
для каждо й строк и в выпадающе м меню. Помим о всег о прочего, тако й подхо д тес-
но привязывае т запро с к конкретном у элемент у пользовательског о интерфейса,
поэтом у выполнит ь тот же запро с чере з друго й интерфей с буде т нелегко.
Предположим, что на последню ю страниц у документ а вы может е перейти,
выбра в соответствующи й пунк т из меню или щелкну в по значк у с изображение м
страниц ы в нижне й част и окна Lex i (для коротки х документо в это удобно). Есл и
мы ассоциируе м запро с с подклассо м Men u It em с помощь ю наследования, то долж-
ны сделат ь то же само е и для значк а страницы, и для любог о другог о виджета,
которы й способе н послат ь подобны й запрос. В результат е числ о классо в буде т
примерн о равн о произведени ю числ а типо в виджето в на числ о запросов.
1 Под повторо м (redo ) понимаетс я выполнени е тольк о что отмененно й операции.
2 Концептуальн о клиенто м являетс я пользовател ь Lexi, но на само м деле это прост о какой-т о друго й
объек т (например, диспетче р событий), которы й управляе т обработко й ввод а пользователя.
Операци и пользовател я
Нам не хватает механизма параметризации пунктов меню запросами, которые
они должны выполнять. Таким способом удалось бы избежать разрастания числа
подклассов и обеспечить большую гибкость во время выполнения. Параметризо-
вать Menu It em можно вызываемой функцией, но это решение неполно по трем
причинам:
а в нем не учитывается проблема отмены/повтора;
а с функцией трудно ассоциироват ь состояние. Например, функция, изменя-
ющая шрифт, должна «знать», какой именно это шрифт;
а функции с трудом поддаются расширению, а использовать их части тоже
затруднительно.
Поэтому лучше параметризоват ь пункты меню объектом, а не функцией. Тог-
да мы сможем прибегнуть к механизму наследования для расширения и повтор-
ного использования реализации запроса. Кроме того, у нас появляется место для
сохранения состояния и возможность реализовать отмену и повтор. Вот еще один
пример инкапсуляции изменяющейс я сущности, в данном случае - запроса. Каж-
дый запрос мы инкапсулируе м в объект-команду.
Класс Command и его подклассы
Сначала определим абстрактный класс Command, который будет предостав-
лять интерфейс для выдачи запроса. Базовый интерфейс включает всего одну аб-
страктную операцию Execute. Подклассы Command по-разному реализуют эту
операцию для выполнения запросов. Некоторые подклассы могут частично или
полностью делегировать работу другим объектам, а остальные выполняют запрос
сами (см. рис. 2.11). Однако для запрашивающег о объект Command — это всего
лишь объект Command, все они рассматриваютс я одинаково.
Рис. 2.11. Часть иерархии класса Command
Проектировани е редактор а документо в
Теперь в классе Menu It em может храниться объект, инкапсулирующи й за-
прос (рис. 2.12). Каждому объекту, представляющему пункт меню, мы передаем
экземпляр того из подклассов Command, который соответствует этому пункту, точ-
но так же, как мы задаем текст, отображаемый в пункте меню. Когда пользователь
выбирает некоторый пункт меню, объект Menu It em просто вызывает операцию
Execut e для своего объекта Command, тем самым предлагая ему выполнить за-
прос. Заметим, что кнопки и другие виджеты могут пользоваться объектами
Command точно так же, как и пункты меню.
Рис. 2.12. Отношение между классами Menultem и Command
Отмена операций
Для того чтобы отменить или повторить команду, нужна операция Unexecut e
в интерфейсе класса Command. Ее выполнение отменяет все, что было сделано пре-
дыдущей командой Execute. При этом используется информация, сохраненная
операцией Execute. Так, при команде FontCommand операция Execute была
бы должна сохранить координаты участка текста, шрифт которого изменялся,
а равно и первоначальный шрифт (или шрифты). Операция Unexecute класса
FontCommand должна была бы восстановить старый шрифт (или шрифты) для
этого участка текста.
Иногда определять, можно ли осуществить отмену, необходимо во время вы-
полнения. Скажем, запрос на изменение шрифта выделенного участка текста не
производит никаких действий, если текст уже отображен требуемым шрифтом.
Предположим, что пользователь выбрал некий текст и решил изменить его шрифт
на случайно выбранный. Что произойдет в результате последующег о запроса на
отмену? Должно ли бессмысленное изменение приводить к столь же бессмыслен-
ной отмене? Наверное, нет. Если пользователь повторит случайное изменение
шрифта несколько раз, то не следует заставлять его выполнять точно такое же
число отмен, чтобы вернуться к последнему осмысленному состоянию. Если сум-
марный эффект выполнения последовательност и команд нулевой, то нет необхо-
димости вообще делать что-либо при запросе на отмену.
Для определения того, можно ли отменить действие команды, мы добавим
к интерфейсу класса Comman d абстрактную операцию Reversible (обратимая),
которая возвращает булево значение. Подклассы могут переопределит ь эту опе-
рацию и возвращать true или f al se в зависимости от критерия, вычисляемог о
во время выполнения.
Операци и пользовател я
История команд
Последни й шаг в направлени и поддержк и отмен ы и повтор а с произвольны м
число м уровне й - определени е истории команд, то есть списк а ране е выполнен -
ных или отмененны х команд. Концептуальн о истори я коман д выгляди т так:
•^ Прошлые команды
Настоящее
Кажды й кружо к представляе т один объек т Command. В данно м случа е пользо -
вател ь выполни л четыр е команды. Лини я с пометко й «настоящее » показывае т са-
мую последню ю выполненну ю (или отмененную ) команду.
Для тог о чтоб ы отменит ь последню ю команду, мы прост о вызывае м операци ю
Unexecut e для само й последне й команды:
Настоящее
Посл е отмен ы команд ы сдвигае м лини ю «настоящее » на одну команд у влево.
Если пользовател ь выполни т еще одну отмену, то произойде т отка т на один шаг
(см. рис. ниже).
Настоящее
Видно, что за счет простог о повторени я процедур ы мы получае м произволь -
ное числ о уровне й отмены, ограниченно е лишь длино й списк а истори и команд.
Чтоб ы повторит ь тольк о что отмененну ю команду, произведе м обратны е дей-
ствия. Команд ы справ а от лини и «настоящее » - те, что могу т быт ь повторен ы
в будущем. Для повтор а последне й отмененно й команд ы мы вызывае м операци ю
Execut e для последне й команд ы справ а от лини и «настоящее»:
Execute()
Настоящее
Зате м мы сдвигае м лини ю «настоящее » так, чтоб ы следующи й повто р вызва л
операци ю Execut e для следующе й команд ы «в област и будущего».
Проектировани е редактор а документо в
•^— Прошлое Будущее —*-
Настоящее
Разумеется, если следующа я операция - это не повтор, а отмена, то команда
слева от линии «настоящее » будет отменена. Таким образом, пользовател ь может
перемещатьс я в обоих направлениях, чтобы исправит ь ошибки.
Паттерн команда
Команды Lexi - это пример применени я паттерна команда, описывающего,
как инкапсулироват ь запрос. Этот паттерн предписывае т единообразный интер-
фейс для выдачи запросов, с помощью которого можно сконфигурироват ь клиен-
ты для обработки разных запросов. Интерфейс изолируе т клиента от реализации
запроса. Команда может полностью или частично делегироват ь реализацию за-
проса другим объектам либо выполнят ь данную операцию самостоятельно. Это
идеальное решение для приложени й типа Lexi, которые должны предоставлят ь
централизованны й доступ к функциональности, разбросанно й по разным частям
программы. Данный паттерн предлагае т также механизмы отмены и повтора, над-
строенные над базовым интерфейсом класса Command.
2.S. Проверк а правописани я
и расстановк а переносо в
Наша последняя задача связана с анализом текста, точнее, с проверкой право-
писания и нахождение м мест, где можно поставит ь перенос для улучшения фор-
матирования.
Ограничени я здесь аналогичны тем, о которых уже говорилос ь при обсужде-
нии форматировани я в разделе 2.3. Как и в случае с разбиение м на строки, есть
много способов реализоват ь поиск орфографически х ошибок и вычисление точек
переноса. Поэтому и здесь планировалас ь поддержк а нескольки х алгоритмов.
Пользователь сможет выбрать тот алгоритм, который его больше устраивае т по
соотношени ю потребляемо й памяти, скорости и качеству. Добавление новых ал-
горитмов тоже должно реализовыватьс я просто.
Также мы хотим избежать жесткой привязки этой информаци и к структуре
документа. В данном случае такая цель даже более важна, чем при форматирова -
нии, поскольку проверка правописани я и расстановка переносов - это лишь два
вида анализа текста, которые Lexi мог бы поддерживать. Со временем мы собира-
емся расширит ь аналитически е возможност и Lexi. Мы могли бы добавить поиск,
подсчет слов, средства вычислени й для суммировани я значений в таблице, про-
верку грамматик и и т.д. Но мы не хотим изменят ь класс Glyph и все его подклас-
сы при каждом добавлении такого рода функциональности.
У этой задачи есть две стороны: доступ к анализируемо й информации, кото-
рая разбросана по разным глифам в структуре документ а и собственно выполне -
ние анализа. Рассмотрим их по отдельности.
Доступ к распределенной информации
Для многи х видо в анализ а необходим о рассматриват ь текс т посимвольно. Но
анализируемы й текс т рассея н по иерархи и структур, состоящи х из объектов-гли -
фов. Чтоб ы исследоват ь текст, представленны й в тако м виде, нуже н механиз м
доступа, знающи й о структура х данных, в которы х хранитс я текст. Для некото -
рых глифо в потомк и могу т хранитьс я в связанны х списках, для други х - в масси -
вах, а для третьи х и вовс е используютс я какие-т о экзотически е структуры. На ш
механиз м доступ а долже н справлятьс я со всем этим.
К сожалению, для разны х видо в анализ а метод ы доступ а к информаци и могу т
различаться. Обычн о текс т сканируетс я от начал а к концу. Но иногд а требуетс я
сделат ь прям о противоположное. Например, для реверсивног о поиск а нужн о про -
ходит ь по текст у в обратном, а не в прямо м направлении. А при вычислени и ал-
гебраически х выражени й необходи м внутренни й порядо к обхода.
Итак, на ш механиз м доступ а долже н умет ь приспосабливатьс я к разны м
структура м данны х и поддерживат ь разны е способ ы обхода.
Инкапсуляция доступа и порядка обхода
Пока что в наше м интерфейс е глифо в для доступ а к потомка м со сторон ы кли -
енто в используетс я целочисленны й индекс. Хот я для тех классо в глифов, кото -
рые содержа т потомко в в массиве, это, може т быть, и разумно, но совершенн о не-
эффективн о для глифов, пользующихс я связанны м списком. Рол ь абстракци и
глифо в в том, чтоб ы скрыт ь структур у данных, в которо й содержатс я потомки.
Тогда мы сможе м изменит ь данну ю структуру, не затрагива я други е классы.
Поэтом у тольк о глифу разрешен о знать, каку ю структур у он использует. От-
сюда следует, что интерфей с глифо в не долже н отдават ь предпочтени е какой-т о
одно й структур е данных. Например, не следуе т оптимизироват ь его в польз у мас -
сивов, а не связанны х списков, как это делалос ь до сих пор.
Мы може м решит ь проблем у и одновременн о поддержат ь нескольк о разны х
способо в обхода. Разумн о включит ь возможност и множественног о доступ а и об-
хода прям о в класс ы глифо в и предоставит ь спосо б выбират ь межд у ними, возмож -
но, передава я константу, являющуюс я элементо м некоторог о перечисления. Вы-
полня я обход, класс ы передаю т этот парамет р дру г другу, чтоб ы гарантировать, что
все они обходя т структур у в одно м и том же порядке. Так же должн а передаватьс я
люба я информация, собранна я во врем я обхода.
Для поддержк и данног о подход а мы могл и бы добавит ь в интерфей с класс а
Glyp h следующи е абстрактны е операции:
void First(Traversal ki nd)
void Next ()
bool IsDone()
Glyph* GetCurrent()
void Insert(Gl yph*)
Операци и First, Next и IsDone управляю т обходом. First производи т ини-
циализацию. В качеств е параметр а передаетс я вид обход а в вид е перечисляемо й
констант ы тип а Traversal, котора я може т принимат ь таки е значения, ка к
Правописание и расстановка переносов
Проектирование редактора документов
CHILDREN (обходить только прямых потомков глифа), PREORDER (обходить всю
структуру в прямом порядке), POSTORDER (в обратном порядке) или INORDER (во
внутреннем порядке). Next переходит к следующему глифу в порядке обхода,
a IsDone сообщает, закончился ли обход. Get Current заменяет операцию
Child - осуществляет доступ к текущему в данном обходе глифу. Старая опера-
ция Insert переписывается, теперь она вставляет глиф в текущую позицию.
При анализе можно было бы использовать следующий код на C++ для обхода
структуры глифов с корнем в g в прямом порядке:
Glyph* g;
for (g->First(PREORDER); !g->IsDone(); g->Next() ) {
Glyph * curren t = g->GetCurrent();
// выполнит ь анали з
}
Обратите внимание, что мы исключили целочисленный индекс из интерфей-
са глифов. Не осталось ничего, что предполагало бы какой-то предпочтительный
контейнер. Мы также уберегли клиенты от необходимости самостоятельно реа-
лизовывать типичные виды доступа.
Но этот подход еще не идеален. Во-первых, здесь не поддерживаются новые
виды обхода, не расширяется множество значений перечисления и не добавляются
новые операции. Предположим, что нам нужен вариант прямого обхода, при кото-
ром автоматически пропускаются нетекстовые глифы. Тогда пришлось бы изменить
перечисление Traversal, включив в него значение TEXTUAL_PREORDER.
Но нежелательно менять уже имеющиеся объявления. Помещение всего ме-
ханизма обхода в иерархию класса Glyph затрудняет модификацию и расшире-
ние без изменения многих других классов. Механизм также трудно использовать
повторно для обхода других видов структур. И еще нельзя иметь более одного
активного обхода над данной структурой.
Как уже не раз бывало, наилучшее решение - инкапсулировать изменяющую-
ся сущность в класс. В данном случае это механизмы доступа и обхода. Допусти-
мо ввести класс объектов, называемых итераторами, единственное назначение
которых - определить разные наборы таких механизмов. Можно также восполь-
зоваться наследованием для унификации доступа к разным структурам данных
и поддержки новых видов обхода. Тогда не придется изменять интерфейсы гли-
фов или трогать реализации существующих глифов.
Класс Iterator и его подклассы
Мы применим абстрактный класс Iterator для определения общего интерфей-
са доступа и обхода. Конкретные подклассы вроде Arraylterator и List Iterator
реализуют данный интерфейс для предоставления доступа к массивам и спис-
кам, а такие подклассы, как Preorderlterator, Postorderlterator и им по-
добные, реализуют разные виды обходов структур. Каждый подкласс класса
Iterator содержит ссылку на структуру, которую он обходит. Экземпляры под-
класса инициализируются этой ссылкой при создании. На рис. 2.13 показан класс
Правописание и расстановка переносов
Рис. 2. 13. Класс Iterator и его подклассы
Iterator и несколько его подклассов. Обратите внимание, что мы добавили
в интерфейс класса Glyph абстрактную операцию Createlterator для поддерж-
ки итераторов.
Интерфейс итератора предоставляе т операции First, Next и IsDone для
управления обходом. В классе Listlterator реализация операции First ука-
зывает на первый элемент списка, a Next перемещае т итератор на следующий эле-
мент. Операция IsDone возвращае т признак, говорящий о том, перешел ли ука-
затель за последний элемент списка. Операция Cur rent It em разыменовывае т
итератор для возврата глифа, на который он указывает. Класс Array Iterator
делает то же самое с массивами глифов.
Теперь мы можем получить доступ к потомкам в структуре глифа, не зная ее
представления:
Glyph * g;
Iterator<Glyph*> * i = g->CreateIterator();
for (i->First(); !i->IsDone(); i->Next() ) {
Glyph * chil d = i->Current!tem();
// выполнит ь действи е с потомко м
}
Createl terator по умолчанию возвращае т экземпля р Nul l l t erat or.
Nulllterator - это вырожденный итератор для глифов, у которых нет потом-
ков, то есть листовых глифов. Операция IsDone для Nulllterato r всегда воз-
вращает истину.
Подклас с глифа, имеющег о потомков, замещае т операцию Createlterator
так, что она возвращае т экземпля р другого подкласс а класса Iterator. Какого
именно - зависит от структуры, в которой содержатс я потомки. Если подклас с Row
класса Glyph размещае т потомков в списке, то его операция Createlterator бу-
дет выглядет ь примерн о так:
Iterator<Glyph*>* Row::Createl terator () {
return new ListIterator<Glyph*>(_children);
}
Для обхода в прямом и внутренне м порядк е итератор ы реализуют алгорит м
обхода в терминах, определенны х для конкретны х глифов. В обоих случаях ите-
ратору передаетс я корнево й глиф той структуры, котору ю нужно обойти. Итера-
торы вызывают Createlterator для каждог о глифа в этой структур е и сохра-
няют возвращенны е итератор ы в стеке.
Например, класс Preorderlterator получае т итератор от корневог о глифа,
инициализируе т его так, чтобы он указыва л на свой первый элемент, а затем по-
мещает в стек:
void Preorderl terator::Fi rst () {
Iterator<Glyph*>* i = _root->Create!terator();
if (i) {
i ->Fi r s t ( );
_iterators.RemoveAll();
_iterators.Push(i);
}
}
Cur rent I tern должна будет просто вызват ь операцию Cur rent It em для ите-
ратора на вершине стека:
Glyph* Preorderlterator::CurrentItem () const {
return
_i t erat ors.Si ze() > 0 ?
_iterators.Top()->CurrentItem( ) : 0;
}
Операция Next обращаетс я к итератор у с вершины стека с тем, чтобы элемент,
на который он указывает, создал свой итератор, спускаяс ь тем самым по структу -
ре глифов как можно ниже (это ведь прямой порядок, не так ли?). Next устанав -
ливает новый итерато р так, чтобы он указыва л на первый элемент в порядк е об-
хода, и помещае т его в стек. Затем Next проверяе т последни й встретившийс я
итератор; если его операция IsDone возвращае т true, то обход текущег о подде-
рева (или листа) закончен. В таком случае Next снимае т итератор с вершины сте-
ка и повторяе т всю последовательност ь действий, пока не найдет следующе е не
полность ю обойденно е дерево, если таковое существует. Если же необойденны х
деревьев больше нет, то мы закончил и обход:
Проектировани е редактора документо в
Iterator<Glyph*> * Row::CreateIterato r () {
return new ListIterator<Glyph*>(_children);
}
Правописани е и расстановк а переносо в
voi d Preorderlterator::Nex t ( ) {
Iterator<Glyph*> * i =
_iterators.Top()->CurrentItem()->CreateIterator();
i->First();
„iterators.Push(i ) ;
whil e (
_iterators.Size( ) > 0 & & „iterators.Top()->IsDone( )
) {
delet e „iterators.Pop();
„iterators.Top()->Next();
}
}
Обратит е внимание, что клас с Iterator позволяе т вводит ь новые вид ы обхо -
дов, не изменя я класс ы глифов, - мы прост о порождае м новый подклас с и добавля -
ем новый обхо д так, как проделал и это для Preorderlterator. Подкласс ы класс а
Glyp h пользуютс я тем же самы м интерфейсом, чтоб ы предоставит ь клиента м до-
ступ к свои м потомкам, не раскрыва я внутренне й структур ы данных, в которо й они
хранятся. Поскольк у итератор ы сохраняю т собственну ю копи ю состояни я обхода,
то одновременн о можн о имет ь нескольк о активны х итераторо в для одно й и той же
структуры. И, хот я в наше м пример е мы занималис ь обходо м структу р глифов, нич -
то не мешае т параметризоват ь клас с типа Preorderlterator типо м объекта
структуры. В C++ мы воспользовалис ь бы для этог о шаблонами. Тогд а описанны й
механиз м итераторо в можн о был о бы применит ь для обход а други х структур.
Паттерн итератор
Паттер н итерато р абстрагируе т описанну ю техник у поддержк и обход а струк -
тур, состоящи х из объектов, и доступ а к их элементам. Он примени м не тольк о
к составны м структурам, но и к группам, абстрагируе т алгорит м обход а и экрани -
рует клиенто в от детале й внутренне й структур ы объектов, которы е они обходят.
Паттер н итерато р - это еще один приме р того, как инкапсуляци я изменяющейс я
сущност и помогае т достич ь гибкост и и повторно й используемости. Но все равн о
проблем а итераци и оказываетс я глубокой, поэтом у паттер н итерато р горазд о
сложней, чем был о рассмотрен о выше.
Обход и действия, выполняемые при обходе
Итак, теперь, когд а у нас есть спосо б обойт и структур у глифов, нужн о занять -
ся проверко й правописани я и расстановко й переносов. Для обои х видо в анализ а
необходим о аккумулироват ь собранну ю во врем я обход а информацию.
Прежд е всег о следуе т решить, на каку ю част ь программ ы возложит ь ответ -
ственност ь за выполнени е анализа. Можн о было бы поручит ь это класса м Iterator,
тем самы м сдела в анали з неотъемлемо й часть ю обхода. Но решени е стал о бы бо-
лее гибки м и пригодны м для повторног о использования, если бы обхо д был отде -
лен от действий, которы е при этом выполняются. Дело в том, что для одног о и того
же вид а обход а могу т выполнятьс я разны е вид ы анализа. Поэтом у один и тот же
Проектировани е редактор а документо в
набор итераторов можно было бы использовать для разных аналитических опе-
раций. Например, прямой порядок обхода применяется в разных случаях, вклю-
чая проверку правописания, расстановку переносов, поиск в прямом направле-
нии и подсчет слов.
Итак, анализ и обход следует разделить. Кому еще можно поручить анализ?
Мы знаем, что разновидностей анализа достаточно много, и в каждом случае в те
или иные моменты обхода будут выполняться различные действия. В зависимос-
ти от вида анализа некоторые глифы могут оказаться более важными, чем другие.
При проверке правописания и расстановке переносов мы хотели бы рассматри-
вать только символьные глифы и пропускать графические - линии, растровые
изображения и т.д. Если мы занимаемся разделением цветов, то желательно было
бы принимать во внимание только видимые, но никак не невидимые глифы. Та-
ким образом, разные глифы должны просматриваться разными видами анализа.
Поэтому данный вид анализа должен уметь различать глифы по их типу. Оче-
видное решение - встроить аналитические возможности в сами классы глифов.
Тогда для каждого вида анализа мы можно было добавить одну или несколько аб-
страктных операций в класс Glyph и реализовать их в подклассах в соответствии
с той ролью, которую они играют при анализе.
Однако неприятная особенность такого подхода состоит в том, что придется
изменять каждый класс глифов при добавлении нового вида анализа. В некото-
рых случаях проблему удается сгладить: если в анализе участвует немного классов
или если большинство из них выполняют анализ одним и тем же способом, то мож-
но поместить подразумеваемую реализацию абстрактной операции прямо в класс
Glyph. Такая операция по умолчанию будет обрабатывать наиболее распростра-
ненный случай. Тогда мы смогли бы ограничиться только изменениями класса
Glyph и тех его подклассов, которые отклоняются от нормы.
Но, несмотря на то что реализация по умолчанию сокращает объем измене-
ний, принципиальная проблема остается: интерфейс класса Glyph необходимо
расширять при добавлении каждого нового вида анализа. Со временем такие опе-
рации затемнят смысл этого интерфейса. Будет трудно понять, что основная цель
глифа - определить и структурировать объекты, имеющие внешнее представле-
ние и форму; в интерфейсе появится много лишних деталей.
Инкапсуляция анализа
Судя по всему, стоит инкапсулиро-
вать анализ в отдельный объект, как мы
уже много раз делали прежде. Можно
было бы поместить механизм конкрет-
ного вида анализа в его собственный
класс, а экземпляр этого класса исполь-
зовать совместно с подходящим итерато-
ром. Тогда итератор «переносил» бы этот
экземпляр от одного глифа к другому,
а объект выполнял бы свой анализ для
каждого элемента. По мере продвижения
Правописани е и расстановк а переносов
обхода анализато р накаплива л бы определенну ю информаци ю (в данном случае -
символы).
Принципиальны й вопрос при таком подходе - как объект-анализато р разли-
чает виды глифов, не прибега я к проверк е или приведения м типов? Мы не хотим,
чтобы класс SpellingChecker включал такой псевдокод:
voi d SpellingChecker::Chec k (Glyph * glyph ) {
Character * c;
Row * r;
Image * i;
if (c = dynamic_cast<Character*>(glyph) ) {
// анализироват ь симво л
} else i f ( r = dynamic_cast<Row*>(glyph) ) {
// анализироват ь потомк и г
} else if (i = dynamic_cast<Image*>(glyph) ) {
// ничег о не делат ь
}
}
Такой код опираетс я на специфически е возможност и безопасных по отноше -
нию к типам приведений. Его трудно расширять. Нужно не забыть изменит ь тело
данной функци и после любого изменени я иерархии класса Glyph. В общем это
как раз такой код, необходимост ь в котором хотелос ь бы устранить.
Как уйти от данног о грубог о подхода? Посмотрим, что произойдет, если мы
добавим в класс Glyph такую абстрактну ю операцию:
void CheckMe (Spel l ingChecker& )
Определим операцию CheckMe в каждом подкласс е класса Glyph следующи м
образом:
/
void GlyphSubc l ass :: CheckMe (SpellingChecker & checker ) {
checker. CheckGlyphSubclas s (this ) ;
}
где GlyphSubclas s заменяетс я именем подкласс а глифа. Заметим, что при вызове
CheckMe конкретны й подклас с класса Glyph известен, ведь мы же выполняе м одну
из его операций. В свою очередь, в интерфейс е класса Spell ingChecker есть опе-
рация типа CheckGlyphSubclas s для каждог о подкласс а класса Glyph1:
clas s SpellingChecke r {
publi c :
SpellingChecke r ( ) ;
1 Мы могли бы воспользоватьс я перегрузко й функций, чтобы присвоит ь этим функциям-члена м оди-
наковые имена, поскольк у их можно различит ь по типам параметров. Здесь мы дали им разные име-
на, чтобы было видно, что это все-таки разные функции, особенно при их вызове.
Проектирование редактора документов
virtual void CheckCharacter(Character*);
virtual void CheckRow(Row*);
virtual void Checklmage(Image*);
// ... и так далее
List<char*>& GetMisspellings();
protected:
virtual bool IsMisspelled(const char*);
private:
char _currentWord[MAX_WORD_SIZE];
List<char*> _misspellings;
};
Операция проверки в классе SpellingChecker для глифов типа Character
могла бы выглядеть так:
void SpellingChecker::CheckCharacter (Character* с) {
const char ch = c->GetCharCode();
if (isalpha(ch)) {
// добавить букву к _currentWord
} else {
// встретилась не-буква
if (IsMisspelled(_currentWord)) {
// добавить _currentWord в „misspellings
_misspellings.Append(strdup(_currentWord));
}
_currentWord[0] = '\б';
// переустановить _currentWord для проверки
// следующего слова
}
}
Обратите внимание, что мы определили специальную операцию Get Char Code
только для класса Character. Объект проверки правописания может работать со
специфическими для подклассов операциями, не прибегая к проверке или приве-
дению типов, а это позволяет нам трактовать некоторые объекты специальным
образом.
Объект класса CheckCharacter накапливает буквы в буфере _currentWord.
Когда встречается не-буква, например символ подчеркивания, этот объект вызы-
вает операци ю IsMi sspel l ed для проверк и орфографии слова, находящегося
в _currentWord.1 Если слово написано неправильно, то CheckCharacter добав-
ляет его в список слов с ошибками. Затем буфер _currentWord очищается для
приема следующего слова. По завершении обхода можно добраться до списка слов
с ошибками с помощью операции GetMis spell ings.
Теперь логично обойти всю структуру глифов, вызывая CheckMe для каждого
глифа и передавая ей объект проверки правописания в качестве аргумента. Тем
самым текущий глиф для Spell ingChecker идентифицируется и может продол-
жать проверку:
SpelIingChecker spel1ingChecker;
Composition* с;
// ...
Glyph * g;
Preorderlterato r i(c);
for (i.Firs t (); !i.IsDone(); i.NextO ) {
g = i.CurrentItern();
g->CheckMe(spellingChecker ) ;
}
На следующей диаграмме показано, как взаимодействуют глифы типа Character
и объект SpellingChecker.
|
Этот подход работает при поиске орфографических ошибок, но как он может
помочь в поддержке нескольких видов анализа? Похоже, что придется добавлять
операцию вроде CheckMe (SpellingChecker&) в класс Glyph и его подклассы
1 Функция IsMisspel led реализует алгоритм проверки орфографии, детали которого мы здесь не
приводим, поскольк у мы сделал и его независимы м от дизайн а Lexi. Мы може м поддержат ь разны е
алгоритмы, порожда я подкласс ы класс а SpellingChecker. Или применит ь для этой цели паттер н
стратеги я (как для форматировани я в раздел е 2.3).
Правописание и расстановка переносов
Проектировани е редактор а документо в
всякий раз, как вводится новый вид анализа. Так оно и есть, если мы настаивае м
на независимо м классе для каждого вида анализа. Но почему бы не придать всем
видам анализа одинаковый интерфейс? Это позволит нам использоват ь их поли-
морфно. И тогда мы сможем заменить специфически е для конкретног о вида ана-
лиза операции вроде CheckMe (SpellingCheckerk ) одной инвариантно й опера-
цией, принимающе й более общий параметр.
Класс Visitor и его подклассы
Мы будем использоват ь термин «посетитель » для обозначения класса объек-
тов, «посещающих » другие объекты во время обхода, дабы сделать то, что необхо-
димо в данном контексте.1 Тогда мы можем определить класс Visitor, описыва-
ющий абстрактный интерфейс для посещения глифов в структуре:
clas s Visito r {
public:
virtua l voi d VisitCharacter(Character* ) { }
virtua l voi d VisitRow(Row* ) { }
virtua l voi d Visitlmage(Image* ) { }
// ... и так дале е
};
Конкретные подклассы Visitor выполняют разные виды анализа. Например,
можно было определить подкласс SpellingCheckingVisitor для проверки
правописания и подкласс Hyphenat ionVisitor для расстановки переносов. При
этом SpellingCheckingVisitor был бы реализован точно так же, как мы реа-
лизовали класс SpellingChecke r выше, только имена операций отражали бы
более общий интерфейс класса Visitor. Так, операция CheckCharacter назы-
валась бы VisitCharacter.
Поскольку имя CheckMe не подходит для посетителей, которые ничего не
проверяют, мы использовал и бы имя Accept. Аргумент этой операции тоже при-
шлось бы изменить на Vi si tor&, чтобы отразить тот факт, что может принимать -
ся любой посетитель. Теперь для добавления нового вида анализа нужно лишь
определит ь новый подкласс класса Visitor, а трогать классы глифов вовсе не
обязательно. Мы поддержал и все возможные в будущем виды анализа, добавив
лишь одну операцию в класс Glyph и его подклассы.
О выполнении проверки правописания говорилось выше. Такой же подход бу-
дет применен для аккумулирования текста в подклассе Hyphenat ionVisitor. Но
после того как операция VisitCharacter из подкласса Hyphenat ionVisitor
закончила распознавание целого слова, она ведет себя по-другому. Вместо провер-
ки орфографии применяетс я алгоритм расстановки переносов, чтобы определить,
в каких местах можно перенести слово на другую строку (если это вообще возмож-
но). Затем для каждой из найденных точек в структуру вставляется разделяющий
«Посетить» - это лишь немногим более общее слово, чем «проанализировать». Оно просто предвос-
хищает ту терминологию, которой мы будем пользоватьс я при обсуждении следующег о паттерна.
Правописани е и расстановк а переносо в
(discretionary) глиф. Разделяющие глифы являются экземплярами подкласса
Glyph - класса Discretionary.
Разделяющий глиф может выглядеть по-разному в зависимости от того, явля-
ется он последним символом в строке или нет. Если это последний символ, глиф
выглядит как дефис, в противном случае не отображается вообще. Разделяющий
глиф запрашивает у своего родителя (объекта Row), является ли он последним
потомком, и делает это всякий раз, когда от него требуют отобразить себя или
вычислить свои размеры. Стратегия форматирования трактует разделяющие гли-
фы точно так же, как пропуски, считая их «кандидатами» на завершающий сим-
вол строки. На диаграмме ниже показано, как может выглядеть встроенный раз-
делитель.
Паттерн посетитель
Вышеописанна я процедура - пример применения паттерна посетитель. Его
главными участниками являются класс Vi si t or и его подклассы. Паттерн посе-
титель абстрагирует метод, позволяющий иметь заранее неопределенное число
видов анализа структур глифов без изменения самих классов глифов. Еще одна
полезная особенность посетителей состоит в том, что их можно применять не
только к таким агрегатам, как наши структуры глифов, но и к любым структурам,
состоящим из объектов. Сюда входят множества, списки и даже направленные
ациклические графы. Более того, классы, которые обходит посетитель, необяза-
тельно должны быть связаны друг с другом через общий родительский класс.
А это значит, что посетители могут пересекать границы иерархий классов.
Важный вопрос, который надо задать себе перед применением паттерна посе-
титель, звучит так: «Какие иерархии классов наиболее часто будут изменяться?»
Этот паттерн особенно удобен, если необходимо выполнять действия над объек-
тами, принадлежащими классу со стабильной структурой. Добавление нового
вида посетителя не требует изменять структуру класса, что особенно важно, когда
класс большой. Но при каждом добавлении нового подкласса вы будете вынужде-
ны обновить все интерфейсы посетителя с целью включить операцию Vi s i t. . .
для этого подкласса. В нашем примере это означает, что добавление подкласса Foo
класса Glyph потребует изменить класс Vi si t or и все его подклассы, чтобы до-
бавить операцию Vi s it Foo. Однако при наших проектных условиях гораздо бо-
лее вероятно добавление к Lexi нового вида анализа, а не нового вида глифов.
Поэтому для наших целей паттерн посетитель вполне подходит.
Проектировани е редактор а документо в
2.9. Резюм е
При проектировани и Lex i мы применил и восем ь различны х паттернов:
а компоновщи к для представлени я физическо й структур ы документа;
а стратеги я для возможност и использовани я различны х алгоритмо в форма -
тирования;
а декоратор для оформления пользовательского интерфейса;
а абстрактна я фабрик а для поддержк и нескольки х стандарто в внешнег о об-
лика;
а мос т для поддержк и нескольки х оконны х систем;
а команд а для реализаци и отмен ы и повтор а операци й пользователя;
а итерато р для обход а структу р объектов;
а посетител ь для поддержк и неизвестног о заране е числ а видо в анализ а без
усложнени я реализаци и структур ы документа.
Ни одно из этих проектны х решени й не ограничен о документо-ориентиро -
ванным и редакторам и врод е Lexi. На само м деле в большинств е нетривиальны х
приложени й ест ь возможност ь воспользоватьс я многим и из эти х паттернов,
быт ь может, для разны х целей. В приложени и для финансовог о анализ а паттер н
компоновщи к можн о был о бы применит ь для определени я инвестиционны х
портфелей, разбитых на субпортфел и и счета разных видов. Компилято р мог бы
использоват ь паттер н стратегия, чтоб ы поддержат ь реализаци ю разны х схе м
распределени я машинны х регистро в для целевы х компьютеро в с различно й архи -
тектурой. Приложени я с графически м интерфейсо м пользовател я вполн е могл и бы
применит ь паттерн ы декорато р и команд а точн о так же, как это сделал и мы.
Хот я мы и рассмотрел и нескольк о крупны х пробле м проектировани я Lexi, но
осталос ь горазд о больш е таких, которы х мы не касались. Но ведь и в книг е описа -
ны не тольк о рассмотренны е восем ь паттернов. Поэтому, изуча я остальны е пат-
терны, подумайт е о том, как вы могл и бы применит ь их к Lexi. А еще лучше поду -
майт е об их использовани и в свои х собственны х проектах!
Глава 3. Порождающи е паттерн ы
Порождающи е паттерн ы проектировани я абстрагирую т процес с инстанцирова -
ния. Они помогу т сделат ь систем у независимо й от способ а создания, композици и
и представлени я объектов. Паттерн, порождающи й классы, используе т наследо -
вание, чтобы варьироват ь инстанцируемы й класс, а паттерн, порождающи й объек -
ты, делегируе т инстанцировани е другом у объекту.
Эти паттерн ы оказываютс я важны, когда систем а больше зависи т от компози -
ции объектов, чем от наследовани я классов. Получаетс я так, что основно й упор
делаетс я не на жестко м кодировани и фиксированног о набор а поведений, а на опре-
делени и небольшог о набор а фундаментальны х поведений, с помощь ю компози -
ции которы х можн о получат ь любое числ о боле е сложных. Таким образом, для
создани я объекто в с конкретны м поведение м требуетс я нечт о большее, чем прос -
тое инстанцировани е класса.
Для порождающи х паттерно в актуальн ы две темы. Во-первых, эти паттерн ы
инкапсулирую т знани я о конкретны х классах, которы е применяютс я в системе.
Во-вторых, скрываю т детал и того, как эти класс ы создаютс я и стыкуются. Един-
ственна я информаци я об объектах, известна я системе, - это их интерфейсы, опре-
деленны е с помощь ю абстрактны х классов. Следовательно, порождающи е паттер -
ны обеспечиваю т большу ю гибкост ь при решени и вопрос а о том, что создается,
кто это создает, как и когда. Можн о собрат ь систем у из «готовых » объекто в с са-
мой различно й структуро й и функциональность ю статическ и (на этапе компиля -
ции) или динамическ и (во время выполнения).
Иногд а допустим о выбират ь межд у тем или иным порождающи м паттерном.
Например, есть случаи, когд а с пользо й для дела можно использоват ь как прото -
тип, так и абстрактну ю фабрику. В други х ситуация х порождающи е паттерн ы
дополняю т друг друга. Так, применя я строитель, можн о использоват ь други е пат-
терны для решени я вопрос а о том, какие компонент ы нужн о строить, а прототи п
часто реализуетс я вмест е с одиночкой.
Поскольк у порождающи е паттерн ы тесно связан ы друг с другом, мы изучи м
:разу все пять, чтобы лучше были видны их сходств а и различия. Изучени е будет
нестис ь на обще м пример е - построени и лабиринт а для компьютерно й игры.
Правда, и лабиринт, и игра буду т слегка варьироватьс я для разных паттернов.
Иногд а цель ю игры стане т прост о отыскани е выход а из лабиринта; тогда у игрок а
дет лишь один локальны й вид помещения. В други х случая х в лабиринта х мо-
гут встречатьс я задачки, которы е игрок долже н решить, и опасности, которы е ему
дстоит преодолеть. В подобны х играх може т отображатьс я карт а того участк а
:абиринта, которы й уже был исследован.
Порождающи е паттерны
Мы опустим детали того, что может встречаться в лабиринте и сколько игро-
ков принимают участие в забаве, а сосредоточимся лишь на принципах создания
лабиринта. Лабиринт мы определим как множество комнат. Любая комната «зна-
ет» о своих соседях, в качестве которых могут выступать другая комната, стена
или дверь в другую комнату.
Классы Room (комната), Door (дверь) и Wal l (стена) определяют компонен-
ты лабиринта и используются во всех наших примерах. Мы определим только те
части этих классов, которые важны для создания лабиринта. Не будем рассматри-
вать игроков, операции отображения и блуждания в лабиринте и другие важные
функции, не имеющие отношения к построению нашей структуры.
На диаграмме ниже показаны отношения между классами Room, Door и Wai 1.
У каждой комнаты есть четыре стороны. Для задания северной, южной, вос-
точной и западной сторон будем использовать перечисление Direction в терми-
нологии языка C++:
enum Directio n {North, South, East, West };
В программах на языке Smalltalk для представления направлений воспользу-
емся соответствующими символами.
Класс MapSite - это общий абстрактный класс для всех компонентов лаби-
ринта. Определим в нем только одну операцию Enter. Когда вы входите в комна-
ту, ваше местоположение изменяется. При попытке затем войти в дверь может
произойти одно из двух. Если дверь открыта, то вы попадаете в следующую ком-
нату. Если же дверь закрыта, то вы разбиваете себе нос:
class MapSite {
public:
virtual void Enter () = 0;
};
Операция Enter составляет основу для более сложных игровых операций.
Например, если вы находитесь в комнате и говорите «Иду на восток», то игрой
определяется, какой объект класса Map Si te находится к востоку от вас, и для него
вызывается операция Enter. Определенные в подклассах операции Enter «выяс-
нят», изменили вы направление или расшибли нос. В реальной игре Enter могла
бы принимать в качестве аргумента объект, представляющий блуждающего игрока.
Порождающи е паттерн ы
Room - это конкретны й подклас с класса MapSite, который определяе т клю-
чевые отношени я между компонентам и лабиринта. Он содержит ссылки на дру-
гие объекты MapSite, а также хранит номер комнаты. Номерами идентифициру -
ются все комнат ы в лабиринте:
clas s Roo m : publi c MapSit e {
public:
Room(in t roomNo);
MapSite * GetSide(Direction ) const;
voi d SetSide(Direction, MapSite*);
virtua l voi d Enter();
private:
MapSite * _sides[4];
int _rbomNumber;
};
Следующие классы представляют стены и двери, находящиес я с каждой сто-
роны комнаты:
class Wal l : publi c MapSit e {
public:
Wall();
virtua l voi d Enter();
};
clas s Doo r : publi c MapSit e {
public:
Door(Room * = 0, Room * = 0);
virtua l voi d Enter();
Room * OtherSideFrom(Room*);
private:
Room * _rooml;
Room * _room2;
boo l _isOpen;
};
Но нам необходимо знать не только об отдельных частях лабиринта. Опреде-
лим еще класс Maze для представлени я набора комнат. В этом классе есть опера-
ция RoomNo для нахождени я комнат ы по ее номеру:
clas s Maz e {
public:
Maze();
voi d AddRoom(Room*);
Порождающи е паттерн ы
Room* RoomNo(int ) const;
private:
// ...
};
RoomN o могл а бы реализовыват ь свою задач у с помощь ю линейног о списка,
хэш-таблиц ы или даже простог о массива. Но пок а нас не интересую т таки е дета -
ли. Займемс я тем, как описат ь компонент ы объекта, представляющег о лабиринт.
Определи м такж е клас с MazeGame, которы й создае т лабиринт. Самы й прос -
той способ сделать это - строить лабиринт с помощью последовательност и опе-
раций, добавляющи х к нему компоненты, которы е пото м соединяются. Например,
следующа я функция-чле н создас т лабирин т из дву х комна т с одно й дверь ю меж-
ду ними:
Maze* MazeGame::CreateMaze () {
Maze* aMaze = new Maze;
Room* rl = new Room(l);
Room* r2 = new Room(2);
Door* theDoor = new Door(rl, r2);
aMaze->AddRoom(rl);
aMaze->AddRoom(r2);
rl->SetSide(North, new Wall);
rl->SetSide(East, theDoor);
rl->SetSide(South, new Wall);
rl->SetSide(West, new Wall);
r2->SetSide(North, new Wall);
r2->SetSide(East, new Wall);
r2->SetSide(South, new Wall);
r2->SetSide(West, theDoor);
return aMaze;
Довольн о сложна я функция, если принят ь во внимание, что она всег о лишь
создае т лабирин т из дву х комнат. Ест ь очевидны е способ ы упростит ь ее. Напри -
мер, конструкто р класс а Roo m мог бы инициализироват ь сторон ы без двере й за-
ранее. Но это означае т лишь перемещени е кода в друго е место. Сут ь проблем ы не
в размер е этой функции-члена, а в ее негибкости. В функци и жестк о «зашита »
структур а лабиринта. Чтоб ы изменит ь структуру, придетс я изменит ь саму функ -
цию, либо замести в ее (то есть полность ю переписа в заново), либ о непосредствен -
но модифицирова в ее фрагменты. Оба пут и чреват ы ошибкам и и не способству -
ют повторном у использованию.
Порождающи е паттерн ы показывают, как сделат ь дизай н боле е гибким, хот я
и необязательн о меньши м по размеру. В частности, их применени е позволи т лег -
ко менят ь классы, определяющи е компонент ы лабиринта.
Паттерн Abstrac t Factor y
Предположим, что мы хотим использоват ь уже существующу ю структур у
в новой игре с волшебным и лабиринтами. В такой игре появляютс я не существо -
вавшие ранее компоненты, наприме р DoorNeedingSpel l - заперта я дверь, для
открывани я которо й нужно произнест и заклинание, или Enchan t edRoo m - ком-
ната, где есть необычны е предметы, скажем, волшебны е ключ и или магически е
слова. Как легко изменит ь операци ю CreateMaze, чтобы она создавал а лабирин -
ты с новыми классами объектов?
Самое серьезно е препятстви е лежи т в жестк о зашито й в код информаци и
о том, какие класс ы инстанцируются. С помощь ю порождающи х паттерно в мож-
но различным и способам и избавитьс я от явных ссылок на конкретны е класс ы из
кода, выполняющег о их инстанцирование:
а если CreateMaz e вызывае т виртуальны е функци и вмест о конструкторо в
для создани я комнат, стен и дверей, то инстанцируемы е классы можно под-
менить, создав подклас с MazeGam e и переопредели в в нем виртуальны е
функции. Такой подход применяетс я в паттерн е фабричны й метод;
а когда функци и CreateMaz e в качеств е параметр а передаетс я объект, ис-
пользуемы й для создани я комнат, стен и дверей, то их классы можно изме-
нить, переда в другой параметр. Это приме р паттерн а абстрактна я фабрика;
а если функци и CreateMaz e передаетс я объект, способны й целико м создат ь
новый лабирин т с помощь ю своих операци й для добавлени я комнат, дверей
и стен, можно воспользоватьс я наследование м для изменени я часте й лаби-
ринта или способа его построения. Такой подхо д применяетс я в паттерн е
строитель;
а если CreateMaz e параметризован а прототипам и комнаты, двери и стены,
которые она затем копируе т и добавляе т к лабиринту, то состав лабиринт а
можно варьировать, заменя я одни объекты-прототип ы другими. Это пат-
терн прототип.
Последни й из порождающи х паттернов, одиночка, може т гарантироват ь на-
личие единственног о лабиринт а в игре и свободны й доступ к нему со стороны всех
игровых объектов, не прибега я к глобальны м переменны м или функциям. Оди-
ночк а также позволяе т легко расширит ь или заменит ь лабиринт, не трогая суще-
гтвующи й код.
Паттер н Abstrac t Factor y
Название и классификация паттерна
Абстрактна я фабрик а - паттерн, порождающи й объекты.
Назначение
Предоставляе т интерфей с для создани я семейст в взаимосвязанны х или взаи-
мозависимы х объектов, не специфициру я их конкретны х классов.
Известен также под именем
Kit (инструментарий).
Порождающи е паттерны
Мотивация
Рассмотри м инструментальну ю программ у для создания пользовательског о
интерфейса, поддерживающег о разные стандарт ы внешнег о облика, наприме р
Moti f и Presentatio n Manager. Внешни й облик определяе т визуально е представле -
ние и поведени е элементо в пользовательског о интерфейс а («виджетов» ) - полос
прокрутки, окон и кнопок. Чтобы приложени е можно было перенест и на другой
стандарт, в нем не долже н быть жестко закодирова н внешни й облик виджетов.
Если инстанцировани е классов для конкретног о внешнег о облика разбросан о по
всему приложению, то изменит ь облик впоследстви и будет нелегко.
Мы можем решить эту проблему, определив абстрактный класс WidgetFa c tory,
в котором объявле н интерфей с для создания всех основных видов виджетов.
Есть также абстрактны е классы для каждог о отдельног о вида и конкретны е под-
классы, реализующи е виджет ы с определенны м внешни м обликом. В интерфейс е
WidgetFactor y имеетс я операция, возвращающа я новый объект-видже т для
каждог о абстрактног о класса виджетов. Клиент ы вызывают эти операци и для
получения экземпляро в виджетов, но при этом ничего не знают о том, какие имен-
но классы используют. Стало быть, клиент ы остаютс я независимым и от выбран-
ного стандарт а внешнег о облика.
Для каждог о стандарт а внешнег о облика существуе т определенны й подклас с
WidgetFactory. Каждый такой подкласс реализует операции, необходимые для со-
здания соответствующег о стандарт у виджета. Например, операция Great eScrollBar
в классе Mot if Widget Fac tory инстанцируе т и возвращае т полосу прокрутк и
в стандарт е Motif, тогда как соответствующа я операция в классе PMWidgetFactor y
возвращае т полосу прокрутк и в стандарт е Presentatio n Manager. Клиент ы созда-
ют виджеты, пользуяс ь исключительн о интерфейсо м WidgetFactory, и им ни-
чего не известно о классах, реализующи х виджет ы для конкретног о стандарта.
Другими словами, клиент ы должны лишь придерживатьс я интерфейса, опреде-
ленног о абстрактным, а не конкретны м классом.
Паттер н Abstrac t Factor y
Клас с Widge t Factor y также устанавливае т зависимости межд у конкретным и
классам и виджетов. Полос а прокрутк и для Moti f должн а использоватьс я с кнопко й
и текстовы м поле м Motif, и это ограничени е поддерживаетс я автоматически, как
следствие использования класса Mot ifWidgetFactory.
Применимость
Используйт е паттер н абстрактна я фабрика, когда:
Q систем а не должн а зависет ь от того, как создаются, компонуютс я и пред -
ставляютс я входящи е в нее объекты;
а входящи е в семейств о взаимосвязанны е объект ы должн ы использоватьс я
вмест е и вам необходим о обеспечит ь выполнени е этог о ограничения;
а систем а должн а конфигурироватьс я одни м из семейст в составляющи х ее
объектов;
а вы хотит е предоставит ь библиотек у объектов, раскрыва я тольк о их интер -
фейсы, но не реализацию.
Структура
Участники
a AbstractFactory (WidgetFactory) - абстрактная фабрика:
- объявляе т интерфей с для операций, создающи х абстрактны е объекты -
продукты;
a ConcreteFactory (Mot if WidgetFactory, PMWidgetFactory) - конкрет-
ная фабрика:
- реализуе т операции, создающи е конкретны е объекты-продукты;
a AbstractProduct (Window, ScrollBar) - абстрактны й продукт:
- объявляе т интерфей с для типа объекта-продукта;
Порождающие паттерны
a ConcreteProduct (Mot if Window, Mot if ScrollBar) - конкретны й продукт:
- определяе т объект-продукт, создаваемы й соответствующе й конкретно й
фабрикой;
- реализует интерфейс Abstract Product;
a Client - клиент:
- пользуетс я исключительн о интерфейсами, которы е объявлен ы в класса х
AbstractFactory и AbstractProduct.
Отношения
а Обычн о во врем я выполнени я создаетс я единственны й экземпля р класс а
ConcreteFactory. Эт а конкретна я фабрик а создае т объекты-продукты,
имеющи е вполн е определенну ю реализацию. Для создани я други х видо в
объекто в клиен т долже н воспользоватьс я друго й конкретно й фабрикой;
a AbstractFactory передоверяет создание объектов-продуктов своему под-
классу ConcreteFactory.
Результаты
Паттер н абстрактна я фабрик а обладае т следующим и плюсам и и минусами:
а изолирует конкретные классы. Помогае т контролироват ь класс ы объектов,
создаваемы х приложением. Поскольк у фабрик а инкапсулируе т ответствен -
ност ь за создани е классо в и сам процес с их создания, то она изолируе т
клиент а от детале й реализаци и классов. Клиент ы манипулирую т экземпля -
рами чере з их абстрактны е интерфейсы. Имен а изготавливаемы х классо в из-
вестн ы тольк о конкретно й фабрике, в код е клиент а они не упоминаются;
а упрощает замену семейств продуктов. Клас с конкретно й фабрик и появля -
ется в приложени и тольк о один раз: при инстанцировании. Это облегчае т за-
мену используемо й приложение м конкретно й фабрики. Приложени е може т
изменит ь конфигураци ю продуктов, прост о подстави в нову ю конкретну ю
фабрику. Поскольк у абстрактна я фабрик а создае т все семейств о продуктов,
то и заменяетс я сраз у все семейство. В наше м пример е пользовательског о
интерфейс а перейт и от виджето в Moti f к виджета м Presentatio n Manage r
можно, прост о переключившис ь на продукт ы соответствующе й фабрик и
и занов о созда в интерфейс;
а гарантирует сочетаемость продуктов. Есл и продукт ы некоторог о семей -
ства спроектирован ы для совместног о использования, то важно, чтоб ы при-
ложени е в кажды й момен т времен и работал о тольк о с продуктам и един -
ственног о семейства. Клас с AbstractFactor y позволяе т легк о соблюст и
это ограничение;
а поддержать новый вид продуктов трудно. Расширени е абстрактно й фабри -
ки для изготовлени я новы х видо в продукто в - непроста я задача. Интерфей с
AbstractFactor y фиксируе т набо р продуктов, которы е можн о создать.
Для поддержк и новы х продукто в необходим о расширит ь интерфей с фаб-
рики, то ест ь изменит ь клас с AbstractFactor y и все его подклассы. Ре-
шени е этой проблем ы мы обсуди м в раздел е «Реализация».
Паттер н Abstrac t Factor y
Реализация
Вот некоторые полезные приемы реализации паттерна абстрактная фабрика:
а фабрики как объекты, существующие в единственном экземпляре. Как пра-
вило, приложению нужен только один экземпляр класса ConcreteFactory
на каждое семейство продуктов. Поэтому для реализации лучше всего при-
менить паттерн одиночка;
а создание продуктов. Класс AbstractFactory объявляет только интерфейс
для создания продуктов. Фактическое их создание - дело подклассов
ConcreteProduct. Чаще всего для этой цели определяется фабричный
метод для каждого продукта (см. паттерн фабричный метод). Конкретная
фабрика специфицирует свои продукты путем замещения фабричного ме-
тода для каждого из них. Хотя такая реализация проста, она требует созда-
вать новый подкласс конкретной фабрики для каждого семейства продук-
тов, даже если они почти ничем не отличаются.
Если семейств продуктов может быть много, то конкретную фабрику удаст-
ся реализовать с помощью паттерна прототип. В этом случае она инициа-
лизируется экземпляром-прототипом каждого продукта в семействе и со-
здает новый продукт путем клонирования этого прототипа. Подход на основе
прототипов устраняет необходимость создавать новый класс конкретной
фабрики для каждого нового семейства продуктов.
Вот как можно реализовать фабрику на основе прототипов в языке Small-
talk. Конкретная фабрика хранит подлежащие клонированию прототипы
в словаре под названием partCatalog. Метод make: извлекает прототип
и клонирует его:
make: partNam e
^ (partCatalo g at: partName ) copy
У конкретной фабрики есть метод для добавления деталей в каталог:
addPart: partTemplate named: partName
partCatalog at: partName put: partTemplate
Прототипы добавляются к фабрике путем идентификации их символом:
aFactory addPart: aPrototype named: #ACMEWidge t
В языках, где сами классы являются настоящими объектами (например,
Smalltalk и Objective С), возможны некие вариации подхода на базе прото-
типов. В таких языках класс можно представлять себе как вырожденный
случай фабрики, умеющей создавать только один вид продуктов. Можно
хранить классы внутри конкретной фабрики, которая создает разные кон-
кретные продукты в переменных. Это очень похоже на прототипы. Такие
классы создают новые экземпляры от имени конкретной фабрики. Новая
фабрика инициализируется экземпляром конкретной фабрики с классами
продуктов, а не путем порождения подкласса. Подобный подход задейству-
ет некоторые специфические свойства языка, тогда как подход, основанный
на прототипах, от языка не зависит.
^ Порождающие паттерны
Как и для только что рассмотренной фабрики на базе прототипов в Smalltalk,
в версии на основе классов будет единственная переменная экземпляра
partCatalog, представляющая собой словарь, ключом которого является
название детали. Но вместо хранения подлежащих клонированию прототи-
пов partCatalog хранит классы продуктов. Метод make: выглядит теперь
следующим образом:
make: partNam e
4 (partCatalo g at: partName ) new
а определение расширяемых фабрик. Класс AbstractFactory обычно опре-
деляет разные операции для каждого вида изготавливаемых продуктов.
Виды продуктов кодируются в сигнатуре операции. Для добавления нового
вида продуктов нужно изменить интерфейс класса AbstractFactory
и всех зависящих от него классов.
Более гибкий, но не такой безопасный способ - добавить параметр к опера-
циям, создающим объекты. Данный параметр определяет вид создаваемого
объекта. Это может быть идентификатор класса, целое число, строка или
что-то еще, однозначно описывающее вид продукта. При таком подходе
классу AbstractFactory нужна только одна операция Make с параметром,
указывающим тип создаваемого объекта. Данный прием применялся в об-
суждавшихся выше абстрактных фабриках на основе прототипов и классов.
Такой вариант проще использовать в динамически типизированных языках
вроде Smalltalk, нежели в статически типизированных, каким является C++.
Воспользоваться им в C++ можно только, если у всех объектов имеется об-
щий абстрактный базовый класс или если объекты-продукты могут быть без-
опасно приведены к корректному типу клиентом, который их запросил.
В разделе «Реализация» из описания паттерна фабричный метод показа-
но, как реализовать такие параметризованные операции в C++.
Но даже если приведение типов не нужно, остается принципиальная про-
блема: все продукты возвращаются клиенту одним и тем же абстрактным
интерфейсом с уже определенным типом возвращаемого значения. Клиент
не может ни различить классы продуктов, ни сделать какие-нибудь предпо-
ложения о них. Если клиенту нужно выполнить операцию, зависящую от
подкласса, то она будет недоступна через абстрактный интерфейс. Хотя кли-
ент мог бы выполнить динамическое приведение типа (например, с помо-
щью оператора dynamic_cas t в C++), это небезопасно и необязательно за-
канчивается успешно. Здесь мы имеем классический пример компромисса
между высокой степенью гибкости и расширяемостью интерфейса.
Пример кода
Паттерн абстрактная фабрика мы применим к построению обсуждавшихся
в начале этой главы лабиринтов.
Класс Maze Factory может создавать компоненты лабиринтов. Он строит
комнаты, стены и двери между комнатами. Им разумно воспользоваться из про-
граммы, которая считывает план лабиринта из файла, а затем создает его, или из
Паттер н Abstrac t Factor y
приложения, строящего случайный лабиринт. Программы построения лабиринта
принимают MazeFactory в качестве аргумента, так что программист может сам
указать классы комнат, стен и дверей:
clas s MazeFactor y {
public:
MazeFactory();
virtua l Maze * MakeMaze O cons t
{ retur n new Maze; }
virtua l Wall * MakeWalK ) cons t
{ retur n new Wall; }
virtua l Room * MakeRoom(in t n) cons t
{ retur n new Room(n); }
virtua l Door * MakeDoor(Room * rl, Room * r2) cons t
{ retur n new Door(rl, r2); }
};
Напомним, что функция-член CreateMaze строит небольшой лабиринт, со-
стоящий всего из двух комнат, соединенных одной дверью. В ней жестко «заши-
ты» имена классов, поэтому воспользоваться функцией для создания лабиринтов
с другими компонентами проблематично.
Вот версия CreateMaze, в которой нет подобного недостатка, поскольку она
принимает MazeFactory в качестве параметра:
Maze * MazeGame::CreateMaz e (MazeFactory & factory ) {
Maze * aMaz e = factory.MakeMaze();
Room * r l = factory.MakeRoom(l);
Room * r 2 = factory.MakeRoom(2);
Door * aDoo r = factory.MakeDoor(rl, r2);
aMaze->AddRoom(rl.) ;
aMaze->AddRoom(r2);
rl->SetSide(North, f act ory.MakeWal l ());
rl->SetSide(East, aDoor);
rl->SetSide(South, factory.MakeWal l ( ) ) ;
rl->SetSide(West, f act ory.MakeWal l ());
r2->SetSide(North, f act ory.MakeWal l ());
r2->SetSide(East, f act or y.MakeWal l ());
r2->SetSide(South, f act ory.MakeWal l ());
r2->SetSide(West, aDoor);
return aMaze;
}
Мы можем создать фабрику Enchant edMazeFactory для производства вол-
шебных лабиринтов, породив подкласс от MazeFactory. В этом подклассе заме-
щены различные функции-члены, так что он возвращает другие подклассы клас-
сов Room, Wall и т.д.:
Порождающие паттерны
clas s EnchantedMazeFactor y : publi c MazeFactor y {
public:
EnchantedMazeFactory();
virtua l Room * MakeRoom(in t n) cons t
{ retur n ne w EnchantedRoom(n, CastSpell()); }
virtua l Door * MakeDoor(Room * rl, Room * r2) cons t
{ retur n ne w DoorNeedingSpell(rl, r2); }
protected:
Spell * CastSpell( ) const;
};
А теперь предположим, что мы хотим построить для некоторой игры лаби-
ринт, в одной из комнат которого заложена бомба. Если бомба взрывается, то
она как минимум обрушивает стены. Тогда можно породить от класса Room под-
класс, отслеживающий, есть ли в комнате бомба и взорвалась ли она. Также нам
понадобится подкласс класса Wal l, который хранит информацию о том, был ли
нанесен ущерб стенам. Назовем эти классы соответственно RoomWithABomb
и BombedWall.
И наконец, мы определим класс BombedMazeFactory, являющийся под-
классом BombedMazeFactory, который создает стены класса BombedWall
и комнаты класса RoomWithABomb. В этом классе надо переопределить всего две
функции:
Wall * BombedMazeFactory::MakeWal l () cons t {
return new BombedWall;
}
Room * BombedMazeFactory::MakeRoom(in t n) const {
return ne w RoomWithABomb(n);
}
Для того чтобы построить простой лабиринт, в котором могут быть спрятаны
бомбы, просто вызовем функцию CreateMaze, передав ей в качестве параметра
BombedMazeFactory:
MazeGam e game;
BombedMazeFactor y factory/-
game. CreateMaz e (factory ) ;
Для построения волшебных лабиринтов CreateMaze может принимать в ка-
честве параметра и EnchantedMazeFactory.
Отметим, что MazeFactory - всего лишь набор фабричных методов. Это са-
мый распространенный способ реализации паттерна абстрактная фабрика.
Еще заметим, что MazeFactory - не абстрактный класс, то есть он работает
и как AbstractFactory, и как ConcreteFactory. Это еще одна типичная
Паттер н Abstrac t Factor y
реализация для простых применений паттерна абстрактная фабрика. Посколь-
ку MazeFactory - конкретный класс, состоящий только из фабричных мето-
дов, легко получить новую фабрику MazeFactory, породив подкласс и замес-
тив в нем необходимые операции.
В функции CreateMaze используется операция SetSide для описания сто-
рон комнат. Если она создает комнаты с помощью фабрики BombedMazeFactory,
то лабиринт будет составлен из объектов класса RoomWithABomb, стороны кото-
рых описываются объектами класса BombedWall. Если классу RoomWithABomb
потребуется доступ к членам BombedWall, не имеющим аналога в его предках, то
придется привести ссылку на объекты-стены от типа Wall* к типу BombedWall*.
Такое приведение к типу подкласса безопасно при условии, что аргумент действи-
тельно принадлежит классу BombedWall*, а это обязательно так, если стены соз-
даются исключительно фабрикой BombedMazeFactory.
В динамически типизированных языках вроде Smalltalk приведение, разуме-
ется, не нужно, но будет выдано сообщение об ошибке во время выполнения, если
объект/класса Wai 1 встретится вместо ожидаемого объекта подкласса класса Wai 1.
Использование абстрактной фабрики для создания стен предотвращает такие
ошибки, гарантируя, что могут быть созданы лишь стены определенных типов.
Рассмотрим версию MazeFactory на языке Smalltalk, в которой есть единствен-
ная операция make, принимающая вид изготавливаемого объекта в качестве парамет-
ра. Конкретная фабрика при этом будет хранить классы изготавливаемых объектов.
Для начала напишем на Smalltalk эквивалент CreateMaze:
В разделе «Реализация» мы уже говорили о том, что классу MazeFactory
нужна всего одна переменная экземпляра partCatalog, предоставляющая сло-
варь, в котором ключом служит класс компонента. Напомним еще раз реализа-
цию метода make:
make: partName
^ (partCatalog at: partName) new
Теперь мы можем создать фабрику MazeFactory и воспользоваться ей
для реализации createMaze. Данную фабрику мы создадим с помощью метода
createMazeFactory класса MazeGame:
createMaze: aFactory
| rooml room2 aDoor |
rooml := (aFactory make: #room) number: 1.
room2 := (aFactory make: #room) number: 2.
aDoor := (aFactory make: #door) from: rooml to: room2.
rooml atSide: #north put: (aFactory make: #wall).
rooml atSide: #east put: aDoor.
rooml atSide: #south put: (aFactory make: #wall).
rooml atSide: #west put: (aFactory make: #wall).
room2 atSide: #north put: (aFactory make: #wall).
room2 atSide: #east put: (aFactory make: #wall).
room2 atSide: #south put: (aFactory make: #wall).
room2 atSide: #west put: aDoor.
^ Maze new addRoom: rooml; addRoom: room2; yourself
Порождающие паттерны
createMazeFactor y
^ (MazeFactor y ne w
addPart: Wal l named: #wall;
addPart: Roo m named: #room;
addPart: Doo r named: #door;
yourself )
BombedMazeFactory и EnchantedMazeFactory создаются путем ассоции-
рования других классов с ключами. Например, EnchantedMazeFactory можно
создать следующим образом:
createMazeFactor y
^ (MazeFactor y ne w
addPart: Wal l named: #wall;
addPart: EnchantedRoo m named: #room;
addPart: DoorNeedingSpel l named: #door;
yourself )
Известные применения
В библиотеке Interviews [Lin92] для обозначения классов абстрактных фаб-
рик используется суффикс «Kit». Так, для изготовления объектов пользователь-
ского интерфейса с заданным внешним обликом определены абстрактные фабри-
ки WidgetKit и DialogKit. В Interviews есть также класс Lay out Kit, который
генерирует разные объекты композиции в зависимости от того, какая требуется
стратегия размещения. Например, размещение, которое концептуально можно
было бы назвать «в строку», может потребовать разных объектов в зависимости
от ориентации документа (книжной или альбомной).
В библиотеке ЕТ++ [WGM88] паттерн абстрактная фабрика применяется для
достижения переносимости между разными оконными системами (например, X
Windows и SunView). Абстрактный базовый класс WindowSystem определяет ин-
терфейс для создания объектов, которое представляют ресурсы оконной системы
(MakeWindow, MakeFont, MakeColor и т.п.). Его конкретные подклассы реализу-
ют эти интерфейсы для той или иной оконной системы. Во время выполнения
ЕТ++ создает экземпляр конкретного подкласса WindowSystem, который уже
и порождает объекты, соответствующие ресурсам данной оконной системы.
Родственные паттерны
Классы Abstract Factory часто реализуются фабричными методами (см.
паттерн фабричный метод), но могут быть реализованы и с помощью паттерна
прототип.
Конкретная фабрика часто описывается паттерном одиночка.
Паттер н Builde r
Название и классификация паттерна
Строитель - паттерн, порождающий объекты.
Паттер н Builde r
Назначение
Отделяе т конструировани е сложног о объекта от его представления, так что
в результат е одного и того же процесс а конструировани я могут получатьс я раз-
ные представления.
Мотивация
Программа, в которую заложена возможност ь распознавани я и чтения доку-
мента в формате RTF (Rich Text Format), должна также «уметь» преобразовыват ь
его во многие другие форматы, наприме р в простой ASCII-текс т или в представ-
ление, которое можно отобразит ь в виджет е для ввода текста. Однако число веро-
ятных преобразовани й заранее неизвестно. Поэтому должна быть обеспечена воз-
можност ь без труда добавлят ь новый конвертор.
Таким образом, нужно сконфигурироват ь класс RTFReade r с помощь ю объек-
та Text Converter, который мог бы преобразовывать RTF в другой текстовый
формат. При разборе документ а в формат е RTF класс RTFReade r вызывае т
TextConverte r для выполнени я преобразования. Всякий раз, как RTFReade r
распознае т лексему RTF (простой текст или управляюще е слово), для ее преобра-
зования объекту TextConverter посылается запрос. Объекты TextConverter
отвечают как за преобразовани е данных, так и за представлени е лексемы в кон-
кретном формате.
Подкласс ы TextConverte r специализируютс я на различных преобразования х
и форматах. Например, ASCIIConverte r игнорируе т запросы на преобразовани е
чего бы то ни было, кроме простого текста. С другой стороны, TeXConverte r будет
реализовыват ь все запросы для получения представлени я в формате редактора TJX,
собирая по ходу необходимую информацию о стилях. A Text Widget Converter
станет строить сложный объект пользовательског о интерфейса, который позво-
лит пользовател ю просматриват ь и редактироват ь текст.
Порождающи е паттерны
Класс каждого конвертора принимает механизм создания и сборки сложного
объекта и скрывает его за абстрактным интерфейсом. Конвертор отделен от загруз-
чика, который отвечает за синтаксический разбор RTF-документа.
В паттерне строитель абстрагированы все эти отношения. В нем любой класс
конвертора называется строителем, а загрузчик - распорядителем. В применении
к рассмотренному примеру строитель отделяет алгоритм интерпретации форма-
та текста (то есть анализатор RTF-документов) от того, как создается и представля-
ется документ в преобразованном формате. Это позволяет повторно использовать
алгоритм разбора, реализованный в RTFReader, для создания разных текстовых
представлений RTF-документов; достаточно передать в RTFReader различные под-
классы класса Text Converter.
Применимость
Используйте паттерн строитель, когда:
а алгоритм создания сложного объекта не должен зависеть от того, из каких
частей состоит объект и как они стыкуются между собой;
а процесс конструирования должен обеспечивать различные представления
конструируемого объекта.
Структура
Участники
a Builder (TextConverter) - строитель:
- задает абстрактный интерфейс для создания частей объекта Product;
a ConcreteBuilder(ASCIIConverter,TeXConverter,TextWidgetConverter)-
конкретный строитель:
- конструирует и собирает вместе части продукта посредством реализации
интерфейса Builder;
- определяет создаваемое представление и следит за ним;
- предоставляет интерфейс для доступа к продукту (например, GetASCI IText,
GetTextWidget);
a Director (RTFReader) - распорядитель:
- конструирует объект, пользуясь интерфейсом Builder;
a Product (ASCIIText, TeXText, TextWidget) - продукт:
Паттерн Builder
- представляет сложный конструируемый объект. ConcreteBuilde r
строит внутреннее представление продукта и определяет процесс его
сборки;
- включает классы, которые определяют составные части, в том числе ин-
терфейсы для сборки конечного результата из частей.
Отношения
а клиент создает объект-распорядитель Director и конфигурирует его нуж-
ным объектом-строителем Builder;
а распорядитель уведомляет строителя о том, что нужно построить очеред-
ную часть продукта;
а строитель обрабатывает запросы распорядителя и добавляет новые части
к продукту;
а клиент забирает продукт у строителя.
Следующая диаграмма взаимодействий иллюстрирует взаимоотношения стро-
ителя и распорядителя с клиентом.
Результаты
Плюсы и минусы паттерна строитель и его применения:
а позволяет изменять внутреннее представление продукта. Объект Builder
предоставляет распорядителю абстрактный интерфейс для конструирова-
ния продукта, за которым он может скрыть представление и внутреннюю
структуру продукта, а также процесс его сборки. Поскольку продукт констру-
ируется через абстрактный интерфейс, то для изменения внутреннего пред-
ставления достаточно всего лишь определить новый вид строителя;
а изолирует код, реализующий конструирование и представление. Паттерн
строитель улучшает модульность, инкапсулируя способ конструирования
и представления сложного объекта. Клиентам ничего не надо знать о клас-
сах, определяющих внутреннюю структуру продукта, они отсутствуют в ин-
терфейсе строителя.
Порождающи е паттерны
Каждый конкретный строител ь ConcreteBuilde r содержит весь код, не-
обходимый для создания и сборки конкретног о вида продукта. Код пишет-
ся только один раз, после чего разные распорядител и могут использоват ь
его повторно для построения вариантов продукт а из одних и тех же частей.
В примере с RTF-документо м мы могли бы определит ь загрузчик для фор-
мата, отличног о от RTF, скажем, SGMLReader, и воспользоватьс я теми же
самыми классами TextConverters для генерирования представлений
SGML-документо в в виде ASCII-текста, ТеХ-текст а или текстовог о виджета;
а дает более тонкий контроль над процессом конструирования. В отличие от
порождающи х паттернов, которые сразу конструирую т весь объект цели-
ком, строител ь делает это шаг за шагом под управление м распорядителя.
И лишь когда продукт завершен, распорядител ь забирает его у строителя.
Поэтому интерфейс строител я в большей степени отражае т процесс кон-
струировани я продукта, нежели другие порождающи е паттерны. Это позво-
ляет обеспечит ь более тонкий контрол ь над процессо м конструирования,
а значит, и над внутренне й структуро й готовог о продукта.
Реализация
Обычно существуе т абстрактный класс Builder, в котором определены опера-
ции для каждог о компонента, который распорядител ь может «попросить » создать.
По умолчани ю эти операции ничего не делают. Но в классе конкретног о строите-
ля ConcreteBuilde r они замещены для тех компонентов, в создании которых
он принимае т участие.
Вот еще некоторые достойные внимани я вопросы реализации:
а интерфейс сборки и конструирования. Строител и конструирую т свои про-
дукты шаг за шагом. Поэтому интерфейс класса Builde r должен быть до-
статочно общим, чтобы обеспечит ь конструировани е при любом виде кон-
кретного строителя.
Ключевой вопрос проектировани я связан с выбором модели процесса кон-
струировани я и сборки. Обычно бывает достаточно модели, в которой ре-
зультаты выполнени я запросов на конструировани е просто добавляютс я
к продукту. В примере с RTF-документам и строитель преобразуе т и добав-
ляет очередну ю лексему к уже конвертированном у тексту.
Но иногда может потребоватьс я доступ к частям сконструированног о к дан-
ному момент у продукта. В примере с лабиринтом, который будет описан
в разделе «Пример кода», интерфейс класса MazeBu i Ider позволяе т добав-
лять дверь между уже существующим и комнатами. Другим примером явля-
ются древовидные структуры, скажем, деревья синтаксическог о разбора, ко-
торые строятся снизу вверх. В этом случае строитель должен был бы вернуть
узлы-потомк и распорядителю, который затем передал бы их назад строите-
лю, чтобы тот мог построит ь родительски е узлы.
Q почему нет абстрактного класса для продуктов. В типично м случае продук-
ты, изготавливаемы е различным и строителями, имеют настольк о разные
представления, что изобретени е для них общег о родительског о класса
Паттер н Builde r
ничего не дает. В примере с RTF-документами трудно представить себе об-
щий интерфейс у объектов ASCIITex t и TextWidget, да он и не нужен.
Поскольк у клиен т обычн о конфигурируе т распорядител я подходящи м кон-
кретны м строителем, то, надо полагать, ему известно, како й именн о под-
класс класса Builder используетс я и как нужно обращатьс я с произведен-
ными продуктами;
а пустые методы класса Builder no умолчанию. В C++ методы строителя на-
меренн о не объявлен ы чист о виртуальным и функциями-членами. Вмест о
этог о они определен ы как пусты е функции, что позволяе т подкласс у заме -
щать тольк о те операции, в которы х он заинтересован.
Пример кода
Определи м вариан т функции-член а CreateMaze, котора я принимае т в каче -
стве аргумент а строитель, принадлежащи й класс у MazeBuilder.
Клас с MazeBuilde r определяе т следующи й интерфей с для построени я ла-
биринтов:
clas s MazeBuilde r {
public:
virtua l voi d BuildMaze( ) { }
virtua l voi d BuildRoom(in t room ) { }
virtua l voi d BuildDoor(in t roomFrom, in t roomTo ) { }
virtua l Maze * GetMaze( ) { retur n 0; }
protected:
MazeBuilder();
};
Этот интерфей с позволяе т создават ь три вещи: лабиринт, комнат у с конкрет -
ным номером, двер и межд у пронумерованным и комнатами. Операци я GetMaz e
возвращае т лабирин т клиенту. В подкласса х MazeBu i Ider данна я операци я пере -
определяетс я для возврат а реальн о созданног о лабиринта.
Все операции построения лабиринта в классе MazeBuilde r по умолчанию
ничег о не делают. Но они не объявлен ы исключительн о виртуальными, чтоб ы
в производны х класса х можн о было замещат ь лишь част ь методов.
Имея интерфей с MazeBuilder, можн о изменит ь функцию-чле н CreateMaze,
чтобы она принимал а строител ь в качеств е параметра:
Maze * MazeGame::CreateMaz e (MazeBuilder & builder ) {
builder.BuildMaze();
builder.BuiIdRoom(l);
builder.BuiIdRoom(2 ) ;
builder.BuildDoor(1, 2);
retur n builder.GetMaze();
}
Порождающие паттерны
Сравните эту версию CreateMaz e с первоначальной. Обратите внимание, как
строитель скрывает внутреннее представление лабиринта, то есть классы комнат,
дверей и стен, и как эти части собираютс я вместе для завершения построения ла-
биринта. Кто-то, может, и догадается, что для представлени я комнат и дверей есть
особые классы, но относительно стен нет даже намека. За счет этого становитс я
проще модифицироват ь способ представлени я лабиринта, поскольку ни одного
из клиентов MazeBuilde r изменять не надо.
Как и другие порождающи е паттерны, строитель инкапсулируе т способ со-
здания объектов; в данном случае с помощью интерфейса, определенног о классом
MazeBuilder. Это означает, что MazeBuilder можно повторно использовать для
построения лабиринто в разных видов. В качестве примера приведем функцию
GreateComplexMaze:
Maze* MazeGame::CreateComplexMaze (MazeBuilder& builder) {
bui l der.Bui l dRoom(l );
builder.BuildRoom(lOOl);
return builder.GetMazeO ;
}
Обратите внимание, что MazeBuilde r не создает лабиринт ы самостоятель -
но, его основная цель - просто определит ь интерфейс для создания лабиринтов.
Пустые реализации в этом интерфейс е определены только для удобства. Реаль-
ную работу выполняют подкласс ы MazeBuilder.
Подкласс StandardMazeBuilde r содержит реализацию построения простых
лабиринтов. Чтобы следить за процессом создания, используетс я переменна я
_currentMaze:
class StandardMazeBuilder : public MazeBuilder {
public:
StandardMazeBuilder();
virtual void Bui l dMazeO;
virtual void BuildRoom(int);
virtual void BuildDoor(int, i nt );
vi rtual Maze * Get Mazef );
private:
Direction CommonWall(Room*, Room*);
Maze* _currentMaze;
};
CommonWal l (общая стена) - это вспомогательна я операция, которая опреде-
ляет направлени е общей для двух комнат стены.
Конструктор StandardMazeBuilder просто инициализирует „currentMaze:
StandardMazeBuilder::StandardMazeBuilder () {
_currentMaze = 0;
}
Паттер н Builde r
BuildMaze инстанцирует объект класса Maze, который будет собираться дру-
гими операциями и, в конце концов, возвратится клиенту (с помощью GetMaze):
void StandardMazeBuilder: : BuildMaz e () {
_currentMaz e = new Maze;
}
Maze* StandardMazeBuilder::GetMaz e () {
retur n _currentMaze;
}
Операция BuildRoom создает комнату и строит вокруг нее стены:
void StandardMazeBuilder::BuildRoom (int n) {
if (!_currentMaze->RoomNo(n) ) {
Room * roo m = ne w Room(n);
_currentMaze->AddRoom(room);
room->SetSide(North, ne w Wal l );
room->SetSide(South, new Wal l );
room->SetSide(East, ne w Wa l l );
room->SetSide(West, new Wal l );
}
}
Чтобы построить дверь между двумя комнатами, StandardMazeBuilder на-
ходит обе комнаты в лабиринте и их общую стену:
void StandardMazeBuilder : rBuildDoor (int nl , int n2 ) {
Room * r l = _currentMaze->RoomN o (nl ) ;
Room * r 2 = _currentMaze->RoomN o (n2 ) ;
Door* d = new Door(rl, r2) ;
rl->SetSide(CommonWall(rl,r2 ) , d) ;
r2->SetSide(CommonWall(r2,rl ) , d) ;
}
Теперь для создания лабиринта клиенты могут использовать Great eMaze в со-
четании с StandardMazeBuilder:
Maze * maze;
MazeGam e game;
StandardMazeBuilder builder;
game. CreateMaz e (builder ) ;
maz e = builder. GetMaz e ( );
Мы могли бы поместить все операции класса StandardMazeBuilder в класс
Maze и позволить каждому лабиринту строить самого себя. Но чем меньше класс
Maze, тем проще он для понимания и модификации, a StandardMazeBuilder
легко отделяется от Maze. Еще важнее то, что разделение этих двух классов поз-
воляет иметь множество разновидностей класса MazeBuilder, в каждом из кото-
рых есть собственные классы для комнат, дверей и стен.
Порождающи е паттерны
Необычным вариантом MazeBuiIder является класс Count ingMazeBuiIder.
Этот строитель вообще не создает никакого лабиринта, он лишь подсчитывает
число компонентов разного вида, которые могли бы быть созданы:
clas s CountingMazeBuilde r : publi c MazeBuilde r {
public:
CountingMazeBuilder O ;
virtua l voi d BuildMazeO;
virtua l voi d BuildRoom(int ) ;
virtua l voi d BuildDoo r (int, int);
virtua l voi d AddWall(int, Direction);
voi d GetCount s (int&, int& ) const;
private:
in t _doors,-
int _rooms;
};
Конструктор инициализирует счетчики, а замещенные операции класса
MazeBuilder увеличивают их:
CountingMazeBuilder: : CountingMazeBuilde r () {
_rooms = _doors = 0;
}
voi d CountingMazeBuilder::BuildRoo m (int ) {
_rooms++;
}
void CountingMazeBuilder: .-BuildDoor (int, int) {
_doors++;
}
voi d CountingMazeBuilder::GetCount s (
int& rooms, int & door s
) cons t {
room s = _rooms;
door s = _doors;
}
Вот как клиент мог бы использовать класс CountingMazeBuilder:
int rooms, doors;
MazeGam e game;
CountingMazeBuilde r builder;
game.CreateMaze(builder);
buiIder.GetCoun t s(rooms, doors);
cou t « "В лабиринт е ест ь "
Паттерн Factor y Method
« room s « " комна т и "
« door s « " дверей" « endl;
Известные применения
Приложени е для конвертировани я из формат а RTF взят о из библиотек и
ЕТ++ [WGM88]. В ней используетс я строител ь для обработк и текста, хранящего -
ся в таком формате.
Паттер н строител ь широк о применяетс я в язык е Smalltalk-8 0 [РагЭО]:
а клас с Parser в подсистем е компиляци и - это распорядитель, котором у
в качеств е аргумент а передаетс я объек т ProgramNodeBuilder. Объек т
класса Parser извещае т объек т ProgramNodeBuilder после распознава -
ния каждо й ситаксическо й конструкции. После завершени я синтаксическо -
го разбор а Parse r обращаетс я к строител ю за созданны м дерево м разбор а
и возвращае т его клиенту;
a Class Builder- это строитель, которы м пользуютс я все класс ы для созда -
ния своих подклассов. В данно м случа е этот класс выступае т одновременн о
в качеств е распорядител я и продукта;
a ByteCodeStream - это строитель, которы й создае т откомпилированны й
метод в виде массив а байтов. ByteCodeStrea m являетс я примеро м нестан -
дартног о применени я паттерн а строитель, поскольк у сложны й объек т
представляетс я как масси в байтов, а не как обычны й объек т Smalltalk. Но
интерфей с к ByteCodeStrea m типиче н для строителя, и этот класс легко
можно было бы заменит ь другим, который представляе т программ у в виде
составног о объекта.
Родственные паттерны
Абстрактна я фабрик а похожа на строител ь в том смысле, что може т кон-
струироват ь сложны е объекты. Основно е различи е межд у ними в том, что строи-
тель делае т акцент на пошагово м конструировани и объекта, а абстрактна я фаб-
рика - на создани и семейст в объекто в (просты х или сложных). Строител ь
возвращае т продук т на последне м шаге, тогда как с точк и зрени я абстрактно й
фабрик и продук т возвращаетс я немедленно.
Паттер н компоновщи к - это то, что часто создае т строитель.
Паттер н Factor y Metho d
Название и классификация паттерна
Фабричны й метод - паттерн, порождающи й классы.
Назначение
Определяе т интерфей с для создани я объекта, но оставляе т подкласса м реше -
ние о том, какой класс инстанцировать. Фабричны й метод позволяе т класс у де-
легироват ь инстанцировани е подклассам.
Порождающие паттерны
Известен также под именем
Virtual Constructor (виртуальный конструктор).
Мотивация
Каркасы пользуются абстрактными классами для определения и поддержания
отношений между объектами. Кроме того, каркас часто отвечает за создание са-
мих объектов.
Рассмотрим каркас для приложений, способных представлять пользователю
сразу несколько документов. Две основных абстракции в таком каркасе - это
классы Application и Document. Оба класса абстрактные, поэтому клиенты
должны порождать от них подклассы для создания специфичных для приложения
реализаций. Например, чтобы создать приложение для рисования, мы определим
классы DrawingApplication и DrawingDocument. Класс Application отве-
чает за управление документами и создает их по мере необходимости, допустим,
когда пользователь выбирает из меню пункт Open (открыть) или New (создать).
Поскольку решение о том, какой подкласс класса Document инстанцировать,
зависит от приложения, то Application не может «предсказать», что именно по-
надобится. Этому классу известно лишь, когда нужно инстанцировать новый доку-
мент, а не какой документ создать. Возникает дилемма: каркас должен инстанциро-
вать классы, но «знает» он лишь об абстрактных классах, которые инстанцировать
нельзя.
Решение предлагает паттерн фабричный метод. В нем инкапсулируется ин-
формация о том, какой подкласс класса Document создать, и это знание выводит-
ся за пределы каркаса.
Подклассы класса Appl i cati on переопределяют абстрактную операцию
CreateDocument таким образом, чтобы она возвращала подходящий подкласс
класса Document. Как только подкласс Application инстанцирован, он может
инстанцировать специфические для приложения документы, ничего не зная об их
классах. Операцию CreateDocument мы называем фабричным методом, по-
скольку она отвечает за «изготовление» объекта.
Паттерн Factory Method
Применимость
Используйт е паттерн фабричны й метод, когда:
а классу заранее неизвестно, объект ы каких классов ему нужно создавать;
а класс спроектирова н так, чтобы объекты, которые он создает, специфици -
ровалис ь подклассами;
а класс делегируе т свои обязанност и одному из нескольки х вспомогательны х
подклассов, и вы планирует е локализоват ь знание о том, какой класс при-
нимает эти обязанност и на себя.
Структура
Участники
a Product (Document) - продукт:
- определяе т интерфей с объектов, создаваемы х фабричны м методом;
a ConcreteProduc t (MyDocument ) - конкретны й продукт:
- реализует интерфейс Product;
a Creator (Application) - создатель:
- объявляет фабричный метод, возвращающий объект типа Product.
Creator может также определять реализацию по умолчанию фабрич-
ного метода, который возвращает объект ConcreteProduct;
- может вызывать фабричный метод для создания объекта Product.
a ConcreteCreator (MyApplication) - конкретный создатель:
- замещает фабричный метод, возвращающий объект ConcreteProduct.
Отношения
Создател ь «полагается » на свои подкласс ы в определени и фабричног о ме-
тода, который будет возвращать экземпляр подходящего конкретного продукта.
Результаты
Фабричные методы избавляют проектировщик а от необходимост и встраиват ь
в код зависящи е от приложени я классы. Код имеет дело только с интерфейсо м
класса Product, поэтому он может работат ь с любыми определенным и пользова -
телями классами конкретны х продуктов.
Порождающи е паттерн ы
Потенциальный недостаток фабричног о метода состоит в том, что клиентам,
возможно, придется создавать подкласс класса Creator для создания лишь од-
ного объекта ConcreteProduct. Порождение подклассов оправдано, если кли-
енту так или иначе приходитс я создавать подклассы Creator, в противном слу-
чае клиенту придется иметь дело с дополнительным уровнем подклассов.
А вот еще два последствия применения паттерна срабричный метод:
а предоставляет подклассам операции-зацепки (hooks). Создание объектов
внутри класса с помощью фабричног о метода всегда оказывается более гиб-
ким решением, чем непосредственно е создание. Фабричный метод создает
в подкласса х операции-зацепк и для предоставлени я расширенно й версии
объекта.
В примере с документом класс Document мог бы определить фабричный
метод CreateFileDialog, который создает диалоговое окно для выбора
файла существующег о документа. Подкласс этого класса мог бы определит ь
специализированно е для приложения диалоговое окно, заместив этот фаб-
ричный метод. В данном случае фабричный метод не является абстрактным,
а содержит разумную реализацию по умолчанию;
а соединяет параллельные иерархии. В примерах, которые мы рассматривал и
до сих пор, фабричные методы вызывались только создателем. Но это со-
вершенно необязательно: клиенты тоже могут применят ь фабричные мето-
ды, особенно при наличии параллельных иерархий классов.
Параллельные иерархии возникают в случае, когда класс делегирует часть
своих обязанносте й другому классу, не являющемус я производным от него.
Рассмотрим, например, графические фигуры, которыми можно манипули-
ровать интерактивно: растягивать, двигать или вращать с помощью мыши.
Реализация таких взаимодействи й с пользователе м - не всегда простое
дело. Часто приходитс я сохранять и обновлять информацию о текущем со-
стоянии манипуляций. Но это состояние нужно только во время самой ма-
нипуляции, поэтому помещать его в объект, представляющи й фигуру, не
следует. К тому же фигуры ведут себя по-разному, когда пользовател ь ма-
нипулируе т ими. Например, растягивание отрезка может сводиться к изме-
нению положения концевой точки, а растягивание текста - к изменению
междустрочных интервалов.
При таких ограничения х лучше использоват ь отдельный объект-манипуля -
тор Manipulator, который реализует взаимодействие и контролирует его те-
кущее состояние. У разных фигур будут разные манипуляторы, являющиеся
подклассом Manipulator. Получающаяс я иерархия класса Manipulato r
параллельна (по крайней мере, частично) иерархии класса Figure.
Класс Figure предоставляет фабричный метод CreateManipulator, ко-
торый позволяет клиентам создавать соответствующи й фигуре манипуля-
тор. Подклассы Figur e замещают этот метод так, чтобы он возвращал под-
ходящий для них подкласс Manipulator. Вместо этого класс Figure может
реализоват ь CreateManipulato r так, что он будет возвращат ь экземпляр
класса Manipulato r по умолчанию, а подклассы Figur e могут наследовать
Паттер н Factor y Metho d
это умолчание. Те класс ы фигур, которы е функционирую т по описанном у
принципу, не нуждаютс я в специально м манипуляторе, поэтом у иерархи и
параллельн ы тольк о отчасти.
Обратит е внимание, как фабричны й мето д определяе т связ ь межд у обеими
иерархиям и классов. В нем локализуетс я знани е о том, каки е класс ы спо-
собны работат ь совместно.
Реализация
Рассмотри м следующи е вопросы, возникающи е при использовани и паттерн а
фабричный метод:
а две основных разновидности паттерна. Во-первых, это случай, когд а клас с
С г eat or'являетс я абстрактны м и не содержи т реализаци и объявленног о
в нем фабричног о метода. Втора я возможность: Creato r - конкретны й
класс, в которо м по умолчани ю есть реализаци я фабричног о метода. Редко,
но встречаетс я и абстрактны й класс, имеющи й реализаци ю по умолчанию;
В перво м случа е для определени я реализаци и необходим ы подклассы, по-
скольк у никаког о разумног о умолчани я не существует. При этом обходитс я
проблема, связанна я с необходимость ю инстанцироват ь заране е неизвест -
ные классы. Во второ м случа е конкретны й класс Creato r используе т фаб-
ричны й метод, главны м образо м ради повышени я гибкости. Выполняетс я
правило: «Создава й объект ы в отдельно й операции, чтоб ы подкласс ы мог -
ли подменит ь спосо б их создания». Соблюдени е этог о правил а гарантирует,
что автор ы подклассо в смогу т при необходимост и изменит ь клас с объектов,
инстанцируемы х их родителем;
а параметризованные фабричные методы. Это еще один вариан т паттерна, ко-
торый позволяе т фабричном у метод у создават ь разны е вид ы продуктов.
Фабричном у метод у передаетс я параметр, которы й идентифицируе т вид
создаваемого объекта. Все объекты, получающиес я с помощь ю фабричног о
метода, разделяют общий интерфейс Product. В примере с документами
класс Applicatio n може т поддерживат ь разны е виды документов. Вы пе-
редает е метод у CreateDocumen t лишни й параметр, которы й и определя-
ет, докумен т каког о вида нужн о создать.
Порождающи е паттерны
В каркасе Unidraw для создания графических редакторов [VL90] использует-
ся именно этот подход для реконструкции объектов', сохраненных на диске.
Unidraw определяет класс Creator с фабричным методом Create, которо-
му в качестве аргумента передается идентификатор класса, определяющий,
какой класс инстанцировать. Когда Unidraw сохраняет объект на диске, он
сначала записывает идентификатор класса, а затем его переменные экземпля-
ра. При реконструкции объекта сначала считывается идентификатор класса.
Прочитав идентификатор класса, каркас вызывает операцию Create, пере-
давая ей этот идентификатор как параметр. Create ищет конструктор со-
ответствующего класса и с его помощью производит инстанцирование.
И наконец, Create вызывает операцию Read созданного объекта, которая
считывает с диска остальную информацию и инициализирует переменные
экземпляра.
Параметризованный фабричный метод в общем случае имеет следующий
вид (здесь My Product и Your Product - подклассы Product):
clas s Creato r {
public:
virtua l Product * Create(Productld);
};
Product* Creator::Create (Productld id) {
if (i d == MINE ) return ne w MyProduct;
if (i d == YOURS ) return new YourProduct;
// выполнит ь для всех остальны х продуктов...
return 0;
}
Замещение параметризованного фабричного метода позволяет легко и изби-
рательно расширить или заменить продукты, которые изготавливает созда-
тель. Можно завести новые идентификаторы для новых видов продуктов или
ассоциировать существующие идентификаторы с другими продуктами.
Например, подкласс MyCreator мог бы переставить местами MyProduct
и YourProduct для поддержки третьего подкласса Their Product:
Product* MyCreator::Create (Productld id) {
if (i d == YOURS ) return new MyProduct;
if (i d == MINE ) return ne w YourProduct;
// N.B.: YOUR S и MIN E переставлен ы
if (id == THEIRS ) return new TheirProduct;
return Creator::Create(id); // вызывается, если больше ничего
//н е осталось
}
Обратите внимание, что в самом конце операция вызывает метод Create ро-
дительского класса. Так делается постольку, поскольку MyCreator: : Create
Паттерн Factor y Method
обрабатывае т только продукт ы YOURS, MINE и THEIRS иначе, чем родитель -
ский класс. Поэтому MyCreator расширяет некоторые виды создаваемых
продуктов, а создание остальных поручает своему родительскому классу;
а языково-зависимые вариации и проблемы. В разных языках возникают соб-
ственные интересные варианты и некоторые нюансы.
Так, в программах на Smalltalk часто используется метод, который возвра-
щает класс подлежащего инстанцированию объекта. Фабричный метод
Creator может воспользоваться возвращенным значением для создания
продукта, a ConcreteCreator может сохранить или даже вычислить это
значение. В результате привязка к типу конкретного инстанцируемого про-
дукта ConcreteProduct происходит еще позже.
В версии примера Document на языке Smalltalk допустимо определить ме-
тод documentClass в классе Application. Данный метод возвращает
подходящий класс Document для инстанцирования документов. Реализа-
ция метода documentClass в классе MyApplication возвращает класс
MyDocument. Таким образом, в классе Application мы имеем
clientMethod
document := self documentClass new.
documentClass
self subclassResponsibility
а в классе MyApplication —
documentClass
^ MyDocument
что возвращает класс MyDocument, который должно инстанцировать при-
ложение Application.
Еще более гибкий подход, родственный параметризованным фабричным
методам, заключается в том, чтобы сохранить подлежащий созданию класс
в качестве переменной класса Application. В таком случае для измене-
ния продукта не нужно будет порождать подкласс Application.
В C++ фабричные методы всегда являются виртуальными функциями, а час-
то даже исключительно виртуальными. Нужно быть осторожней и не вызы-
вать фабричные методы в конструкторе класса Creator: в этот момент фаб-
ричный метод в производном классе ConcreteCreator еще недоступен.
Обойти такую сложность можно, если получать доступ к продуктам только
с помощью функций доступа, создающих продукт по запросу. Вместо того
чтобы создавать конкретный продукт, конструктор просто инициализирует
его нулем. Функция доступа возвращает продукт, но сначала проверяет, что
он существует. Если это не так, функция доступа создает продукт. Подоб-
ную технику часто называют отложенной инициализацией. В следующем
примере показана типичная реализация:
clas s Creato r {
public:
Product * GetProduct();
Порождающи е паттерн ы
protected:
virtua l Product * CreateProduct();
private:
Product * _product;
};
Product * Creator: :GetProduc t () {
if (.product == 0) {
_produc t = CreateProduc t ( ) ;
}
retur n _product;
}
а использование шаблонов, чтобы не порождать подклассы. К сожалению, до-
пустима ситуация, когда вам придется порождат ь подкласс ы только для
того, чтобы создать подходящие объекты-продукты. В C++ этого можно из-
бежать, предоставив шаблонный подкласс класса Creator, параметризо -
ванный классом Product:
clas s Creato r {
publi c :
virtua l Product * CreateProduc t () = 0;
};
templat e <clas s TheProduct >
clas s StandardCreator: publi c Creato r {
public:
virtua l Product * CreateProduct();
};
templat e <clas s TheProduct >
Product * StandardCreator<TheProduct>::CreateProduc t () {
retur n ne w TheProduct;
}
С помощью данного шаблона клиент передает только класс продукта, по-
рождать подклассы от Creator не требуется:
clas s MyProduc t : publi c Produc t {
public:
MyProduct();
// ...
};
StandardCreator<MyProduct > myCreato r ;
а соглашения об именовании. На практике рекомендуетс я применять такие согла-
шения об именах, которые дают ясно понять, что вы пользуетесь фабричными
методами. Например, каркас МасАрр на платформе Macintos h [App89] всегда
объявляет абстрактную операцию, которая определяет фабричный метод, в ви-
де Class* DoMakeClass ( ) , где Class - это класс продукта.
Паттерн Factor y Method
Пример кода
Функция CreateMaze строит и возвращает лабиринт. Одна из связанных
с ней проблем состоит в том, что классы лабиринта, комнат, дверей и стен жестко
«зашиты» в данной функции. Мы введем фабричные методы, которые позволят
выбирать эти компоненты подклассам.
Сначала определим фабричные методы в игре MazeGame для создания объек-
тов лабиринта, комнат, дверей и стен:
clas s MazeGam e {
public:
Maze * CreateMaze();
// фабричны е методы:
virtua l Maze * MakeMaze O cons t
{ retur n new Maze; }
virtua l Room * MakeRoom(in t n) cons t
{ retur n new Room(n); }
virtua l Wall * MakeWalK ) cons t
{ retur n new Wall; }
virtua l Door * MakeDoor(Room * rl, Room * r2) cons t
{ retur n new Door(rl, r2); }
};
Каждый фабричный метод возвращает один из компонентов лабиринта. Класс
MazeGame предоставляет реализации по умолчанию, которые возвращают прос-
тейшие варианты лабиринта, комнаты, двери и стены.
Теперь мы можем переписать функцию CreateMaze с использованием этих
фабричных методов:
Maze * MazeGame::CreateMaz e () {
Maze * aMaz e = MakeMaze();
Room * rl = MakeRoom(l);
Room * r2 = MakeRoom(2);
Door * theDoo r = MakeDoor(rl, r2);
aMaze->AddRoom(rl);
aMaze->AddRoom(r2);
rl->SetSide(North, MakeWall());
rl->SetSide(East, theDoor);
rl->SetSide(South, MakeWall());
rl->SetSide(West, MakeWall());
r2->SetSide(North, MakeWall());
r2->SetSide(East, MakeWall());
r2->SetSide(South, MakeWall());
r2->SetSide(West, theDoor);
Порождающи е паттерны
return aMaze;
}
В играх могут порождатьс я различные подкласс ы MazeGame для специализа -
ции частей лабиринта. В этих подкласса х допустимо переопределени е некоторых
или всех методов, от которых зависят разновидност и продуктов. Например, в игре
BombedMazeGam e продукт ы Room и Wal l могут быть переопределен ы так, чтобы
возвращат ь комнат у и стену с заложенно й бомбой:
clas s BombedMazeGam e : publi c MazeGam e {
public:
BombedMazeGame();
virtua l Wall * MakeWall( ) cons t
{ retur n ne w BombedWall; }
virtua l Room * MakeRoom(in t n ) cons t
{ retur n ne w RoomWithABomb(n); }
};
А в игре Enchan t edMazeGam e допустимо определит ь такие варианты:
clas s EnchantedMazeGam e : publi c MazeGam e {
public:
EnchantedMazeGame();
virtua l Room * MakeRoomdn t n ) cons t
{ retur n ne w EnchantedRoom(n, CastSpell()); }
virtua l Door * MakeDoor(Room * rl, Room * r2 ) cons t
{ retur n ne w DoorNeedingSpell(rl, r2); }
protected:
Spell * CastSpell( ) const;
};
Известные применения
Фабричные методы в изобилии встречаютс я в инструментальны х библиоте -
ках и каркасах. Рассмотренны й выше приме р с документам и - это типично е при-
менение в каркасе МасАрр и библиотек е ЕТ++ [WGM88]. Приме р с манипулято -
ром заимствова н из каркас а Unidraw.
Класс Vie w в схеме модель/вид/контролле р из языка Smalltalk-8 0 имеет ме-
тод defaultController, который создает контроллер, и этот метод выглядит
как фабричный [РагЭО]. Но подкласс ы Vie w специфицирую т класс своего кон-
троллера по умолчанию, определя я метод def aultControllerClass, возвраща -
ющий класс, экземпляр ы которог о создает defaultController. Таким образом,
реальным фабричны м методо м являетс я def aultControllerClass, то есть
метод, который должен переопределятьс я в подклассах.
Более необычным являетс я приме р фабричног о метода parserClass, тоже
взятый из Smalltalk-80, который определяетс я поведение м Behavio r (суперклас с
Паттерн Prototyp e
всех объектов, представляющих классы). Он позволяет классу использовать спе-
циализированный анализатор своего исходного кода. Например, клиент может опре-
делить класс SQLParser для анализа исходного кода класса, содержащего встроен-
ные предложения на языке SQL. Класс Behavior реализует par serClass так, что
тот возвращает стандартный для Smalltal k класс анализатора Parser. Класс же,
включающий предложения SQL, замещает этот метод (как метод класса) и во-
звращает класс SQLParser.
Система Orbix ORB от компании IONA Technologies [ION94] использует фаб-
ричный метод для генерирования подходящих заместителей (см. паттерн замес-
титель) в случае, когда объект запрашивает ссылку на удаленный объект. Фаб-
ричный метод позволяет без труда заменить подразумеваемог о заместителя,
например таким, который применяет кэширование на стороне клиента.
Родственные паттерны
Абстрактная фабрика часто реализуется с помощью фабричных методов.
Пример в разделе «Мотивация» из описания абстрактной фабрики иллюстри-
. ет также и паттерн фабричные методы.
Паттерн фабричные методы часто вызывается внутри шаблонных методов.
В примере с документами NewDocument - это шаблонный метод.
Прототипы не нуждаются в порождении подклассов от класса Creator. Од-
нак о им часто бывает необходима операция I ni t i al i ze в классе Product.
Treator использует I ni t i al i ze для инициализации объекта. Фабричному
методу такая операция не требуется.
Паттерн Prototype
Название и классификация паттерна
Прототип - паттерн, порождающий объекты.
Назначение
Задает виды создаваемых объектов с помощью экземпляра-прототип а и созда-
ет новые объекты путем копирования этого прототипа.
Мотивация
Построить музыкальный редактор удалось бы путем адаптации общего кар-
каса графических редакторов и добавления новых объектов, представляющих
ноты, паузы и нотный стан. В каркасе редактора может присутствоват ь палитра
инструментов для добавления в партитуру этих музыкальных объектов. Палитра
может также содержать инструменты для выбора, перемещения и иных манипу-
ляций с объектами. Так, пользователь, щелкнув, например, по значку четверти
поместил бы ее тем самым в партитуру. Или, применив инструмент перемещения,
: двигал бы ноту на стане вверх или вниз, чтобы изменить ее высоту.
Предположим, что каркас предоставляет абстрактный класс Graphi c для гра-
фических компонентов вроде нот и нотных станов, а также абстрактный класс
Порождающи е паттерны
Tool для определения инструментов в палитре. Кроме того, в каркасе имеется пре-
допределенный подкласс GraphicTool для инструментов, которые создают гра-
фические объекты и добавляют их в документ.
Однако класс GraphicTool создает некую проблему для проектировщика
каркаса. Классы нот и нотных станов специфичны для нашего приложения,
а класс GraphicTool принадлежит каркасу. Этому классу ничего неизвестно о том,
как создавать экземпляры наших музыкальных классов и добавлять их в партиту-
ру. Можно было бы породить от GraphicTool подклассы для каждого вида музы-
кальных объектов, но тогда оказалось бы слишком много классов, отличающихся
только тем, какой музыкальный объект они инстанцируют. Мы знаем, что гибкой
альтернативой порождению подклассов является композиция. Вопрос в том, как
каркас мог бы воспользоваться ею для параметризации экземпляров GraphicTool
классом того объекта Graphic, который предполагается создать.
Решение - заставить GraphicTool создавать новый графический объект, ко-
пируя или «клонируя» экземпляр подкласса класса Graphic. Этот экземпляр мы
будем называть прототипом. GraphicTool параметризуется прототипом, кото-
рый он должен клонировать и добавить в документ. Если все подклассы Graphic
поддерживают операцию Clone, то GraphicTool может клонировать любой вид
графических объектов.
Итак, в нашем музыкальном редакторе каждый инструмент для создания му-
зыкального объекта - это экземпляр класса GraphicTool, инициализированный
тем или иным прототипом. Любой экземпляр GraphicTool будет создавать му-
зыкальный объект, клонируя его прототип и добавляя клон в партитуру.
Можно воспользоваться паттерном прототип, чтобы еще больше сократить
число классов. Для целых и половинных нот у нас есть отдельные классы, но, быть
может, это излишне. Вместо этого они могли бы быть экземплярами одного и того
же класса, инициализированного разными растровыми изображениями и дли-
тельностями звучания. Инструмент для создания целых нот становится просто
объектом класса GraphicTool, в котором прототип MusicalNot e инициализи-
рован целой нотой. Это может значительно уменьшить число классов в системе.
Заодно упрощается добавление нового вида нот в музыкальный редактор.
Паттер н Prototyp e
Применимость
Используйт е паттер н прототип, когда система не должна зависет ь от того, как
в ней создаются, компонуютс я и представляютс я продукты:
а инстанцируемы е класс ы определяютс я во время выполнения, наприме р
с помощь ю динамическо й загрузки;
а для того чтобы избежат ь построени я иерархи й классов или фабрик, парал-
лельных иерархи и классов продуктов;
а экземпляр ы класса могут находитьс я в одном из не очень большог о числа
различны х состояний. Може т оказатьс я удобне е установит ь соответствую -
щее число прототипо в и клонироват ь их, а не инстанцироват ь каждый раз
класс вручну ю в подходяще м состоянии.
Структура
Участники
a Prototype (Graphic) - прототип:
- объявляе т интерфейс для клонировани я самог о себя;
a ConcretePrototype ( St a f f- нотный стан, WholeNot e - целая нота,
Hal f Not e - половинна я нота) - конкретны й прототип:
- реализуе т операци ю клонировани я себя;
a Client (GraphicTool) - клиент:
- создае т новый объект, обращаяс ь к прототип у с запросо м клонироват ь
себя.
Отношения
Клиен т обращаетс я к прототипу, чтобы тот создал свою копию.
Результаты
У прототип а те же самые результаты, что у абстрактно й фабрик и и строите -
ля: он скрывае т от клиент а конкретны е класс ы продуктов, уменьша я тем самым
число известны х клиент у имен. Кроме того, все эти паттерн ы позволяю т клиен-
там работат ь со специфичным и для приложени я классами без модификаций.
Порождающи е паттерны
Ниже перечислены дополнительные преимущества паттерна прототип:
а добавление и удаление продуктов во время выполнения. Прототип позволяет
включать новый конкретный класс продуктов в систему, просто сообщив
клиенту о новом экземпляре-прототипе. Это несколько более гибкое реше-
ние по сравнению с тем, что удастся сделать с помощью других порождаю-
щих паттернов, ибо клиент может устанавливат ь и удалять прототипы во
время выполнения;
а спецификация новых объектов путем изменения значений. Динамичные сис-
темы позволяют определять поведение за счет композиции объектов - на-
пример, путем задания значений переменных объекта, - а не с помощью
определения новых классов. По сути дела, вы определяете новые виды
объектов, инстанцируя уже существующие классы и регистрируя их-экземп-
ляры как прототипы клиентских объектов. Клиент может изменить поведе-
ние, делегируя свои обязанности прототипу.
Такой дизайн позволяет пользователям определять новые классы без про-
граммирования. Фактически клонирование объекта аналогично инстанци-
рованию класса. Паттерн прототип может резко уменьшить число необхо-
димых системе классов. В нашем музыкальном редакторе с помощью одного
только класса GraphicTool удастся создать бесконечное разнообразие му-
зыкальных объектов;
а специфицирование новых объектов путем изменения структуры. Многие
приложения строят объекты из крупных и мелких составляющих. Напри-
мер, редакторы для проектирования печатных плат создают электрические
схемы из подсхем.1 Такие приложения часто позволяют инстанцироват ь
сложные, определенные пользователем структуры, скажем, для многократ-
ного использования некоторой подсхемы.
Паттерн прототип поддерживает и такую возможность. Мы просто добав-
ляем подсхему как прототип в палитру доступных элементов схемы. При
условии, что объект, представляющий составную схему, реализует операцию
Clone как глубокое копирование, схемы с разными структурами могут вы-
ступать в качестве прототипов;
а уменьшение числа подклассов. Паттерн фабричный метод часто порождает
иерархию классов Creator, параллельну ю иерархии классов продуктов.
Прототип позволяет клонировать прототип, а не запрашивать фабричный
метод создать новый объект. Поэтому иерархия класса Creator становит-
ся вообще ненужной. Это преимущество касается главным образом языков
типа C++, где классы не рассматриваютс я как настоящие объекты. В языках
же типа Smalltal k и Objective С это не так существенно, поскольку всегда мож-
но использовать объект-класс в качестве создателя. В таких языках объекты-
классы уже выступают как прототипы;
а динамическое конфигурирование приложения классами. Некоторые среды по-
зволяют динамически загружать классы в приложение во время его выпол-
нения. Паттерн прототип - это ключ к применению таких возможносте й
в языке типа C++.
Для таки х приложени й характерн ы паттерн ы компоновщи к и декоратор.
Паттерн Prototyp e
Приложение, которое создает экземпляры динамически загружаемого клас-
са, не может обращаться к его конструктору статически. Вместо этого ис-
полняющая среда автоматически создает экземпляр каждого класса в мо-
мент его загрузки и регистрирует экземпляр в диспетчере прототипов (см.
раздел «Реализация»). Затем приложение может запросить у диспетчера
прототипов экземпляры вновь загруженных классов, которые изначально не
были связаны с программой. Каркас приложений ЕТ++ [WGM88] в своей
исполняющей среде использует именно такую схему.
Основной недостаток паттерна прототип заключается в том, что каждый под-
•пасс класса Prototype должен реализовывать операцию Clone, а это далеко не
всегда просто. Например, сложно добавить операцию Clone, когда рассматрива-
тмые классы уже существуют. Проблемы возникают и в случае, если во внутреннем
представлении объекта есть другие объекты или наличествуют круговые ссылки.
Реализация
Прототип особенно полезен в статически типизированных языках вроде C++,
где классы не являются объектами, а во время выполнения информации о типе
достаточно или нет вовсе. Меньший интерес данный паттерн представляет для
"аких языков, как Smalltalk или Objective С, в которых и так уже есть нечто экви-
валентное прототипу (именно - объект-класс) для создания экземпляров каждо-
::» класса. В языки, основанные на прототипах, например Self [US87], где созда-
ние любого объекта выполняется путем клонирования прототипа, этот паттерн
просто встроен.
Рассмотрим основные вопросы, возникающие при реализации прототипов:
а использование диспетчера прототипов. Если число прототипов в системе не
фиксировано (то есть они могут создаваться и уничтожаться динамически),
ведите реестр доступных прототипов. Клиенты должны не управлять про-
тотипами самостоятельно, а сохранять и извлекать их из реестра. Клиент
запрашивает прототип из реестра перед его клонированием. Такой реестр
мы будем называть диспетчером прототипов.
Диспетчер прототипов - это ассоциативное хранилище, которое возвраща-
ет прототип, соответствующий заданному ключу. В нем есть операции для
регистрации прототипа с указанным ключом и отмены регистрации. Кли-
енты могут изменять и даже «просматривать» реестр во время выполнения,
а значит, расширять систему и вести контроль над ее состоянием без напи-
сания кода;
а реализация операции Clone. Самая трудная часть паттерна прототип - пра-
вильная реализация операции Clone. Особенно сложно это в случае, когда
в структуре объекта есть круговые ссылки.
В большинстве языков имеется некоторая поддержка для клонирования
объектов. Например, Smalltalk предоставляет реализацию копирования, ко-
торую все подклассы наследуют от класса Object. В C++ есть копирую-
щий конструктор. Но эти средства не решают проблему «глубокого и по-
верхностного копирования» [GR83]. Суть ее в следующем: должны ли при
Порождающи е паттерны
клонировании объекта клонироваться также и его переменные экземпляра
или клон просто разделяет с оригиналом эти переменные?
Поверхностное копирование просто, и часто его бывает достаточно. Имен-
но такую возможность и предоставляет по умолчанию Smalltalk. В C++ ко-
пирующий конструктор по умолчанию выполняет почленное копирование,
. то есть указатели разделяются копией и оригиналом. Но для клонирования
прототипов со сложной структурой обычно необходимо глубокое копиро-
вание, поскольку клон должен быть независим от оригинала. Поэтому нуж-
но гарантировать, что компоненты клона являются клонами компонентов
прототипа. При клонировании вам приходится решать, что именно может
разделяться и может ли вообще.
Если объекты в системе предоставляют операции Save (сохранить) и Load
(загрузить), то разрешается воспользоваться ими для реализации операции
Clone по умолчанию, просто сохранив и сразу же загрузив объект. Опера-
ция Save сохраняет объект в буфере памяти, a Load создает дубликат, ре-
конструируя объект из буфера;
а инициализация клонов. Хотя некоторым клиентам вполне достаточно клона
как такового, другим нужно инициализировать его внутреннее состояние
полностью или частично. Обычно передать начальные значения операции
Clone невозможно, поскольку их число различно для разных классов про-
тотипов. Для некоторых прототипов нужно много параметров инициализа-
ции, другие вообще ничего не требуют. Передача Clone параметров мешает
построению единообразного интерфейса клонирования.
Может оказаться, что в ваших классах прототипов уже определяются опе-
рации для установки и очистки некоторых важных элементов состояния.
Если так, то этими операциями можно воспользоваться сразу после клони-
рования. В противном случае, возможно, понадобится ввести операцию
Ini ti al i ze (см. раздел «Пример кода»), которая принимает начальные
значения в качестве аргументов и соответственно устанавливает внутрен-
нее состояние клона. Будьте осторожны, если операция Clone реализует
глубокое копирование: копии может понадобиться удалять (явно или внут-
ри Initialize) перед повторной инициализацией.
Пример кода
Мы определим подкласс MazePrototypeFactory класса MazeFactory.
Этот подкласс будет инициализироваться прототипами объектов, которые ему
предстоит создавать, поэтому нам не придется порождать подклассы только ради
изменения классов создаваемых стен или комнат.
MazePrototypeFactory дополняет интерфейс MazeFactory конструкто-
ром, принимающим в качестве аргументов прототипы:
class MazePrototypeFactor y : public MazeFactor y {
public:
MazePrototypeFactory(Maze*, Wal l *, Room*, Door*);
virtual Maze * MakeMaze( ) const;
virtual Room * MakeRoom(int ) const;
Паттерн Prototype
virtua l Wall * MakeWalK ) const;
virtua l Door * MakeDoor(Room*, Room* ) const;
private:
Maze * _prototypeMaze;
Room * __prototypeRoom;
Wall * _prototypeWall;
Door * _prototypeDoor;
};
Новый конструктор просто инициализируе т свои прототипы:
MazePrototypeFactory::MazePrototypeFactor y (
Maze * m, Wall * w, Room * r, Door * d
) {
_prototypeMaz e = m;
_prototypeWal l = w;
_prototypeRoo m = r;
_prototypeDoor = d;
}
Функции-член ы для создания стен, комна т и дверей похожи друг на друга:
каждая клонирует, а затем инициализируе т прототип. Вот определени я функций
..'akeWall и MakeDoor:
Wall * MazePrototypeFactory::MakeWal l () cons t {
retur n _prototypeWall->Clone();
}
Door * MazePrototypeFactory::MakeDoo r (Room * rl, Roo m *r2 ) cons t {
Door * doo r = _prototypeDoor->Clone();
door->Initialize(rl, r2);
retur n door;
}
Мы можем применит ь MazePrototypeFactor y для создания прототипич -
ного или принимаемог о по умолчани ю лабиринта, просто инициализиру я его про-
тотипами базовых компонентов:
MazeGam e game;
MazePrototypeFactor y simpleMazeFactory (
new Maze, ne w Wall, ne w Room, ne w Doo r
);
Maze * maz e = game.CreateMaze(simpleMazeFactory);
Для изменения типа лабиринта инициализируе м MazePrototypeFactor y
другим набором прототипов. Следующий вызов создает лабиринт с дверью типа
BombedDoor и комнатой типа RoomWithABomb:
MazePrototypeFactor y bombedMazeFactory (
new Maze, ne w BombedWall,
new RoomWithABomb, ne w Doo r
);
Порождающие паттерны
Объект, который предполагается использовать в качестве прототипа, напри-
мер экземпляр класса Wal l, должен поддерживать операцию Clone. Кроме того.
у него должен быть копирующий конструктор для клонирования. Также может
потребоваться операция для повторной инициализации внутреннего состояния.
Мы добавим в класс Door операцию Ini ti al i ze, чтобы дать клиентам возмож-
ность инициализировать комнаты клона.
Сравните следующее определение Door с приведенным на стр. 91:
clas s Doo r : publi c MapSit e {
public:
Door();
Door(cons t Door&);
•
virtua l voi d Initialize(Room*, Room*);
virtua l Door * Clone( ) const;
virtua l voi d Enter();
Room * OtherSideFrom(Room*);
private:
Room * _rooml;
Room * _room2;
};
Door::Doo r (cons t Door & other ) {
_room l = other._rooml;
_room 2 = other._room2;
}
void Door::Initializ e (Room * rl, Room * r2) {
_room l = rl;
_room 2 = r2;
}
Door * Door::Clon e () cons t {
retur n ne w Door(*this);
}
Подкласс BombedWall должен заместить операцию Clone и реализовать ее
ответствующий копирующий конструктор:
clas s BombedWal l : publi c Wal l {
public:
BombedWall();
BombedWall(cons t BombedWallk);
virtua l Wall * Clone( ) const;
bool HasBomb();
private:
bool _bomb;
};
Паттерн Prototyp e
BombedWall::BombedWal l (cons t BombedWall k other ) : Wall(other ) {
_bom b = other._bomb;
}
Wall * BombedWall::Clon e () cons t {
retur n new BombedWall(*this);
}
Операция BombedWall: : Clone возвращает Wal l *, а ее реализация - указа-
тель на новый экземпляр подкласса, то есть BombedWal 1 *. Мы определяем Clone
в базовом классе именно таким образом, чтобы клиентам, клонирующим прото-
тип, не надо было знать о его конкретных подклассах. Клиентам никогда не при-
дется приводить значение, возвращаемое Clone, к нужному типу.
В Smalltalk разрешается использовать стандартный метод копирования, уна-
следованный от класса Object, для клонирования любого прототипа MapSite.
Можно воспользоваться фабрикой MazeFactory для изготовления любых необ-
ходимых прототипов. Например, допустимо создать комнату по ее номеру #room.
В классе MazeFactory есть словарь, сопоставляющий именам прототипы. Его
метод make: выглядит так:
k
make: partNam e
^ (partCatalo g at: partName ) copy
Имея подходящие методы для инициализации MazeFactory прототипами,
можно было бы создать простой лабиринт с помощью следующего кода:.
CreateMaz e
on: (MazeFactor y ne w
with: Door new named: #door;
with: Wal l new named: #wall;
with: Roo m ne w named: #room;
yoursel f)
где определение метода класса on: для CreateMaze имеет вид
on: aFactor y
| room l room 2 |
room l := (aFactor y make: #room). location: 1@1.
room 2 := (aFactor y make: #room ) location: 2@1.
door := (aFactor y make: #door ) from: room l to: room2.
room l
atSide: #nort h put: (aFactor y make: #wall);
atSide: #eas t put: door;
atSide: #sout h put: (aFactor y make: #wall);
atSide: #wes t put: (aFactor y make: #wall).
room 2
atSide: #nort h put: (aFactor y make: #wall);
atSide: #eas t put: (aFactor y make: #wall);
atSide: #sout h put: (aFactor y make: #wall);
atSide: #wes t put: door.
Порождающи е паттерны
^ Maz e ne w
addRoom: rooml;
addRoom: room2;
yoursel f
Известные применения
Быть может, впервые паттерн прототи п был использова н в системе Sketchpa d
Ивана Сазерленд а (Ivan Sutherland ) [Sut63]. Первым широко известным приме-
нением этого паттерна в объектно-ориентированно м языке была система Thing-
Lab, в которой пользовател и могли сформироват ь составно й объект, а затем пре-
вратить его в прототип, поместив в библиотек у повторно используемы х объектов
[Вог81]. Ад ель Голдберг и Давид Робсон упоминают прототип ы в качеств е пат-
тернов в работе [GR83], но Джеймс Коплиен [Сор92] рассматривает этот вопрос
гораздо шире. Он описывае т связанные с прототипо м идиомы языка C++ и при-
водит много примеро в и вариантов.
Etgdb - это оболочка отладчико в на базе ЕТ++, где имеется интерфей с вида
point-and-clic k (укажи и щелкни ) для различны х командны х отладчиков. Для
каждог о из них есть свой подклас с DebuggerAdaptor. Например, GdbAdapto r
настраивае т etgdb на синтакси с команд GNU gdb, a SunDbxAdapto r - на отлад-
чик dbx компании Sun. Набор подклассов DebuggerAdaptor не «зашит» в etgdb.
Вместо этого он получае т имя адаптера из переменно й среды, ищет в глобально й
таблице прототип с указанным именем, а затем его клонирует. Добавит ь к etgdb
новые отладчик и можно, связав ядро с подклассо м DebuggerAdaptor, разрабо-
танным для этого отладчика.
Библиотек а приемо в взаимодействи я в программ е Mode Compose r хранит
прототип ы объектов, поддерживающи х различные способы интерактивны х отно-
шений [Sha90]. Любой созданный с помощь ю Mode Compose r способ взаимодей -
ствия можно применит ь в качеств е прототипа, если поместит ь его в библиотеку.
Паттерн прототип позволяе т программе поддерживат ь неограниченно е число ва-
риантов отношений.
Пример музыкальног о редактора, обсуждавшийс я в начале этого раздела, ос-
нован на каркас е графически х редакторо в Unidra w [VL90].
Родственные паттерны
В некоторых отношения х прототи п и абстрактна я фабрик а являютс я кон-
курентами. Но их используют и совместно. Абстрактна я фабрика может хранить
набор прототипов, которые клонируютс я и возвращают изготовленны е объекты.
В тех проектах, где активно применяютс я паттерны компоновщи к и декора -
тор, тоже можно извлечь пользу из прототипа.
Паттер н Singleto n
Название и классификация паттерна
Одиночк а - паттерн, порождающи й объекты.
Паттерн Singleton
Назначение
Гарантирует, что у класса есть только один экземпляр, и предоставляет к нему
глобальную точку доступа.
Мотивация
Для некоторых классов важно, чтобы существовал только один экземпляр. Хотя
в системе может быть много принтеров, но возможен лишь один спулер. Должны
быть только одна файловая система и единственный оконный менеджер. В циф-
ровом фильтре может находиться только один аналого-цифровой преобразователь
(АЦП). Бухгалтерская система обслуживает только одну компанию.
Как гарантировать, что у класса есть единственный экземпляр и что этот эк-
земпляр легко доступен? Глобальная переменная дает доступ к объекту, но не за-
прещает инстанцироват ь класс в нескольких экземплярах.
Более удачное решение - сам класс контролирует то, что у него есть только
один экземпляр, может запретить создание дополнительных экземпляров, пере-
хватывая запросы на создание новых объектов, и он же способен предоставить
доступ к своему экземпляру. Это и есть назначение паттерна одиночка.
Применимость
Используйте паттерн одиночка, когда:
а должен быть ровно один экземпляр некоторого класса, легко доступный
всем клиентам;
а единственный экземпляр должен расширяться путем порождения подклас-
сов, и клиентам нужно иметь возможность работать с расширенным экземп-
ляром без модификации своего кода.
Структура
Участники
a Singleton - одиночка:
- определяет операцию Instance, которая позволяет клиентам получать
доступ к единственному экземпляру. Instance - это операция класса, то
есть метод класса в терминологии Smalltal k и статическая функция-член
в C++;
- может нести ответственност ь за создание собственного уникальног о эк-
земпляра.
Порождающие паттерны
Отношения
Клиенты получают доступ к экземпляру класса Singleton только через его
операцию Instance.
Результаты
У паттерна одиночка есть определенные достоинства:
Q контролируемый доступ к единственному экземпляру. Поскольку класс
Singleton инкапсулируе т свой единственный экземпляр, он полностью
контролирует то, как и когда клиенты получают доступ к нему;
а уменьшение числа имен. Паттерн одиночка - шаг вперед по сравнению с гло-
бальными переменными. Он позволяет избежать засорения пространства
имен глобальными переменными, в которых хранятся уникальные экземп-
ляры;
а допускает уточнение операций и представления. От класса Singleton мож-
но порождать подклассы, а приложение легко сконфигурироват ь экземп-
ляром расширенног о класса. Можно конкретизироват ь приложение экземп-
ляром того класса, который необходим во время выполнения;
а допускает переменное число экземпляров. Паттерн позволяет вам легко изме-
нить свое решение и разрешить появление более одного экземпляра класса
Singleton. Вы можете применять один и тот же подход для управления чис-
лом экземпляров, используемых в приложении. Изменить нужно будет лишь
операцию, дающую доступ к экземпляру класса Singleton;
а большая гибкость, чем у операций класса. Еще один способ реализовать функ-
циональность одиночки - использовать операции класса, то есть статичес-
кие функции-члены в C++ и методы класса в Smalltalk. Но оба этих приема
препятствуют изменению дизайна, если потребуется разрешить наличие
нескольких экземпляров класса. Кроме того, статические функции-члены
в C++ не могут быть виртуальными, так что их нельзя полиморфно замес-
тить в подклассах.
Реализация
При использовании паттерна одиночка надо рассмотреть следующие вопросы:
а гарантирование единственного экземпляра. Паттерн одиночка устроен так,
что тот единственный экземпляр, который имеется у класса, - самый обыч-
ный, но больше одного экземпляра создать не удастся. Чаще всего для этого
прячут операцию, создающую экземпляры, за операцией класса (то есть за
статической функцией-члено м или методом класса), которая гарантирует
создание не более одного экземпляра. Данная операция имеет доступ к пе-
ременной, где хранится уникальный экземпляр, и гарантирует инициализа-
цию переменной этим экземпляром перед возвратом ее клиенту. При таком
подходе можно не сомневаться, что одиночка будет создан и инициализи-
рован перед первым использованием.
В C++ операция класса определяется с помощью статической функции-чле-
на Instance класса Singleton. В этом классе есть также статическая
Паттерн Singleton Jill
переменная-чле н „instance, котора я содержи т указател ь на уникальны й
экземпляр.
Класс Singleton объявлен следующим образом:
class Singleton {
public:
static Singleton* Instance();
protected:
Singleton();
private:
static Singleton* „instance;
};
А реализаци я такова:
Singleton* Singleton::_instance = 0;
Singleton* Singleton::Instance () {
if (_instance == 0) {
_instance = new Singleton;
}
return „instance;
}
Клиент ы осуществляю т досту п к одиночк е исключительн о чере з функцию -
член Instance. Переменна я „instance инициализируетс я нулем, а ста-
тическа я функция-чле н Instanc e возвращае т ее значение, инициализиру я
ее уникальны м экземпляром, если в текущи й момен т оно равн о 0. Функци я
Instance используе т отложенну ю инициализацию: возвращаемо е ей зна-
чени е не создаетс я и не хранитс я вплот ь до момент а первог о обращения.
Обратит е внимание, что конструкто р защищенный. Клиент, которы й попы -
тается инстанцироват ь класс Singleto n непосредственно, получит ошиб-
ку на этап е компиляции. Это дае т гарантию, что буде т созда н тольк о один
экземпляр.
Далее, поскольк у „instance - указател ь на объек т класс а Singleton, то
функция-чле н Instanc e може т присвоит ь этой переменно й указател ь на
любо й подклас с данног о класса. Применени е возможност и мы увиди м
в раздел е «Приме р кода».
О реализаци и в C++ скаже м особо. Недостаточн о определит ь рассматрива -
емый патер н как глобальны й или статически й объект, а зате м полагатьс я на
автоматическу ю инициализацию. Тому есть три причины:
- мы не може м гарантировать, что буде т объявле н тольк о один экземпля р
статическог о объекта;
- у нас може т не быт ь достаточн о информаци и для инстанцировани я лю-
бого одиночк и во врем я статическо й инициализации. Одиночк е могу т
быт ь необходим ы данные, вычисляемы е позже, во врем я выполнени я
программы;
- в C++ не определяетс я порядо к вызов а конструкторо в для глобальны х
объекто в чере з границ ы едини ц трансляци и [ES90]. Эт о означает, что
Порождающи е паттерны
межд у одиночкам и не може т существоват ь никаки х зависимостей. Есл и
они есть, то ошибо к не избежать.
Еще один (хот я и не слишко м серьезный ) недостато к глобальных/статически х
объекто в в том, что приходитс я создават ь всех одиночек, даже, если они не
используются. Применени е статическо й функции-член а решае т эту проблему.
В Smalltal k функция, возвращающа я уникальны й экземпляр, реализуетс я
как мето д класс а Singleton. Чтоб ы гарантироват ь единственност ь экземп -
ляра, следуе т заместит ь операци ю new. Получающийс я клас с мог бы имет ь
два метод а класс а (в них Solelnstanc e - это переменна я класса, котора я
больше нигд е не используется):
new
self error: 'не удается создать новы й объект1
default
Solelnstance isNil i fTrue: [Solelnstance := super new].
^ Solelnstance
а порождение подклассов Singleton. Основно й вопро с не стольк о в том, как опре -
делит ь подкласс, а в том, как сделать, чтоб ы клиент ы могл и использоват ь
его единственны й экземпляр. По существу, переменная, ссылающаяс я на
экземпля р одиночки, должн а инициализироватьс я вместе с экземпляро м
подкласса. Простейши й спосо б добитьс я этог о - определит ь одиночку, ко-
торог о нужн о применят ь в операци и Instance класс а Singleton. В раз -
деле «Приме р кода » показывается, как можн о реализоват ь эту техник у с по-
мощь ю переменны х среды.
Друго й спосо б выбор а подкласс а Singleto n - вынест и реализаци ю опера -
ции Instance из родительског о класса (например, MazeFactory ) и помес-
тить ее в подкласс. Это позволи т программист у на C++ задат ь клас с оди-
ночк и на этап е компоновк и (скомпонова в программ у с объектны м файлом,
содержащи м другу ю реализацию), но от клиент а одиночк а буде т по-прежне -
му скрыт.
Тако й подхо д фиксируе т выбо р класс а одиночк и на этап е компоновки, за-
трудня я тем самым его подмен у во врем я выполнения. Применени е услов -
ных операторо в для выбор а подкласс а увеличивае т гибкост ь решения, но
все равн о множеств о возможны х классо в Singleto n остаетс я жестк о «за -
шитым » в код. В обще м случа е ни тот, ни друго й подхо д не обеспечиваю т
достаточно й гибкости.
Ее можн о добитьс я за сче т использовани я реестра одиночек. Вмест о тог о
чтоб ы задават ь множеств о возможны х классо в Singleto n в операци и
Instance, одиночк и могу т регистрироват ь себя по имен и в некоторо м всем
известно м реестре.
Реест р сопоставляет одиночка м строковы е имена. Когд а операци и Instanc e
нуже н некоторы й одиночка, она запрашивае т его у реестр а по имени. Начи -
наетс я поис к указанног о одиночки, и, если он существует, реест р возвраща -
ет его. Такой подход освобождае т Instanc e от необходимост и «знать» все
Паттер н Singleto n
возможны е класс ы или экземпляр ы Singleton. Нуже н лишь едины й для
всех классов Singleton интерфейс, включающий операции с реестром:
class Singleton {
public:
static void Register(const char* name, Singleton*);
static Singleton* Instance ().;
protected:
static Singleton* Lookup(const char* name);
private:
static Singleton* „instance;
static List<NameSingletonPair>* „registry;
Операци я Register регистрируе т экземпля р класс а Singleton под ука-
занным именем. Чтоб ы не усложнят ь реестр, мы буде м хранит ь в нем спи-
сок объекто в NameSingletonPair. Кажды й тако й объек т отображае т имя
на одиночку. Операци я Looku p ище т одиночк у по имени. Предположим, что
имя нужног о одиночк и передаетс я в переменно й среды:
Singleton* Singleton::Instance () {
if („instance == 0) {
const char* singletonName = getenv("SINGLETON");
// пользователь или среда предоставляют это имя на стадии
// запуска программы
_instance = Lookup(singletonName);
// Lookup возвращает 0, если такой одиночка не найден
return „instance;
В како й момен т класс ы Singleto n регистрирую т себя? Одна из возмож -
носте й - конструктор. Например, подклас с MySingleto n мог бы работат ь
так:
MySingleton::MySingleton() {
Singleton::Register("MySingleton", this);
}
Разумеется, конструкто р не буде т вызван, пок а кто-т о не инстанцируе т
класс, но ведь это та самая проблема, котору ю паттер н одиночк а и пытаетс я
разрешить! В C++ ее можн о попытатьс я обойти, определи в статически й эк-
земпля р класс а My Single ton. Например, можн о вставит ь строк у
static MySingleton theSingleton;
в файл, где находитс я реализация MySingleton.
Теперь класс Singleto n не отвечает за создание одиночки. Его основной
обязанность ю становитс я обеспечени е доступ а к объекту-одиночк е из
Порождающи е паттерны
любой части системы. Подход, сводящийс я к применени ю статическог о
объекта, по-прежнем у имеет потенциальны й недостаток: необходим о созда-
вать экземпляр ы всех возможных подклассо в Singleton, иначе они не бу-
дут зарегистрированы.
Пример кода
Предположим, нам надо определит ь класс MazeFactor y для создания лаби-
ринтов, описанный на стр. 99. MazeFactor y определяе т интерфейс для построени я
различных частей лабиринта. В подкласса х эти операции могут переопределять -
ся, чтобы возвращат ь экземпляр ы специализированны х классов продуктов, на-
пример объект ы BombedWall, а не просто Wall.
Существенн о здесь то, что приложени ю Maz e нужен лишь один экземпля р
фабрики лабиринто в и он должен быть доступе н в коде, строяще м любую часть
лабиринта. Тут-то паттерн одиночк а и приходи т на помощь. Сделав фабрик у
MazeFactor y одиночкой, мы сможем обеспечит ь глобальну ю доступност ь объек-
та, представляющег о лабиринт, не прибега я к глобальным переменным.
Для простот ы предположим, что мы никогд а не порождае м подклассо в от
MazeFactory. (Чуть ниже будет рассмотре н альтернативны й подход.) В C++ для
того, чтобы превратит ь фабрику в одиночку, мы добавляе м в класс MazeFactor y
статическую операцию Instance и статический член _instance, в котором бу-
дет хранитьс я единственны й экземпляр. Нужно также сделать конструкто р защи-
щенным, чтобы предотвратит ь случайно е инстанцирование, в результат е которо-
го будет создан лишний экземпляр:
class MazeFactory {
public:
static MazeFactory* Instance();
// здесь находится существующий интерфейс
protected:
MazeFactory();
private:
static MazeFactory* „instance;
};
Реализаци я класс а такова:
MazeFactory* MazeFactory::_instance = 0;
MazeFactory* MazeFactory::Instance 0 {
if (_instance == 0) {
_instance = new MazeFactory;
}
return _instance;
}
Теперь посмотрим, что случится, когда у MazeFa c tory есть подкласс ы и опре-
деляется, какой из них использовать. Вид лабиринт а мы будем выбират ь с по-
мощью переменно й среды, поэтому добавим код, который инстанцируе т нужный
Паттерн Singleton
подклас с MazeFactor y в зависимост и от значени я данно й переменной. Лучш е
всег о поместит ь код в операци ю Instance, поскольк у он а уж е и так инстанциру -
ет MazeFactory:
MazeFactory * MazeFactory::Instanc e () {
if (_instanc e == 0) {
cons t char * mazeStyl e = getenv("MAZESTYLE");
if (strcmp(mazeStyle, "bombed") == 0) {
„.instanc e = new BombedMazeFactory;
} els e if (strcmp(mazeStyle, "enchanted") == 0) {
_instanc e = new EnchantedMazeFactory;
// ... други е возможны е подкласс ы
} els e { // по умолчани ю
_instanc e = new MazeFactory;
}
}
retur n _instance;
}
Отметим, что операцию Instance нужно модифицировать при определении
каждого нового подкласса MazeFactory. В данном приложении это, может быть,
и не проблема, но для абстрактных фабрик, определенных в каркасе, такой под-
ход трудно назвать приемлемым.
Одно из решений - воспользоваться принципом реестра, описанным в разде-
ле «Реализация». Может помочь и динамическое связывание, тогда приложению
не нужно будет загружать все неиспользуемые подклассы.
Известные применения
Примером паттерна одиночка в Smalltalk-80 [РагЭО] является множество из-
менений кода, представленное классом ChangeSet. Более тонкий пример - это
отношение между классами и их метаклассами. Метаклассом называется класс
класса, каждый метакласс существует в единственном экземпляре. У метакласса
нет имени (разве что косвенное, определяемое экземпляром), но он контролирует
свой уникальный экземпляр, и создать второй обычно не разрешается.
В библиотеке Interviews для создания пользовательских интерфейсов
[LCI+92] - паттерн одиночка применяется для доступа к единственным экземпля-
рам классов Session (сессия) и WidgetKit (набор виджетов). Классом Session
определяется главный цикл распределения событий в приложении. Он хранит
пользовательские настройки стиля и управляет подключением к одному или не-
скольким физическим дисплеям. WidgetKit - это абстрактная фабрика для
определения внешнего облика интерфейсных виджетов. Операция Widget-
Ki t: : instance () определяет конкретный инстанцируемый подкласс WidgetKit
на основе переменной среды, которую устанавливает Session. Аналогичная опе-
рация в классе Session «выясняет», поддерживаются ли монохромные или цвет-
ные дисплеи, и соответственно конфигурирует одиночку Session.
Порождающие паттерны
Родственные паттерны
С помощь ю паттерн а одиночк а могу т быт ь реализован ы многи е паттерны. См.
описани е абстрактно й фабрики, строител я и прототипа.
Обсуждени е порождающи х паттерно в
Есть два наиболее распространенных способа параметризовать систему клас-
сами создаваемых ей объектов. Первый способ - порождение подклассов от клас-
са, создающего объекты. Он соответствует паттерну фабричный метод. Основ-
ной недостаток метода: требуется создавать новый подкласс лишь для того, чтобы
изменить класс продукта. И таких изменений может быть очень много. Напри-
мер, если создатель продукта сам создается фабричным методом, то придется
замещать и создателя тоже.
Другой способ параметризации системы в большей степени основан на ком-
позиции объектов. Вы определяете объект, которому известно о классах объек-
тов-продуктов, и делаете его параметром системы. Это ключевой аспект таких
паттернов, как абстрактная фабрика, строитель и прототип. Для всех трех
характерно создание «фабричного объекта», который изготавливает продукты.
В абстрактной фабрике фабричный объект производит объекты разных классов.
Фабричный объект строителя постепенно создает сложный продукт, следуя спе-
циальному протоколу. Фабричный объект прототипа изготавливает продукт пу-
тем копирования объекта-прототипа. В последнем случае фабричный объект
и прототип - это одно и то же, поскольку именно прототип отвечает за возврат
продукта.
Рассмотрим каркас графических редакторов, описанный при обсуждении naV-
терна прототип. Есть несколько способов параметризовать класс GraphicTool
классом продукта:
а применить паттерн фабричный метод. Тогда для каждого подкласса клас-
са Graphic в палитре будет создан свой подкласс GraphicTool. В классе
GraphicTool будет присутствовать операция NewGraphic, переопределя-
емая каждым подклассом;
а использовать паттерн абстрактная фабрика. Возникнет иерархия классов
GraphicsFactories, по одной для каждого подкласса Graphic. В этом
случае каждая фабрика создает только один продукт: CircleFactor y -
окружности Circle, LineFactory - отрезки Line и т.д. GraphicTool па-
раметризуется фабрикой для создания подходящих графических объектов;
о применить паттерн прототип. Тогда в каждом подклассе Graphic будет ре-
ализована операция Clone, a GraphicTool параметризуется прототипом
создаваемого графического объекта.
Выбор паттерна зависит от многих факторов. В нашем примере каркаса гра-
фических редакторов, на первый взгляд, проще всего воспользоваться фабрич-
ным методом. Определить новый подкласс GraphicTool легко, а экземпляры
GraphicTool создаются только в момент определения палитры. Основной недо-
статок такого подхода заключается в комбинаторном росте числа подклассов
GraphicTool, причем все они почти ничего не делают.
Абстрактна я фабрик а лишь немноги м лучше, поскольк у требуе т создани я
равновелико й иерархи и классо в GraphicsFactory. Абстрактну ю фабрик у сле-
дует предпочест ь фабричном у метод у лишь тогда, когда уже и так существуе т
иерархи я класс а GraphicsFactory: либо потому, что ее автоматическ и строи т
компилято р (как в Smalltal k или Objectiv e С), либо она необходим а для друго й
части системы.
Очевидно, целя м каркас а графически х редакторо в лучше всего отвечае т пат-
терн прототип, поскольк у для его применени я требуетс я лишь реализоват ь опе-
рацию Clon e в каждо м класс е Graphics. Это сокращае т числ о подклассов,
a Clon e можн о с пользо й применит ь и для решени я други х задач - например, для
реализаци и пункт а меню Duplicat e (дублировать), - а не тольк о для инстанциро -
вания.
В случа е применени я паттерн а фабричны й метод проек т в больше й степен и
поддаетс я настройк е и оказываетс я лишь немноги м боле е сложным. Други е пат-
терны нуждаютс я в создани и новых классов, а фабричны й метод - тольк о в со-
здании одной новой операции. Част о этот паттер н рассматриваетс я как стандарт -
ный спосо б создани я объектов, но вряд ли его стоит рекомендоват ь в ситуации,
когда инстанцируемы й клас с никогд а не изменяетс я или когда инстанцировани е
выполняетс я внутр и операции, котору ю легко можн о заместит ь в подкласса х (на-
пример, во время инициализации).
Проекты, в которы х используютс я паттерн ы абстрактна я фабрика, прототи п
или строитель, оказываютс я еще более гибкими-, чем те, где применяетс я фабрич -
ный метод, но за это приходитс я платит ь повышенно й сложностью. Част о
в начал е работ ы над проекто м за основ у беретс я фабричны й метод, а позже, когда
проектировщи к обнаруживает, что решени е получаетс я недостаточн о гибким, он
выбирае т другие паттерны. Владени е разным и паттернам и проектировани я откры-
вает перед вами широки й выбор при оценк е различны х критериев.
Обсуждени е порождающи х паттерно в
Глава 4. Структурные паттерны
В структурных паттернах рассматривается вопрос о том, как из классов и объектов
образуются более крупные структуры. Структурные паттерны уровня класса
используют наследование для составления композиций из интерфейсов и реали-
заций. Простой пример - использование множественного наследования для объе-
динения нескольких классов в один. В результате получается класс, обладающий
свойствами всех своих родителей. Особенно полезен этот паттерн, когда нужно
организовать совместную работу нескольких независимо разработанных библи-
отек. Другой пример паттерна уровня класса - адаптер. В общем случае адаптер
делает интерфейс одного класса (адаптируемого) совместимым с интерфейсом
другого, обеспечивая тем самым унифицированную абстракцию разнородных
интерфейсов. Это достигается за счет закрытого наследования адаптируемому
классу. После этого адаптер выражает свой интерфейс в терминах операций адапти-
руемого класса.
Вместо композиции интерфейсов или реализаций структурные паттерны уров-
ня объекта компонуют объекты для получения новой функциональности. Допол-
нительная гибкость в этом случае связана с возможностью изменить композицию
объектов во время выполнения, что недопустимо для статической композиции
классов.
Примером структурного паттерна уровня объектов является компоновщик.
Он описывает построение иерархии классов для двух видов объектов: примитив-
ных и составных. Последние позволяют создавать произвольно сложные структу-
ры из примитивных и других составных объектов. В паттерне заместитель
объект берет на себя функции другого объекта. У заместителя есть много приме-
нений. Он может действовать как локальный представитель объекта, находяще-
гося в удаленном адресном пространстве. Или представлять большой объект, за-
гружаемый по требованию. Или ограничивать доступ к критически важному
объекту. Заместитель вводит дополнительный косвенный уровень доступа к от-
дельным свойствам объекта. Поэтому он может ограничивать, расширять или из-
менять эти свойства.
Паттерн приспособленец определяет структуру для совместного использо-
вания объектов. Владельцы разделяют объекты, по меньшей мере, по двум причи-
нам: для достижения эффективности и непротиворечивости. Приспособленец
акцентирует внимание на эффективности использования памяти. В приложени-
ях, в которых участвует очень много объектов, должны снижаться накладные рас-
ходы на хранение. Значительной экономии можно добиться за счет разделения
объектов вместо их дублирования. Но объект может быть разделяемым, только
если его состояние не зависит от контекста. У объектов-приспособленцев такой
Паттерн Adapter
зависимости нет. Любая дополнительна я информация передается им по мере не-
обходимости. В отсутствие контекстных зависимостей объекты-приспособленц ы
могут легко разделяться.
Если паттерн приспособлене ц дает способ работы с большим числом мел-
ких объектов, то фасад показывает, как один объект может представлять целую
подсистему. Фасад представляет набор объектов и выполняет свои функции, пе-
ренаправляя сообщения объектам, которых он представляет. Паттерн мост от-
деляет абстракцию объекта от его реализации, так что их можно изменять неза-
висимо.
Паттерн декоратор описывает динамическое добавление объектам новых
обязанностей. Это структурный паттерн, который рекурсивно компонует объек-
ты с целью реализации заранее неизвестного числа дополнительных функций. На-
пример, объект-декоратор, содержащий некоторый элемент пользовательског о
интерфейса, может добавить к нему оформление в виде рамки или тени либо но-
вую функциональность, например возможность прокрутки или изменения масш-
таба. Два разных оформления прибавляются путем простого вкладывания одного
декоратора в другой. Для достижения этой цели каждый объект-декоратор дол-
жен соблюдать интерфейс своего компонента и перенаправлят ь ему сообщения.
Свои функции (скажем, рисование рамки вокруг компонента) декоратор может
выполнять как до, так и после перенаправления сообщения.
Многие структурные паттерны в той или иной мере связаны друг с другом.
Эти отношения обсуждаются в конце главы.
Паттер н Adapte r
Название и классификация паттерна
Адаптер - паттерн, структурирующи й классы и объекты.
Назначение
Преобразует интерфейс одного класса в интерфейс другого, который ожида-
ют клиенты. Адаптер обеспечивает совместную работу классов с несовместимы-
ми интерфейсами, которая без него была бы невозможна.
Известен также под именем
Wrapper (обертка).
Мотивация
Иногда класс из инструментально й библиотеки, спроектированный для по-
вторного использования, не удается использовать только потому, что его интер-
фейс не соответствует тому, который нужен конкретному приложению.
Рассмотрим, например, графический редактор, благодаря которому пользо-
ватели могут рисовать на экране графические элементы (линии, многоугольники,
текст и т.д.) и организовыват ь их в виде картинок и диаграмм. Основной аб-
стракцией графическог о редактора является графический объект, который имеет
Структурны е паттерны
изменяемую форму и изображает сам себя. Интерфейс графических объектов опре-
делен абстрактным классом Shape. Редактор определяет подкласс класса Shape
для каждого вида графических объектов: LineShape для прямых, PolygonShape
для многоугольников и т.д.
Классы для элементарных геометрических фигур, например LineShape
и PolygonShape, реализовать сравнительно просто, поскольку заложенные
в них возможности рисования и редактирования крайне ограничены. Но подкласс
Text Shape, умеющий отображать и редактировать текст, уже значительно слож-
нее, поскольку даже для простейших операций редактирования текста нужно не-
тривиальным образом обновлять экран и управлять буферами. В то же время, воз-
можно, существует уже готовая библиотека для разработки пользовательских
интерфейсов, которая предоставляет развитый класс Text View, позволяющий
отображать и редактировать текст. В идеале мы хотели бы повторно использовать
Text View для реализации Text Shape, но библиотека разрабатывалась без учета
классов Shape, поэтому заставить объекты Text View и Shape работать совмест-
но не удается.
Так каким же образом существующие и независимо разработанные классы вро-
де Text View могут работать в приложении, которое спроектировано под другой,
несовместимый интерфейс? Можно было бы так изменить интерфейс класса
Text View, чтобы он соответствовал интерфейсу Shape, только для этого нужен
исходный код. Но даже если он доступен, то вряд ли разумно изменять Text View;
библиотека не должна приспосабливаться к интерфейсам каждого конкретного
приложения.
Вместо этого мы могли бы определить класс Text Shape так, что он будет
адаптировать интерфейс Text View к интерфейсу Shape. Это допустимо сделать
двумя способами: наследуя интерфейс от Shape, а реализацию от Text View;
включив экземпляр Text View в Text Shape и реализовав Text Shape в терми-
нах интерфейса Text View. Два данных подхода соответствуют вариантам паттер-
на адаптер в его классовой и объектной ипостасях. Класс Text Shape мы будем
называть адаптером.
На этой диаграмме показан адаптер объекта. Видно, как запрос BoundingBox,
объявленный в классе Shape, преобразуется в запрос Get Extent, определенный
Паттер н Adapte r
в классе Text View. Поскольку класс Text Shape адаптирует TextView к интер-
фейсу Shape, графически й редакто р може т воспользоватьс я классо м TextView,
хотя тот и имее т несовместимы й интерфейс.
Часто адапте р отвечае т за функциональность, котору ю не може т предоставит ь
адаптируемы й класс. На диаграмм е показано, как адапте р выполняе т таког о рода
функции. У пользовател я должн а быть возможност ь перемещат ь любой объект
класс а Shape в друго е место, но в класс е TextVie w такая операци я не предусмот -
рена. Text Shape может добавить недостающую функциональность, самостоя-
тельно реализова в операци ю CreateManipulato r класс а Shape, котора я воз-
вращает экземпляр подходящего подкласса Manipulator.
Manipulato r - это абстрактны й клас с объектов, которы м известно, как ани-
мироват ь Shape в ответ на такие действи я пользователя, как перетаскивани е фи-
гуры в друго е место. У класс а Manipulato r имеютс я подкласс ы для различны х
фигур. Например, TextManipulator - подкласс для Text Shape. Возвращая эк-
земпля р TextManipulator, объек т класс а TextShap e добавляе т нову ю функ -
циональность, которо й в класс е TextVie w нет, а класс у Shape требуется.
Применимость
Применяйт е паттер н адаптер, когда:
Q хотит е использоват ь существующи й класс, но его интерфей с не соответству -
ет вашим потребностям;
а собираетес ь создат ь повторн о используемы й класс, которы й долже н взаимо -
действоват ь с заране е неизвестным и или не связанным и с ним классами,
имеющим и несовместимы е интерфейсы;
Q (только для адаптера объектов!) нужн о использоват ь нескольк о существу -
ющих подклассов, но непрактичн о адаптироват ь их интерфейс ы путем по-
рождени я новых подклассо в от каждого. В этом случа е адапте р объекто в
може т приспосабливат ь интерфей с их общег о родительског о класса.
Структура
Адапте р класс а используе т множественно е наследовани е для адаптаци и одно-
го интерфейс а к другому.
Адапте р объект а применяе т композици ю объектов.
Структурны е паттерн ы
Участник и
a Target (Shape) - целевой:
- определяе т зависящи й от предметно й област и интерфейс, которы м поль -
зуется Client;
a Client (DrawingEditor) - клиент:
- вступае т во взаимоотношени я с объектами, удовлетворяющим и интер -
фейсу Target;
a Adaptee (Textview) - адаптируемый:
- определяе т существующи й интерфейс, которы й нуждаетс я в адаптации;
a Adapter (Text Shape) - адаптер:
- адаптирует интерфейс Adaptee к интерфейсу Target.
Отношения
Клиент ы вызываю т операци и экземпляр а адаптер а Adapter. В свою очеред ь
адапте р вызывае т операци и адаптируемог о объект а или класс а Adaptee, которы й
и выполняе т запрос.
Результаты
Результат ы применени я адаптеро в объекто в и классо в различны. Адапте р
класса:
а адаптирует Adaptee к Target, перепоручая действия конкретному классу
Adaptee. Поэтом у данны й паттер н не буде т работать, если мы захоти м од-
новременн о адаптироват ь клас с и его подклассы;
а позволяе т адаптер у Adapte r заместит ь некоторы е операци и адаптируемог о
класса Adaptee, так как Adapter есть не что иное, как подкласс Adaptee;
а вводи т тольк о один новый объект. Чтоб ы добратьс я до адаптируемог о клас -
са, не нужн о никаког о дополнительног о обращени я по указателю.
Адапте р объектов:
а позволяе т одном у адаптер у Adapte r работат ь со многи м адаптируемым и
объектами Adaptee, то есть с самим Adaptee и его подклассами (если та-
ковые имеются). Адапте р може т добавит ь нову ю функциональност ь сраз у
всем адаптируемы м объектам;
а затрудняе т замещени е операци й класс а Adaptee. Для этог о потребуетс я по-
родить от Adaptee подкласс и заставить Adapter ссылаться на этот под-
класс, а не на сам Adaptee.
Ниже приведены вопросы, которые следует рассмотреть, когда вы решаете
применить паттерн адаптер:
а объем работы по адаптации. Адаптеры сильно отличаются по тому объему
работы, который необходим для адаптации интерфейса Adapt ее к интер-
фейсу Target. Это может быть как простейшее преобразование, например
изменение имен операций, так и поддержка совершенно другого набора опе-
раций. Объем работы зависит от того, насколько сильно отличаются друг от
друга интерфейсы целевого и адаптируемого классов;
а сменные адаптеры. Степень повторной используемости класса тем выше,
чем меньше предположений делается о тех классах, которые будут его при-
менять. Встраивая адаптацию интерфейса в класс, вы отказываетесь от
предположения, что другим классам станет доступен тот же самый интер-
фейс. Другими словами, адаптация интерфейса позволяет включить ваш
класс в существующие системы, которые спроектированы для класса с дру-
гим интерфейсом. В системе ObjectWorks\Smalltalk [РагЭО] используется
термин сменный адаптер (pluggable adapter) для обозначения классов со
встроенной адаптацией интерфейса.
Рассмотрим виджет TreeDisplay, позволяющий графически отображать
древовидные структуры. Если бы это был специализированный виджет,
предназначенный только для одного приложения, то мы могли бы потребо-
вать специального интерфейса от объектов, которые он отображает. Но если
мы хотим сделать его повторно используемым (например, частью библио-
теки полезных виджетов), то предъявлять такое требование неразумно. Раз-
ные приложения, скорей всего, будут определять собственные классы для
представления древовидных структур, и не следует заставлять их пользо-
ваться именно нашим абстрактным классом Tree. А у разных структур де-
ревьев будут и разные интерфейсы.
Например, в иерархии каталогов добраться до потомков удастся с помощью
операции GetSubdirector ies, тогда как для иерархии наследования со-
ответствующая операция может называться Get Subclasses. Повторно
используемый виджет TreeDisplay должен «уметь» отображать иерар-
хии обоих видов, даже если у них разные интерфейсы. Другими словами,
в TreeDisplay должна быть встроена возможность адаптации интерфейсов.
О способах встраивания адаптации интерфейсов в классы говорится в раз-
деле «Реализация»;
а использование двусторонних адаптеров для обеспечения прозрачности. Адап-
теры непрозрачны для всех клиентов. Адаптированный объект уже не обла-
дает интерфейсом Adapt ее, так что его нельзя использовать там, где Adaptee
был применим. Двусторонние адаптеры способны обеспечить такую про-
зрачность. Точнее, они полезны в тех случаях, когда клиент должен видеть
объект по-разному.
Рассмотрим двусторонний адаптер, который интегрирует каркас графических
редакторов Unidraw [VL90] и библиотеку для разрешения ограничений QOCA
[HHMV92]. В обеих системах-есть классы, явно представляющие переменные:
Паттерн Adapter
Структурны е паттерн ы
в Unidra w это StateVariable, а в QOCA - ConstraintVariable. Чтобы
заставит ь Unidra w работат ь совместн о с QOCA, ConstraintVariabl e
нужно адаптироват ь к StateVariable. А для того чтобы решени я QOCA
распространялис ь на Unidraw, StateVariabl e следуе т адаптироват ь
к ConstraintVariable.
Здесь примене н двусторонни й адапте р класс а ConstraintStateVariable,
который являетс я подклассо м одновременн о StateVariabl e и Const -
raintVariabl e и адаптируе т оба интерфейс а друг к другу. Множествен -
ное наследовани е в данном случае вполне приемлемо, поскольк у интерфей -
сы адаптированны х классо в существенн о различаются. Двусторонни й
адапте р класса соответствуе т интерфейса м каждог о из адаптируемы х клас-
сов и может работат ь в любой системе.
Реализация
Хотя реализаци я адаптер а обычно не вызывае т затруднений, кое о чем все же
стоит помнить:
а реализация адаптеров классов в C++. В C++ реализаци я адаптер а класса
Adapter открыт о наследуе т от класса Target и закрыт о - от Adaptee.
Таким образом, Adapter должен быть подтипом Target, но не Adaptee;
а сменные адаптеры. Рассмотри м три способа реализаци и сменных адапте -
ров для описанног о выше виджет а TreeDisplay, который може т автома -
тически отображат ь иерархически е структуры.
Первый шаг, общий для всех трех реализаций, - найти «узкий» интерфей с
для Adaptee, то есть наименьше е подмножеств о операций, позволяюще е
выполнит ь адаптацию. «Узкий » интерфейс, состоящи й всего из пары ите-
раций, легче адаптировать, чем интерфей с из нескольки х десятко в опера-
ций. Для TreeDi splay адаптаци и подлежи т любая иерархическа я струк-
тура. Минимальны й интерфей с мог бы включат ь всего две операции: одна
определяе т графическо е представлени е узла в иерархическо й структуре,
другая - доступ к потомка м узла.
«Узкий» интерфей с приводи т к трем подхода м к реализации:
- использование абстрактных операций. Определи м в классе TreeDi splay
абстрактны е операции, которые соответствую т «узкому » интерфейс у клас-
са Adaptee. Подкласс ы должны реализовыват ь эти абстрактны е операци и
Паттерн Adapter
и адаптировать иерархически структурированный объект. Например,
подкласс DirectoryTreeDisplay при их реализации будет осуществ-
лять доступ к структуре каталогов файловой системы.
DirectoryTreeDi splay специализирует узкий интерфейс таким обра-
зом, чтобы он мог отображать структуру каталогов, составленную из объек-
тов FileSystemEntity;
использование объектов-уполномоченных. При таком подходе TreeDisplay
переадресует запросы на доступ к иерархической структуре объекту-
уполномоченному. TreeDisplay может реализовывать различные стра-
тегии адаптации, подставляя разных уполномоченных.
Например, предположим, что существует класс DirectoryBrowser, ко-
торый использует TreeDisplay. DirectoryBrowser может быть упол-
номоченным для адаптации TreeDisplay к иерархической структуре
каталогов. В динамически типизированных языках вроде Smalltalk или
Objective С такой подход требует интерфейса для регистрации уполно-
моченного в адаптере. Тогда TreeDisplay просто переадресует запросы
уполномоченному. В системе NEXTSTEP [Add94] этот подход активно
используется для уменьшения числа подклассов.
Структурны е паттерны
В статически типизированных языках вроде C++ требуется явно опреде-
лять интерфейс для уполномоченного. Специфицировать такой интер-
фейс можно, поместив «узкий» интерфейс, который необходим классу
TreeDisplay, в абстрактный класс TreeAccessorDelegate. После
этого допустимо добавить этот интерфейс к выбранному уполномочен-
ному - в данном случае DirectoryBrowser - с помощью наследования.
Если у DirectoryBrowser еще нет существующего родительского клас-
са, то воспользуемся одиночным наследованием, если есть - множествен-
ным. Подобное смешивание классов проще, чем добавление нового под-
класса и реализация его операций по отдельности;
- параметризованные адаптеры. Обычно в Smalltalk для поддержки смен-
ных адаптеров параметризуют адаптер одним или несколькими блоками.
Конструкция блока поддерживает адаптацию без порождения подклассов.
Блок может адаптировать запрос, а адаптер может хранить блок для каж-
дого отдельного запроса. В нашем примере это означает, что TreeDisplay
хранит один блок для преобразования узла в GraphicNode, а другой - для
доступа к потомкам узла.
Например, чтобы создать класс TreeDisplay для отображения иерар-
хии каталогов, мы пишем:
directoryDispla y :=
(TreeDispla y on: treeRoot )
getChiIdrenBlock:
[:nod e | nod e getSubdirectories ]
createGraphicNodeBlock:
[:nod e | nod e createGraphicNode ] .
Есл и вы встраивает е интерфей с адаптаци и в класс, то это т спосо б дае т
удобну ю альтернатив у подклассам.
Пример кода
Приведем краткий обзор реализации адаптеров класса и объекта для при-
мера, обсуждавшегося в разделе «Мотивация», при этом начнем с классов Shape
и TextView:
clas s Shap e {
public:
Shape();
virtua l voi d BoundingBox (
Point s bottomLeft, Point & topRigh t
) const;
virtua l Manipulator * CreateManipulator( ) const;
};
clas s TextVie w {
public:
TextView();
voi d GetOrigin(Coord & x, Coord s y) const;
Паттерн Adapter
void GetExtent(Coord& width, Coords height) const;
virtual bool IsEmpty() const;
};
В классе Shape предполагается, что ограничивающий фигуру прямоугольник
определяется двумя противоположными углами. Напротив, в классе Text View
он характеризуется начальной точкой, высотой и шириной. В классе Shape опре-
делена также операция CreateManipulator для создания объекта-манипулятора
класса Manipulator, который знает, как анимировать фигуру в ответ на действия
пользователя.1 В Text View эквивалентной операции нет. Класс Text Shape явля-
ется адаптером между двумя этими интерфейсами.
Для адаптации интерфейса адаптер класса использует множественное насле-
дование. Принцип адаптера класса состоит в наследовании интерфейса по одной
ветви и реализации - по другой. В C++ интерфейс обычно наследуется открыто,
а реализация - закрыто. Мы будем придерживаться этого соглашения при опре-
делении адаптера Text Shape:
Операция BoundingBox преобразует интерфейс Text View к интерфейсу Shape:
void TextShape::BoundingBo x (
Point s bottomLeft, Point & topRigh t
) cons t {
Coor d bottom, left, width, height;
GetOrigin(bottom, left);
GetExtent(width, height);
bottomLef t = Point(bottom, left);
topRigh t = Point(botto m + height, lef t + width);
}
На пример е операци и IsEmpt y демонстрируетс я пряма я переадресаци я за-
просов, общи х дл я обои х классов:
bool TextShape::IsEmpt y () cons t {
retur n TextView::IsEmpty();
}
CreateManipulato r - это пример фабричног о метода.
class TextShape : public Shape, private TextView {
public:
TextShape();
virtual void BoundingBox(
Point& bottomLeft, Points topRight
) const;
virtual bool IsEmptyO const;
virtual Manipulator* CreateManipulator() const;
};
Структурные паттерны
Наконец, мы определим операцию CreateManipulator (отсутствующую
в классе TextView) с нуля. Предположим, класс TextManipulator, который
поддерживает манипуляции с Text Shape, уже реализован:
Manipulator* TextShape::CreateManipulato r () const {
return new TextManipulator(this);
}
Адаптер объектов применяет композицию объектов для объединения классов
с разными интерфейсами. При таком подходе адаптер TextShape содержит ука-
затель на TextView:
clas s TextShap e : publi c Shap e {
public:
TextShape(TextView*);
virtua l voi d BoundingBox (
Point & bottomLeft, Point s topRigh t
} const;
virtua l boo l IsEmpty O const;
virtua l Manipulator * CreateManipulator( ) const;
private:
TextView * _text;
};
Объект TextShape должен инициализировать указатель на экземпляр
TextView. Делается это в конструкторе. Кроме того, он должен вызывать опера-
ции объекта TextView всякий раз, как вызываются его собственные операции.
В этом примере мы предположим, что клиент создает объект TextView и переда-
ет его конструктору класса TextShape:
TextShape::TextShap e (TextView * t ) {
__tex t = t;
}
voi d TextShape::BoundingBo x (
Point s bottomLeft, Point & topRigh t
) cons t {
Coor d bottom, left, width, height;
_text->GetOrigin(bottom, left);
_text->GetExtent(width, height);
bottomLef t = Point(bottom, left);
topRigh t = Point(botto m + height, lef t + width);
}
boo l TextShape::IsEmpt y () cons t {
retur n _text->IsEmpty();
}
Паттер н Adapte r
Реализаци я CreateManipulato r не зависи т от верси и адаптер а класса, по-
скольк у реализован а с нул я и не используе т повторн о никако й функционально -
сти Text View:
Manipulator* TextShape::CreateManipulator () const {
return new TextManipulator(this);
}
Сравни м этот код с кодо м адаптер а класса. Для написани я адаптер а объект а
нужн о потратит ь чут ь больше усилий, но зато он оказываетс я боле е гибким. На-
пример, вариан т адаптер а объект а TextShap e буде т прекрасн о работат ь и с под-
классами Text View: клиент просто передает экземпляр подкласса Text View кон-
структору TextShape.
Известные применения
Пример, приведенны й в раздел е «Мотивация», заимствова н из графическог о
приложени я ET++Draw, основанног о на каркас е ЕТ++ [WGM88]. ET++Dra w по-
вторн о используе т класс ы ЕТ++ для редактировани я текста, применя я для адап-
тации класс TextShape.
В библиотек е Interviews 2.6 определе н абстрактны й клас с Interactor для
таких элементо в пользовательског о интерфейса, как полос ы прокрутки, кнопк и
и меню [VL88]. Есть также абстрактный класс Graphi c для структурированны х
графически х объектов: прямых, окружностей, многоугольнико в и сплайнов.
И Interactor, и Graphic имеют графическо е представление, но у них разны е
интерфейс ы и реализаци и (общи х родительски х классо в нет), и значит, они не-
совместимы: нельз я непосредственн о вложит ь структурированны й графически й
объект, скажем, в диалогово е окно.
Вместо этого Interviews 2.6 определяе т адаптер объектов GraphicBloc k - под-
класс Interactor, которы й содержи т экземпля р Graphic. GraphicBlock адап-
тирует интерфейс класса Graphic к интерфейсу Interactor, позволяет отобра-
жать, прокручиват ь и изменят ь масшта б экземпляр а Graphi c внутр и структур ы
класс а Interactor.
Сменны е адаптер ы широк о применяютс я в систем е ObjectWorks\Smalltal k
[Раг90]. В стандартно м Smalltal k определе н клас с ValueMode l для видов, которы е
отображают единственное значение. ValueMode l определяет интерфейс value,
value: для доступ а к значению. Это абстрактны е методы. Автор ы приложени й об-
ращаютс я к значени ю по имени, более соответствующем у предметно й области, на-
пример width и width:, но они не обязаны порождать от ValueModel подклассы
для адаптаци и таки х зависящи х от приложени я име н к интерфейс у ValueModel.
Вмест о этог о ObjectWorks\Smalltal k включае т подклас с ValueModel, назы-
вающийс я PluggableAdaptor. Объек т этог о класс а адаптируе т други е объект ы
к интерфейсу ValueModel (value, value:). Его можн о параметризоват ь бло-
ками для получени я и установк и нужног о значения. Внутр и PluggableAdapto r
эти блоки используются для реализации интерфейса value, value:. Этот класс
позволяет также передават ь имена селекторов (например, width, width:) не-
посредственно, обеспечива я тем самым некоторо е синтаксическо е удобство. Дан-
ные селектор ы преобразуютс я в соответствующи е блок и автоматически.
Еще один приме р из ObjectWorks\Smalltal k - это клас с TableAdaptor. Он
може т адаптироват ь последовательност ь объекто в к табличном у представлению.
В таблиц е отображаетс я по одном у объект у в строке. Клиен т параметризуе т
TableAdaptor множеством сообщений, которые используются таблицей для по-
лучени я от объект а значени я в колонках.
В некоторы х класса х библиотек и NeXT AppKi t [Add94 ] используютс я объек -
ты-уполномоченны е для реализаци и интерфейс а адаптации. В качеств е пример а
можно привест и класс NXBrowser, который способе н отображат ь иерархически е
списк и данных. NXBrowse r пользуетс я объектом-уполномоченны м для доступ а
и адаптаци и данных.
Придуманна я Скотто м Мейеро м (Scot t Meyer ) конструкци я «брак по расче -
ту» (Marriage of Convenience) [Mey88] это разновидность адаптера класса. Мейер
описывает, как класс FixedStack адаптирует реализацию класса Array к интер-
фейсу класс а Stack. Результато м являетс я стек, содержащи й фиксированно е чис-
ло элементов.
Родственные паттерны
Структур а паттерн а мост аналогичн а структур е адаптера, но у мост а иное
назначение. Он отделяе т интерфейс от реализации, чтобы то и друго е можно было
изменят ь независимо. Адапте р же призва н изменит ь интерфейс существующего
объекта.
Паттер н декорато р расширяе т функциональност ь объекта, изменя я его ин-
терфейс. Таким образом, декорато р более прозраче н для приложения, чем адап-
тер. Как следствие, декорато р поддерживае т рекурсивну ю композицию, что для
«чистых» адаптеров невозможно.
Заместитель определяет представителя или суррогат другого объекта, но не
изменяе т его интерфейс.
Паттер н Bridg e
Название и классификация паттерна
Мост - паттерн, структурирующи й объекты.
Структурные паттерны
Паттер н Bridg e
Назначение
Отделит ь абстракци ю от ее реализаци и так, чтоб ы то и друго е можн о был о
изменят ь независимо.
Известен также под именем
Handle/Bod y (описатель/тело).
Мотивация
Если для некоторо й абстракци и возможн о нескольк о реализаций, то обычн о
применяю т наследование. Абстрактны й клас с определяе т интерфей с абстракции,
а его конкретны е подкласс ы по-разном у реализую т его. Но тако й подхо д не все-
гда обладае т достаточно й гибкостью. Наследовани е жестк о привязывае т реализа -
цию к абстракции, что затрудняе т независиму ю модификацию, расширени е и по-
вторно е использовани е абстракци и и ее реализации.
Рассмотри м реализаци ю переносимо й абстракци и окна в библиотек е для раз -
работк и пользовательски х интерфейсов. Написанны е с ее помощь ю приложени я
должн ы работат ь в разны х средах, наприме р под X Windo w Syste m и Presentatio n
Manage r (PM) от компани и IBM. С помощь ю наследовани я мы могл и бы опреде -
лит ь абстрактны й клас с Windo w и его подкласс ы XWindo w и PMWindow, реализу -
ющи е интерфейс окна для разны х платформ. Но у таког о решени я ест ь два недо -
статка:
а неудобн о распространят ь абстракци ю Windo w на други е вид ы око н или но-
вые платформы. Представьт е себе подклас с IconWindow, которы й специа -
лизируе т абстракци ю окна для пиктограмм. Чтоб ы поддержат ь пиктограм -
мы на обеи х платформах, нам придетс я реализоват ь два новы х подкласс а
XlconWindo w и PMIconWindow. Боле е того, по два подкласс а необходим о
определят ь для каждого вид а окон. А для поддержк и третье й платформ ы
придетс я определят ь для все х видо в око н новы й подклас с Window;
а клиентски й код становитс я платформенно-зависимым. При создани и окна
клиен т инстанцируе т конкретны й класс, имеющи й вполн е определенну ю
реализацию. Например, создава я объек т XWindow, мы привязывае м абстрак -
цию окн а к ее реализаци и для систем ы X Windo w и, следовательно, делае м
код клиент а ориентированны м именн о на эту оконну ю систему. Таки м об-
разом усложняетс я перено с клиент а на други е платформы.
Структурны е паттерн ы
Клиент ы должн ы имет ь возможност ь создават ь окно, не привязываяс ь к кон-
кретно й реализации. Тольк о сама реализаци я окна должн а зависет ь от плат -
формы, на которо й работае т приложение. Поэтом у в клиентско м коде не
може т быт ь никаки х упоминани й о платформах.
С помощь ю паттерн а мост эти проблем ы решаются. Абстракци я окна и ее реа-
лизаци я помещаютс я в раздельны е иерархи и классов. Таким образом, существуе т
одна иерархи я для интерфейсо в окон (Window, IconWindow, TransientWindow )
и друга я (с корне м Windowimp ) - для платформенно-зависимы х реализаций. Так,
подклас с XWindowIm p предоставляе т реализаци ю в систем е X Windo w System.
Все операци и подклассо в Windo w реализован ы в термина х абстрактны х опе-
раций из интерфейс а Windowimp. Это отделяе т абстракци ю окна от различны х
ее платформенно-зависимы х реализаций. Отношени е межд у классам и Windo w
и Windowim p мы буде м называт ь мостом, поскольк у межд у абстракцие й и реали -
зацие й строитс я мост, и они могу т изменятьс я независимо.
Применимость
Используйт е паттер н мост, когда:
а хотит е избежат ь постоянно й привязк и абстракци и к реализации. Так, на-
пример, бывает, когд а реализаци ю необходим о выбират ь во время выполне -
ния программы;
а и абстракции, и реализаци и должн ы расширятьс я новым и подклассами.
В тако м случа е паттер н мос т позволяе т комбинироват ь разны е абстрак -
ции и реализаци и и изменят ь их независимо;
о изменени я в реализаци и абстракци и не должн ы сказыватьс я на клиентах,
то есть клиентски й код не долже н перекомпилироваться;
Паттерн Bridge
а (только для C++!) вы хотит е полность ю скрыт ь от клиенто в реализаци ю аб-
стракции. В C++ представлени е класса видимо через его интерфейс;
а число классо в начинае т быстро расти, как мы видели на первой диаграмм е
из раздела «Мотивация». Это призна к того, что иерархи ю следуе т разделит ь
на две части. Для таких иерархи й классо в Рамбо (Rumbaugh ) используе т
термин «вложенны е обобщения » [RBP+91];
а вы хотит е разделит ь одну реализаци ю между нескольким и объектам и (быть
может, применя я подсчет ссылок), и этот факт необходим о скрыт ь от клиента.
Простой приме р - это разработанны й Джеймсо м Коплиено м класс String
[Сор92], в которо м разные объект ы могут разделят ь одно и то же представ -
ление строки (StringRep).
Структура
Участники
a Abstraction (Window) - абстракция:
- определяе т интерфей с абстракции;
- храни т ссылк у на объект типа Implemen t or;
a RefinedAbstraction (iconWindow) - уточненная абстракция:
- расширяе т интерфейс, определенны й абстракцие й Abstraction;
a Implementor (Windowlmp) - реализатор:
- определяе т интерфей с для классо в реализации. Он не обязан точно соот-
ветствовать интерфейсу класса Abstraction. На самом деле оба ин-
терфейс а могут быть совершенн о различны. Обычно интерфей с класса
Implemento r предоставляе т тольк о примитивны е операции, а класс
Abstractio n определяе т операци и более высоког о уровня, базирующие -
ся на этих примитивах;
a Concretelmplemento r (XWindowlmp, PMWindowlmp ) - конкретны й реа-
лизатор:
- содержи т конкретну ю реализаци ю интерфейс а класса Implementor.
Структурны е паттерны
Отношения
Объект Abstraction перенаправляе т своему объекту Implementor запро-
сы клиента.
Результаты
Результаты применения паттерна мост таковы:
а отделение реализации от интерфейса. Реализация больше не имеет посто-
янной привязки к интерфейсу. Реализацию абстракции можно конфигури-
ровать во время выполнения. Объект может даже динамически изменять
свою реализацию.
Разделение классов Abstraction и Implementor устраняет также зави-
симости от реализации, устанавливаемые на этапе компиляции. Чтобы из-
менить класс реализации, вовсе не обязательно перекомпилироват ь класс
Abstraction и его клиентов. Это свойство особенно важно, если необхо-
димо обеспечить двоичную совместимость между разными версиями биб-
лиотеки классов.
Кроме того, такое разделение облегчает разбиение системы на слои и тем са-
мым позволяет улучшить ее структуру. Высокоуровневые части системы долж-
ны знать только о классах Abstraction и Implementor;
а повышение степени расширяемости. Можно расширять независимо иерар-
хии классов Abstraction и Implementor;
а сокрытие деталей реализации от клиентов. Клиентов можно изолировать
от таких деталей реализации, как разделение объектов класса Implementor
и сопутствующег о механизма подсчета ссылок.
Реализация
Если вы предполагаете применить паттерн мост, то подумайте о таких вопро-
сах реализации:
а только один класс Implementor. В ситуациях, когда есть только одна реализа-
ция, создавать абстрактный класс Implementor необязательно. Это вырож-
денный случай паттерна мост- между классами Abstracti on и Imp-
lementor существует взаимно-однозначно е соответствие. Тем не менее
разделение все же полезно, если нужно, чтобы изменение реализации класса
не отражалось на существующих клиентах (должно быть достаточно заново
скомпоновать программу, не перекомпилиру я клиентский код).
Для описания такого разделения Каролан (Carolan) [Car89] употребляет
сочетание «чеширский кот». В C++ интерфейс класса Implementor мож-
но определить в закрытом заголовочном файле, который не передается кли-
ентам. Это позволяет полностью скрыть реализацию класса от клиентов;
а создание правильного объекта Implementor. Как, когда и где принимается ре-
шение о том, какой из нескольких классов Implemento r инстанцировать?
Если у класса Abst ract i o n есть информация о конкретных классах
Concretelmplementor, то он может инстанцировать один из них в своем кон-
структоре; какой именно - зависит от переданных конструктору параметров.
Паттер н Bridge
Так, если класс коллекци и поддерживае т нескольк о реализаций, то реше -
ние можн о принят ь в зависимост и от размер а коллекции. Для небольши х
коллекци й применяетс я реализаци я в виде связанног о списка, для боль -
ших - в виде хэшированны х таблиц.
Друго й подхо д - заране е выбрат ь реализаци ю по умолчанию, а позже изме -
нять ее в соответстви и с тем, как она используется. Например, если числ о
элементо в в коллекци и становитс я больше некоторо й условно й величины, то
мы переключаемс я с одно й реализаци и на другую, боле е эффективную.
Можн о также делегироват ь решени е другом у объекту. В пример е с иерархия -
ми Window/Windowlm p уместн о было бы ввест и фабричны й объек т (см. пат-
терн абстрактна я фабрика), единственна я задач а которог о - инкапсулиро -
вать платформенну ю специфику. Фабрик а обладае т информацией, объект ы
Windowlm p каког о вида надо создават ь для данно й платформы, а объек т
Windo w прост о обращаетс я к ней с запросо м о предоставлени и какого-ни -
будь объект а Windowlmp, при этом понятно, что объек т получи т то, что нуж-
но. Преимуществ о описанног о подхода: клас с Abstractio n напряму ю не
привяза н ни к одном у из классо в Imp lamen t or;
а разделение реализаторов. Джейм с Коплие н показал, как в C++ можн о приме -
нить идиом у описатель/тело, чтобы нескольким и объектам и могл а совмест -
но использоватьс я одна и та же реализаци я [Сор92]. В теле хранитс я счет -
чик ссылок, которы й увеличиваетс я и уменьшаетс я в класс е описателя. Код
для присваивани я значени й описателям, разделяющи м одно тело, в обще м
виде выгляди т так:
Handles Handle::operator= (const Handles other) {
other._body->Ref();
_body->Unref();
if (_body->RefCount() == 0) {
delete _body;
_body = other._body;
return *this;
Структурные паттерны
class Window {
public:
Window(View* contents);
// запросы, обрабатываемые окном
virtual void DrawContents();
virtual void Open();
virtual void Close();
virtual void IconifyO,-
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 Points, const Point&);
virtual void DrawRect(const Point&, const Point&);
virtual void DrawPolygon(const Point[], int n);
virtual void DrawText(const char*, const Point&);
protected:
Windowlmp* GetWindowImp();
View* GetViewO ;
private:
Windowlmp* _imp;
View* _contents; // содержимое окна
};
В классе Window хранится ссылка на Windowlmp - абстрактный класс, в ко-
тором объявлен интерфейс к данной оконной системе:
class Windowlmp {
public:
virtual void ImpTopO = 0;
virtual void ImpBottomO = 0;
virtual void ImpSetExtent(const Point&) = 0;
virtual void ImpSetOrigin(const Points) = 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:
Windowlmp();
};
Подкласс ы Windo w определяют различные виды окон, как то: окно приложения,
пиктограмма, временное диалоговое окно, плавающая палитра инструменто в и т.д.
Паттерн Bridge
Например, класс ApplicationWindo w реализуе т операци ю DrawContent s
для отрисовк и содержимог о экземпляр а класса View, который в нем хранится:
class ApplicationWindo w : public Windo w {
public:
// ...
virtua l voi d DrawContents();
};
voi d ApplicationWindow::DrawContent s () {
GetView O ->DrawOn(this ) ;
}
А в классе IconWindo w содержитс я имя растровог о изображени я для пикто-
граммы
clas s IconWindo w : publi c Windo w {
public:
// ...
virtua l voi d DrawContents();
private:
cons t char * _bitmapName;
};
и реализаци я операции DrawContent s для рисовани я этого изображени я в окне:
voi d IconWindow::DrawContents( ) {
Windowlmp * im p = GetWindowImp();
if (imp != 0) {
imp->DeviceBitmap(_bitmapName, 0.0, 0.0);
}
}
Могут существоват ь и другие разновидност и класса Window. Окну класса
TransientWindow иногда необходимо как-то сообщаться с создавшим его ок-
ном во время диалога, поэтому в объект е класса хранитс я ссылка на создателя.
Окно класса PaletteWindo w всегда располагаетс я поверх других. Окно класса
ZconDockWindo w (контейне р пиктограмм ) хранит окна класса IconWindo w и рас-
полагает их в ряд.
Операции класса Windo w определен ы в термина х интерфейс а Windowlmp.
Например, DrawRec t вычисляе т координат ы по двум своим параметра м Point
перед тем, как вызват ь операци ю Windowlmp, котора я рисует в окне прямо -
угольник:
void Window: : DrawRec t (const Point & pi, const Points p2) {
Windowlmp * imp = GetWindowIm p ( ) ;
imp->DeviceRect(pl.X( ) , pl.YO, p 2.X( ), p 2.Y ( ) );
}
Конкретны е подкласс ы Windowlm p поддерживаю т разные оконные системы.
Так, класс XWindowIm p ориентирова н на систему X Window:
Структурны е паттерн ы
class XWindowIm p : public Windowlm p {
public:
XWindowImp();
virtual void DeviceRect(Coord, Coord, Coord, Coord);
// прочие операци и открытог о интерфейса...
private:
// переменные, описывающи е специфично е для X Window состояние,
// в том числе:
Display* _dpy;
Drawable _winid; // идентификато р окна
GC _gc; // графически й контекс т окна
};
Для Presentatio n Manage r (РМ) мы определяе м класс PMWindowImp:
class PMWindowIm p : public Windowlm p {
public:
PMWindowIm p () ;
virtual void DeviceRect(Coord, Coord, Coord, Coord);
// прочие операци и открытог о интерфейса...
private:
// переменные, описывающи е специфично е для РМ Window состояние,
// в том числе:
HPS _hps;
};
Эти подклассы реализуют операции Windowlmp в терминах примитивов
оконной системы. Например, DeviceRect для X Window реализуется так:
void XWindowImp::DeviceRec t (
Coor d xO, Coor d yO, Coor d xl, Coor d yl
) {
int x = round(mi n(xO, xl ) );
int у = rpund(min(yO, yl ) );
int w = round(abs(x O - xl ) };
int h = round(abs(y O - y l ) );
XDrawRectangle(_dpy, _winid, _gc, x, y, w, h);
}
А для РМ - так:
void PMWindowImp::DeviceRec t (
Coord xO, Coord yO, Coord xl, Coord yl
) {
Coord left = min(xO, xl) ;
Coord right = max(xO, xl);
Coord bottom = min(yO, yl) ;
Coord top = max(yO, yl);
PPOINTL point[4];
Паттер н Bridg e
point[0].x = left; point[0].у = top;
point[1].x = right; point[1].у = top;
point[2].x = right; point[2].у = bottom;
point[3].x = left; point[3].у = bottom;
if (
(GpiBeginPath(_hps, 1L) == false) I I
(GpiSetCurrentPosition(_hps, &point[3]) = = false) I I
(GpiPolyLine(_hps, 4L, point) == GPI_ERROR) II
(GpiEndPath(_hps ) == false)
) {
// сообщить об ошибке
} else {
GpiStrokePath(_hps, 1L, OL);
}
}
Как окно получает экземпляр нужного подкласса Windowlmp? В данном приме-
ре мы предположим, что за это отвечает класс Window. Его операция GetWindowImp
получает подходящий экземпляр от абстрактно й фабрики (см. описание паттерна
абстрактна я фабрика), которая инкапсулируе т все зависимост и от оконной сис-
темы.
Windowlmp * Window: : GetWindowIm p ( ) {
if (_imp == 0) {
_imp = WindowSystemFactor y :: Instanc e () ->MakeWindowImp ( );
}
return _imp;
WindowSystemFactory : : Instance ( ) возвращает абстрактную фабрику, ко-
торая изготавливае т все системно-зависимы е объекты. Для простоты мы сделали
эту фабрику одиночко й и позволили классу Window обращатьс я к ней напрямую.
Известные применения
Пример класса Windo w позаимствова н из ЕТ++ [WGM88]. В ЕТ++ класс
Windowlmp называетс я WindowPor t и имеет такие подклассы, как XWindowPor t
и SunWindowPort. Объект Windo w создает соответствующег о себе реализатор а
Implementor, запрашива я его у абстрактно й фабрики, которая называетс я
WindowSystem. Эта фабрика предоставляе т интерфейс для создания платфор-
менно-зависимы х объектов: шрифтов, курсоров, растровых изображени й и т.д.
Дизайн классов Window/WindowPor t в ЕТ++ обобщает паттерн мост в том
отношении, что WindowPor t сохраняет также обратную ссылку на Window. Класс-
реализатор WindowPor t используе т эту ссылку для извещения Windo w о собы-
тиях, специфичных для WindowPort: поступлени й событий ввода, изменения х
размера окна и т.д.
В работах Джеймса Коплиена [Сор92] и Бьерна Страуструпа [Str91] упоми-
наются классы описателе й и приводятс я некоторые примеры. Основной акцент
Структурны е паттерны
в этих примерах сделан на вопросах управления памятью, например разделении
представления строк и поддержке объектов переменного размера. Нас же в пер-
вую очередь интересует поддержка независимых расширений абстракции и ее
реализации.
В библиотеке libg++ [Lea88] определены классы, которые реализуют универ-
сальные структуры данных: Set (множество), LinkedSet (множество как связан-
ный список), HashSet (множество какхэш-таблица), LihkedList (связанный спи-
сок) и HashTabl e (хэш-таблица). Set - это абстрактный класс, определяющий
абстракцию множества, a LinkedList и HashTable - конкретные реализации
связанного списка и хэш-таблицы. LinkedSet и HashSet - реализаторы абстрак-
ции Set, перекидывающие мост между Set и LinkedList и HashTable соот-
ветственно. Перед вами пример вырожденног о моста, поскольку абстрактног о
класса Implement or здесь нет.
В библиотеке NeXT AppKi t [Add94] паттерн мост используется при реализа-
ции и отображении графических изображений. Рисунок может быть представлен
по-разному. Оптимальный способ его отображения на экране зависит от свойств
дисплея и прежде всего от числа цветов и разрешения. Если бы не AppKit, то для
каждого приложения разработчикам пришлось бы самостоятельно выяснять, какой
реализацией пользоваться в конкретных условиях.
AppKit предоставляет мост NXImage/NXImageRep. Класс NXImage определяет
интерфейс для обработки изображений. Реализация же определена в отдельной иерар-
хии классов NXImageRep, в которой есть такие подклассы, как NXEPSImageRep,
NXCachedlmageRe p и NXBitMapImageRep. В классе NXImage хранятся ссылки
на один или более объектов NXImageRep. Если имеется более одной реализации
изображения, то NXImage выбирает самую подходящую для данного дисплея. При
необходимости NXImage также может преобразовать изображение из одного фор-
мата в другой. Интересная особенность этого варианта моста в том, что NXImage
может одновременно хранить несколько реализаций NXImageRep.
Родственные паттерны
Паттерн абстрактная фабрика может создать и сконфигурироват ь мост.
Для обеспечения совместной работы не связанных между собой классов преж-
де всего предназначен паттерн адаптер. Обычно он применяется в уже готовых
системах. Мост же участвует в проекте с самого начала и призван поддержать воз-
можность независимог о изменения абстракций и их реализаций.
Паттер н Composit e
Название и классификация паттерна
Компоновщик - паттерн, структурирующи й объекты.
Назначение
Компонует объекты в древовидные структуры для представления иерархий
часть-целое. Позволяет клиентам единообразно трактовать индивидуальные и со-
ставные объекты.
Паттер н Composit e
Мотивация
Такие приложения, как графически е редактор ы и редактор ы электрически х
схем, позволяют пользователя м строит ь сложные диаграмм ы из более простых
компонентов. Проектировщи к может сгруппироват ь мелкие компонент ы для фор-
мировани я более крупных, которые, в свою очередь, могут стать основой для со-
здания еще более крупных. В простой реализации допустимо было бы определить
классы графически х примитивов, наприме р текста и линий, а также классы, вы-
ступающи е в роли контейнеро в для этих примитивов.
Но у таког о решения есть существенны й недостаток. Программа, в которой
эти классы используются, должна по-разном у обращатьс я с примитивам и и кон-
тейнерами, хотя пользовател ь чаще всего работае т с ними единообразно. Необхо-
димост ь различат ь эти объект ы усложняе т приложение. Паттерн компоновщи к
описывает, как можно применит ь рекурсивну ю композици ю таким образом, что
клиент у не придетс я проводит ь различие между простыми и составным и объек-
тами.
Ключом к паттерн у компоновщи к являетс я абстрактны й класс, который
представляе т одновременно и примитивы, и контейнеры. В графическо й системе
этот класс может называтьс я Graphic. В нем объявлен ы операции, специфичны е
для каждого вида графического объекта (такие как Draw) и общие для всех со-
ставных объектов, наприме р операции для доступа и управлени я потомками.
Подклассы Line, Rectangle и Text (см. диаграмму выше) определяют при-
митивные графически е объекты. В них операция Draw реализован а соответствен -
но для рисовани я прямых, прямоугольнико в и текста. Поскольк у у примитивны х
объектов нет потомков, то ни один из этих подклассо в не реализуе т операции,
относящиес я к управлени ю потомками.
Класс Picture определяет агрегат, состоящий из объектов Graphic. Реали-
зованна я в нем операци я Draw вызывае т одноименну ю функци ю для каждог о
потомка, а операции для работы с потомкам и уже не пусты. Поскольк у интерфей с
класса Picture соответствует интерфейсу Graphic, то в состав объекта Picture
могут входит ь и другие такие же объекты.
Структурные паттерны
Ниже на диаграмме показана типичная структура составного объекта, рекур-
сивно скомпонованного из объектов класса Graphic.
Применимость
Используйте паттерн компоновщик, когда:
а нужно представить иерархию объектов вида часть-целое;
а хотите, чтобы клиенты единообразно трактовали составные и индивидуаль-
ные объекты.
Структура
Структура типичного составного объекта могла бы выглядеть так:
Паттерн Composite
Участники
Э Component (Graphic ) - компонент:
- объявляе т интерфейс для компонуемых объектов;
- предоставляе т подходящу ю реализацию операций по умолчанию, общую
для всех классов;
- объявляе т интерфейс для доступа к потомкам и управления ими;
- определяе т интерфейс для доступа к родителю компонент а в рекурсив-
ной структуре и при необходимост и реализует его. Описанна я возмож-
ность необязательна;
a Leuf (Rectangle, Line, Text, и т.п.) - лист:
- представляе т листовые узлы композици и и не имеет потомков;
- определяе т поведение примитивных объектов в композиции;
a Composite (Picture) - составной объект:
- определяе т поведение компонентов, у которых есть потомки;
- хранит компоненты-потомки;
- реализует относящиес я к управлению потомками операции в интерфей-
се клдсса Component;
a Client - клиент:
- манипулируе т объектами композици и через интерфейс Component.
Отношения
Клиенты используют интерфейс класса Component для взаимодействи я с объек-
тами в составной структуре. Если получателе м запроса является листовый объект
Leaf, то он и обрабатывае т запрос. Когда же получателе м является составной объект
Composite, то обычно он перенаправляе т запрос своим потомкам, возможно, вы-
полняя некоторые дополнительные операции до или после перенаправления.
Результаты
Паттерн компоновщик:
а определяет иерархии классов, состоящие из примитивных и составных объ-
ектов. Из примитивных объектов можно составлят ь более сложные, кото-
рые, в свою очередь, участвуют в более сложных композиция х и так далее.
Любой клиент, ожидающий примитивног о объекта, может работать и с со-
ставным;
а упрощает архитектуру клиента. Клиент ы могут единообразн о работать
с индивидуальным и и объектами и с составными структурами. Обычно
клиенту неизвестно, взаимодействуе т ли он с листовым или составным
объектом. Это упрощае т код клиента, поскольк у нет необходимост и пи-
сать функции, ветвящиес я в зависимост и от того, с объектом какого клас-
са они работают;
а облегчает добавление новых видов компонентов. Новые подкласс ы классов
Composit e или Leaf будут автоматическ и работать с уже существующи -
ми структурами и клиентским кодом. Изменять клиента при добавлении но-
вых компоненто в не нужно;
* Структурны е паттерн ы
а способствует созданию общего дизайна. Однако такая простота добавле-
ния новых компонентов имеет и свои отрицательные стороны: становится
трудно наложить ограничения на то, какие объекты могут входить в со-
став композиции. Иногда желательно, чтобы составной объект мог вклю-
чать только определенные виды компонентов. Паттерн компоновщик не
позволяет воспользоваться для реализации таких ограничений статичес-
кой системой типов. Вместо этого следует проводить проверки во время
выполнения.
Реализация
При реализации паттерна компоновщик приходится рассматривать много
вопросов:
а явные ссылки на родителей. Хранение в компоненте ссылки на своего роди-
теля может упростить обход структуры и управление ею. Наличие такой
ссылки облегчает передвижение вверх по структуре и удаление компонен-
та. Кроме того, ссылки на родителей помогают поддержать паттерн цепоч-
ка обязанностей.
Обычно ссылку на родителя определяют в классе Component. Классы Leaf
и Composite могут унаследовать саму ссылку и операции с ней.
При наличии ссылки на родителя важно поддерживать следующий инвари-
ант: если некоторый объект в составной структуре ссылается на другой со-
ставной объект как на своего родителя, то для последнего первый является
потомком. Простейший способ гарантировать соблюдение этого условия -
изменять родителя компонента только тогда, когда он добавляется или уда-
ляется из составного объекта. Если это удается один раз реализовать в опе-
рациях Add и Remove, то реализация будет унаследована всеми подкласса-
ми и, значит, инвариант будет поддерживаться автоматически;
а разделение компонентов. Часто бывает полезно разделять компоненты, на-
пример для уменьшения объема занимаемой памяти. Но если у компонента
может быть более одного родителя, то разделение становится проблемой.
Возможное решение - позволить компонентам хранить ссылки на несколь-
ких родителей. Однако в таком случае при распространении запроса по
структуре могут возникнуть неоднозначности. Паттерн приспособленец
показывает, как следует изменить дизайн, чтобы вовсе отказаться от хране-
ния родителей. Работает он в тех случаях, когда потомки могут не посылать
сообщений своим родителям, вынеся за свои границы часть внутреннего со-
стояния;
а максимизация интерфейса класса Component. Одна из целей паттерна ком-
поновщик - избавить клиентов от необходимости знать, работают ли они
с листовым или составным объектом. Для достижения этой цели класс
Component должен сделать как можно больше операций общими для клас-
сов Composite и Leaf. Обычно класс Component предоставляет для этих
операций реализации по умолчанию, а подклассы Composite и Leaf заме-
щают их.
Паттер н Composit e
Однако иногда эта цель вступает в конфликт с принципом проектирования
иерархии классов, согласно которому класс должен определять только ло-
гичные для всех его подклассах операции. Класс Component поддерживает
много операций, не имеющих смысла для класса Leaf. Как же тогда предоста-
вить для них реализацию по умолчанию?
Иногда, проявив изобретательность, удается перенести в класс Component
операцию, которая, на первый взгляд, имеет смысл только для составных
объектов. Например, интерфейс для доступа к потомкам является фунда-
ментальной частью класса Composite, но вовсе не обязательно класса Leaf.
Однако если рассматривать Leaf как Component, у которого никогда не бы-
вает потомков, то мы можем определить в классе Component операцию до-
ступа к потомкам как никогда не возвращающую потомков. Тогда подклас-
сы Leaf могут использовать эту реализацию по умолчанию, а в подклассах
Composite она будет переопределена, чтобы возвращать потомков.
Операции для управления потомками довольно хлопотны, мы обсудим их
в следующем разделе;
а объявление операций для управления потомками. Хотя в классе Composite
реализованы операции Add и Remove для добавления и удаления потомков,
но для паттерна компоновщик важно, в каких классах эти операции объяв-
лены. Надо ли объявлять их в классе Component и тем самым делать до-
ступными в Leaf, или их следует объявить и определить только в классе
Composite и его подклассах?
Решая этот вопрос, мы должны выбирать между безопасностью и прозрач-
ностью:
- если определить интерфейс для управления потомками в корне иерархии
классов, то мы добиваемся прозрачности, так как все компоненты удает-
ся трактовать единообразно. Однако расплачиваться приходится безопас-
ностью, поскольку клиент может попытаться выполнить бессмысленное
действие, например добавить или удалить объект из листового узла;
- если управление потомками сделать частью класса Composite, то безо-
пасность удастся обеспечить, ведь любая попытка добавить или удалить
объекты из листьев в статически типизированном языке вроде C++ бу-
дет перехвачена на этапе компиляции. Но прозрачность мы утрачиваем,
ибо у листовых и составных объектов оказываются разные интерфейсы.
В паттерне компоновщик мы придаем особое значение прозрачности, а не
безопасности. Если для вас важнее безопасность, будьте готовы к тому, что
иногда вы можете потерять информацию о типе и придется преобразовы-
вать компонент к типу составного объекта. Как это сделать, не прибегая
к небезопасным приведениям типов?
Можно, например, объявить в классе Component операцию Composite*
GetComposit e ( ). Класс Component реализует ее по умолчанию, возвра-
щая нулевой указатель. А в классе Composite эта операция переопределе-
на и возвращает указатель this на сам объект:
Структурны е паттерн ы
Аналогичные проверки на принадлежность классу Composite в C++ вы-
полняют и с помощью оператора dynamic_cast.
Разумеется, при таком подходе мы не обращаемся со всеми компонентами
единообразно, что плохо. Снова приходится проверять тип, перед тем как
предпринять то или иное действие.
Единственный способ обеспечить прозрачность - это включить в класс
Component реализации операций Add и Remove по умолчанию. Но появит-
ся новая проблема: нельзя реализовать Component : : Add так, чтобы она
никогда не приводила к ошибке. Можно, конечно, сделать данную опера-
цию пустой, но тогда нарушается важное проектное ограничение; попытка
class Composite;
class Component {
public:
//...
virtua l Composite * GetComposite( ) { retur n 0; }
};
clas s Composit e : publi c Componen t {
public:
void Add(Component*);
// ...
virtua l Composite * GetComposite( } { retur n this; }
};
class Leaf : publi c Componen t {
// ...
};
Благодаря операции Get Composite можно спросить у компонента, явля-
ется ли он составным. К возвращаемому этой операцией составному объек-
ту допустимо безопасно применять операции Add и Remove:
Composite * aComposit e = new Composite;
Leaf * aLea f = new Leaf;
Componen t * aComponent;
Composite * test;
aComponen t = aComposite;
if (test = aComponent->GetComposite()) {
test->Add(new Leaf );
}
aComponen t = aLeaf;
if (test = aComponent->GetComposite() ) {
test->Add(ne w Leaf); // не добави т лис т
}
Паттер н Composit e
добавить что-то в листовый объект, скорее всего, свидетельствуе т об ошиб-
ке. Допустимо было бы заставить ее удалять свой аргумент, но клиент мо-
жет быть не рассчитанным на это.
Обычно лучшим решением является такая реализация Add и Remove по
умолчанию, при которой они завершаются с ошибкой (возможно, возбуж-
дая исключение), если компоненту не разрешено иметь потомков (для Add)
или аргумент не является чьим-либо потомком (для Remove).
Другая возможность - слегка изменить семантику операции «удаления».
Если компонент хранит ссылку на родителя, то можно было бы считать, что
Component: : Remove удаляет самого себя. Но для операции Add по-преж-
нему нет разумной интерпретации;
а должен ли Component реализовывать список компонентов. Может возник-
нуть желание определить множество потомков в виде переменной экземп-
ляра класса Component, в котором объявлены операции доступа и управле-
ния потомками. Но размещение указателя на потомков в базовом классе
приводит к непроизводительном у расходу памяти во всех листовых узлах,
хотя у листа потомков быть не может. Такой прием можно применить, толь-
ко если в структуре не слишком много потомков;
а упорядочение потомков. Во многих случаях порядок следования потомков
составного объекта важен. В рассмотренном выше примере класса Graphi c
под порядком может пониматься Z-порядок расположения потомков. В со-
ставных объектах, описывающих деревья синтаксическог о разбора, состав-
ные операторы могут быть экземплярами класса Composite, порядок сле-
дования потомков которых отражает семантику программы.
Если порядок следования потомков важен, необходимо учитывать его при
проектировании интерфейсов доступа и управления потомками. В этом
может помочь паттерн итератор;
а кэширование для повышения производительности. Если приходится часто
обходить композицию или производить в ней поиск, то класс Composit e мо-
жет кэшировать информацию об обходе и поиске. Кэшироват ь разрешает-
ся либо полученные результаты, либо только информацию, достаточную
для ускорения обхода или поиска. Например, класс Picture из примера,
приведенног о в разделе «Мотивация», мог бы кэшировать охватывающие
прямоугольники своих потомков. При рисовании или выборе эта инфор-
мация позволила бы пропускать тех потомков, которые не видимы в теку-
щем окне.
Любое изменение компонента должно делать кэши всех его родителей не-
действительными. Наиболее эффективен такой подход в случае, когда ком-
понентам известно об их родителях. Поэтому, если вы решите воспользо-
ваться кэшированием, необходимо определить интерфейс, позволяющий
уведомить составные объекты о недействительност и их кэшей;
а кто должен удалять компоненты. В языках, где нет сборщика мусора, луч-
ше всего поручить классу Composit e удалять своих потомков в момент
уничтожения. Исключением из этого правила является случай, когда лис-
товые объекты постоянны и; следовательно, могут разделяться;
_ •» _ Структурные паттерны
а какая структура данных лучше всего подходит для хранения компонентов.
Составные объекты могут хранить своих потомков в самых разных структу-
рах данных, включая связанные списки, деревья, массивы и хэш-таблицы.
Выбор структуры данных определяется, как всегда, эффективностью. Соб-
ственно говоря, вовсе не обязательно пользоваться какой-либо из универ-
сальных структур. Иногда в составных объектах каждый потомок представ-
ляется отдельной переменной. Правда, для этого каждый подкласс Composite
должен реализовывать свой собственный интерфейс управления памятью.
См. пример в описании паттерна интерпретатор.
Пример кода
Такие изделия, как компьютеры и стереокомпоненты, часто имеют иерархичес-
кую структуру. Например, в раме монтируются дисковые накопители и плоские
электронные платы, к шине подсоединяются различные карты, а корпус содержит
раму, шины и т.д. Подобные структуры моделируются с помощью паттерна ком-
поновщик.
Класс Equipment определяет интерфейс для всех видов аппаратуры в иерар-
хии вида часть-целое:
class Equipment {
public:
virtual -Equipment ( ) ;
const char* NameO { return _name; }
virtual Watt Power ();
virtual Currency NetPrice();
virtual Currency DiscountPrice ( ) ;
virtual void Add ( Equipment *);
virtual void Remove (Equipment*) ;
virtual Iterator<Equipment*>* Createlterator ();
protected:
Equipment (const char*);
private:
const char* _name;
};
В классе Equipment объявлены операции, которые возвращают атрибуты ап-
паратного блока, например энергопотребление и стоимость. Подклассы реализу-
ют эти операции для конкретных видов оборудования. Класс Equipment объяв-
ляет также операцию Createlterator, возвращающую итератор Iterator (см.
приложение С) для доступа к отдельным частям. Реализация этой операции по
умолчанию возвращает итератор Null Iterator, умеющий обходить только пус-
тое множество.
Среди подклассов Equipment могут быть листовые классы, представляющие
дисковые накопители, СБИС и переключатели:
Паттерн Composite
class FloppyDisk : public Equipment {
public:
FloppyDisk(const char*);
virtual -FloppyDisk();
virtual Watt Power();
virtual Currency NetPriceO;
virtual Currency DiscountPrice();
};
CompositeEquipmen t - это базовый класс для оборудования, содержащег о
друго е оборудование. Одновременн о это подклас с класс а Equipment:
class CompositeEquipment : public Equipment {
public:
virtual -CompositeEquipment();
virtual Watt Power();
virtual Currency NetPriceO;
virtual Currency DiscountPrice();
virtual void Add(Equipment*);
virtual void Remove(Equipment*);
virtual Iterator<Equipment*>* Createlterator();
protected:
CompositeEquipment(const char*);
private:
List<Equipment*> „equipment;
};
CompositeEquipmen t определяе т операци и для доступ а и управлени я внут -
ренним и аппаратным и блоками. Операци и Add и Remov e добавляю т и удаляют обо-
рудовани е из списка, хранящегос я в переменной-член е _equipment. Операци я
Createlterator возвращает итерато р (точнее, экземпля р класса List Iterator),
который будет обходит ь этот список.
Подразумеваема я реализация операции Net Price могла бы использоват ь
Createlterator для суммировани я цен на отдельны е блоки:1
Currency CompositeEquipment::NetPrice () {
Iterator<Equipment*>* i = Creat el t erat or();
Currency total = 0;
for ( i ->Fi r s t ( ); !i ->IsDone(); i ->Next ( )) {
total += i->CurrentItem()->NetPrice();
}
delete i;
return total;
}
1 Очень легко забыт ь об удалени и итератор а после завершени я работ ы с ним. При обсуждени и паттер -
на итерато р рассказано, как защититьс я от таких ошибок.
_^ ч Структурные паттерны
Теперь мы можем представить аппаратный блок компьютера в виде подкласса
к CompositeEquipment под названием Chassis. Chassis наследует порожден-
ные операции класса CompositeEquipment.
class Chassis : public CompositeEquipmen t {
public:
Chassis(const char*);
virtual -Chassi s();
vi rtual Wat t Power();
virtual Currency Net Pri ceO;
virtual Currency Di scount Pri ce();
};
Мы можем аналогично определить и другие контейнеры для оборудования,
например Cabinet (корпус) и Bus (шина). Этого вполне достаточно для сборки
из отдельных блоков довольно простого персональног о компьютера:
Cabinet* cabinet = new Cabi net ("PC Cabi net");
Chassis* chassis = new Chassis("PC Chassi s");
cabinet->Add(chassis);
Bus* bus = new Bus("MCA Bus");
bus->Add(ne w Card("16Mb s Token Ri ng") );
chassis->Add(bus);
chassis->Add(new Fl oppyDi sk("3.Sin Fl oppy") );
cout « "Полная стоимость равна " « chassis->NetPrice() « endl;
Известные применения
Примеры паттерна компоновщик можно найти почти во всех объектно-ориен-
тированных системах. Первоначально класс View в схеме модель/вид/контрол -
лер в языке Smalltalk [KP88] был компоновщиком, и почти все библиотеки для
построения пользовательских интерфейсов и каркасы проектировались аналогич-
но. Среди них ЕТ++ (со своей библиотекой VObjects [WGM88]) и Interviews (клас-
сы Styles [LCI+92], Graphics [VL88] и Glyphs [CL90]). Интересно отметить, что
первоначально вид View имел несколько подвидов, то есть он был одновременно
и классом Component, и классом Composite. В версии 4.0 языка Smalltalk-80 схема
модель/вид/контролле р была пересмотрена, в нее ввели класс Visual-Component,
подклассами которого являлись View и CompositeView.
В каркасе для построения компиляторов RTL, который написан на Smalltal k
[JML92], паттерн компоновщик используется очень широко. RTLExpressio n -
это разновидность класса Component для построения деревьев синтаксическог о
разбора. У него есть подклассы, например Binary Express ion, потомками кото-
рого являются объекты класса RTLExpression. В совокупности эти классы опре-
деляют составную структуру для деревьев разбора. RegisterTransf'er - класс
Паттерн Decorator
Component для промежуточно й формы представлени я программы SSA (Singl e
Static Assignment). Листовые подкласс^ RegisterTransf er определяют различ-
ные статические присваивания, например:
а примитивные присваивания, которые выполняют операцию над двумя ре-
гистрами и сохраняют результат в третьем;
а присваивание, у которого есть исходный, но нет целевого регистра. Следо-
вательно, регистр используетс я после возврата из процедуры;
а присваивание, у которого есть целевой, но нет исходног о регистра. Это озна-
чает, что присваивани е регистру происходит перед началом процедуры.
Подкласс RegisterTransf erSet является примером класса Composite для
представлени я присваиваний, изменяющи х сразу несколько регистров.
Другой пример применени я паттерна компоновщи к - финансовые програм-
мы, когда инвестиционный портфель состоит их нескольких отдельных активов.
Можно поддержат ь сложные агрегаты активов, £сли реализоват ь портфель в виде
компоновщика, согласованног о с интерфейсо м каждого актива [ВЕ93].
Паттерн команда описывает, как можно компоноват ь и упорядочиват ь объек-
ты Command с помощью класса компоновщик а MacroCommand.
Родственные паттерны
Отношение компонент-родител ь используетс я в паттерне цепочка обязан-
ностей.
Паттерн декоратор часто применяетс я совместно с компоновщиком. Когда
декораторы и компоновщик и используютс я вместе, у них обычно бывает общий
родительский класс. Поэтому декоратора м придется поддержат ь интерфейс ком-
понентов такими операциями, как Add, Remove и GetChild.
Паттерн приспособлене ц позволяет разделять компоненты, но ссылаться на
своих родителей они уже не могут.
Итератор можно использоват ь для обхода составных объектов.
Посетитель локализуе т операции и поведение, которые в противном случае
пришлось бы распределят ь между классами Composit e и Leaf.
Паттер н Decorato r
Название и классификация паттерна
Декоратор - паттерн, структурирующи й объекты.
Назначение
Динамическ и добавляет объекту новые обязанности. Является гибкой альтер-
нативой порождению подклассов с целью расширения функциональности.
Известен также под именем
Wrapper (обертка).
Структурны е паттерн ы
Мотивация
Иногд а бывае т нужн о возложит ь дополнительны е обязанност и на отдельны й
объект, а не на клас с в целом. Так, библиотек а для построени я графически х ин-
терфейсо в пользовател я должн а «уметь » добавлят ь ново е свойство, скажем, рам-
ку или ново е поведени е (например, возможност ь прокрутк и к любом у элемент у
интерфейса).
Добавит ь новые обязанност и допустим о с помощь ю наследования. При насле-
довани и класс у с рамко й вокру г каждог о экземпляр а подкласс а буде т рисоватьс я
рамка. Однак о это решени е статическое, а значит, недостаточн о гибкое. Клиен т не
може т управлят ь оформление м компонент а рамкой.
Более гибки м являетс я друго й подход: поместит ь компонен т в друго й объект,
называемы й декоратором, которы й как раз и добавляе т рамку. Декорато р следу -
ет интерфейс у декорируемог о объекта, поэтом у его присутстви е прозрачн о для
клиенто в компонента. Декорато р переадресуе т запрос ы внутреннем у компонен -
ту, но може т выполнят ь и дополнительны е действи я (например, рисоват ь рамку )
до или посл е переадресации. Поскольк у декоратор ы прозрачны, они могу т вкла -
дыватьс я дру г в друга, добавля я тем самым любо е числ о новых обязанностей.
Предположим, что имеетс я объек т класс а Tex t View, которы й отображае т
текст в окне. По умолчани ю Tex t Vie w не имее т поло с прокрутки, поскольк у они
не всегд а нужны. Но при необходимост и их удастс я добавит ь с помощь ю декоратор а
ScrollDecorator. Допустим, что еще мы хоти м добавит ь жирну ю сплошну ю
рамку вокру г объект а TextView. Здес ь може т помоч ь декорато р BorderDecora t or.
Мы прост о компонуе м оба декоратор а с BorderDecorato r и получае м искомы й
результат.
Ниже на диаграмм е показано, как композици я объект а TextVie w с объекта -
ми BorderDecorator и ScrollDecorator порождает элемент для ввода текс-
та, окруженны й рамко й и снабженны й полосо й прокрутки.
Паттер н Decorato r
Классы ScrollDecorator и BorderDecorator являются подклассами
Decorato r - абстрактног о класса, которы й представляе т визуальны е компонен -
ты, применяемы е для оформлени я други х визуальны х компонентов.
VisualComponen t - это абстрактны й клас с для представлени я визуальны х
объектов. В нем определе н интерфей с для рисовани я и обработк и событий. Отме -
тим, что клас с Decorato r прост о переадресуе т запрос ы на рисовани е своем у ком-
поненту, а его подкласс ы могу т расширят ь эту операцию.
Подкласс ы Decorato r могу т добавлят ь любы е операци и для обеспечени я необ -
ходимой функциональности. Так, операция ScrollTo объекта ScrollDecorator
позволяе т други м объекта м выполнят ь прокрутку, если им известн о о присут -
ствии объект а ScrollDecorator. Важна я особенност ь этог о паттерн а состои т
в том, что декоратор ы могу т употреблятьс я везде, где возможн о появлени е самог о
объект а VisualComponent. Поэтом у клиен т не може т отличит ь декорированны й
объек т от недекорированного, а значит, и никои м образо м не зависи т от наличи я
или отсутствия оформлений.
Применимость
Используйте паттерн декоратор:
а для динамического, прозрачног о для клиенто в добавлени я обязанносте й
объектам;
а для реализаци и обязанностей, которы е могу т быт ь снят ы с объекта;
а когд а расширени е путе м порождени я подклассо в по каким-т о причина м не-
удобн о или невозможно. Иногд а приходитс я реализовыват ь мног о незави -
симы х расширений, так что порождени е подклассо в для поддержк и всех
возможны х комбинаци й приведе т к комбинаторном у рост у их числа. В дру -
гих случая х определени е класс а може т быт ь скрыт о или почему-либ о еще
недоступно, так что породит ь от нег о подклас с нельзя.
Структурные паттерны
Структура
Участники
a Component (VisualComponent) - компонент:
- определяет интерфейс для объектов, на которые могут быть динамичес-
ки возложены дополнительные обязанности;
a ConcreteComponent (TextView) - конкретный компонент:
- определяет объект, на который возлагаются дополнительные обязанности;
a Decorator - декоратор:
- хранит ссылку на объект Component и определяет интерфейс, соответ-
ствующий интерфейсу Component;
a ConcreteDecorator (BorderDecorator, ScrollDecorator) - конкрет-
ный декоратор:
- возлагает дополнительные обязанности на компонент.
Отношения
Decorator переадресует запросы объекту Component. Может выполнять
и дополнительные операции до и после переадресации.
Результаты
У паттерна декоратор есть, по крайней мере, два плюса и два минуса:
а большая гибкость, нежели у статического наследования. Паттерн декоратор
позволяет более гибко добавлять объекту новые обязанности, чем было бы
возможно в случае статического (множественного) наследования. Декоратор
может добавлять и удалять обязанности во время выполнения программы. При
использовании же наследования требуется создавать новый класс для каждой
дополнительной обязанности (например, Bor.deredScrollableTextView,
BorderedTextView), что ведет к увеличению числа классов и, как следствие,
к возрастанию сложности системы. Кроме того, применение нескольких
Паттерн Decorator
декораторов к одному компоненту позволяет произвольным образом соче-
тать обязанности.
Декораторы позволяют легко добавить одно и то же свойство дважды. На-
пример, чтобы окружить объект Text View двойной рамкой, нужно просто
добавить два декоратора BorderDecorators. Двойное наследование клас-
су Border в лучшем случае чревато ошибками;
а позволяет избежать перегруженных функциями классов на верхних уровнях
иерархии. Декоратор разрешает добавлять новые обязанности по мере необ-
ходимости. Вместо того чтобы пытаться поддержать все мыслимые возмож-
ности в одном сложном, допускающем разностороннюю настройку классе,
вы можете определить простой класс и постепенно наращивать его функци^
ональность с помощью декораторов. В результате приложение уже не пла-
тит за неиспользуемые функции. Нетрудно также определять новые виды
декораторов независимо от классов, которые они расширяют, даже если пер-
воначально такие расширения не планировались. При расширении же слож-
ного класса обычно приходится вникать в детали, не имеющие отношения
к добавляемой функции;
а декоратор и его компонент не идентичны. Декоратор действует как про-
зрачное обрамление. Но декорированный компонент все же не идентичен
исходному. При использовании декораторов это следует иметь в виду;
а множество мелких объектов. При использовании в проекте паттерна деко-
ратор нередко получается система, составленная из большого числа мелких
объектов, которые похожи друг на друга и различаются только способом вза-
имосвязи, а не классом и не значениями своих внутренних переменных. Хотя
проектировщик, разбирающийся в устройстве такой системы, может легко
настроить ее, но изучать и отлаживать ее очень тяжело.
Реализация
Применение паттерна декоратор требует рассмотрения нескольких вопросов:
а соответствие интерфейсов. Интерфейс декоратора должен соответствовать ин-
терфейсу декорируемого компонента. Поэтому классы ConcreteDecorator
должны наследовать общему классу (по крайней мере, в C++);
а отсутствие абстрактного класса Decorator. Нет необходимости определять
абстрактный класс Decorator, если планируется добавить всего одну обя-
занность. Так часто происходит, когда вы работаете с уже существующей
иерархией классов, а не проектируете новую. В таком случае ответствен-
ность за переадресацию запросов, которую обычно несет класс Decorator,
можно возложить непосредственно на ConcreteDecorator;
а облегченные классы Component. Чтобы можно было гарантировать соответ-
ствие интерфейсов, компоненты и декораторы должны наследовать общему
классу Component. Важно, чтобы этот класс был настолько легким, насколь-
ко возможно. Иными словами, он должен определять интерфейс, а не хранить
данные. В противном случае декораторы могут стать весьма тяжеловесными,
и применять их в большом количестве будет накладно. Включение большого
Структурные паттерны
числа функций в класс Component также увеличивает вероятность, что кон-
кретным подклассам придется платить за то, что им не нужно;
а изменение облика, а не внутреннего устройства объекта. Декоратор можно
рассматривать как появившуюся у объекта оболочку, которая изменяет его
поведение. Альтернатива - изменение внутреннего устройства объекта, хо-
рошим примером чего может служить паттерн стратегия.
Стратегии лучше подходят в ситуациях, когда класс Component уже доста-
точно тяжел, так что применение паттерна декоратор обходится слишком
дорого. В паттерне стратегия компоненты передают часть своей функцио-
нальности отдельному объекту-стратегии, поэтому изменить или расширить
поведение компонента допустимо, заменив этот объект.
Например, мы можем поддержать разные стили рамок, поручив рисование
рамки специальному объекту Border. Объект Border является примером
объекта-стратегии: в данном случае он инкапсулирует стратегию рисования
рамки. Число стратегий может быть любым, поэтому эффект такой же, как
от рекурсивной вложенности декораторов.
Например, в системах МасАрр 3.0 [Арр89] и Bedrock [Sym93a] графические
компоненты, называемые видами (views), хранят список объектов-оформите-
лей (adoraer), которые могут добавлять различные оформления вроде границ
к виду. Если к виду присоединены такие объекты, он дает им возможность вы-
полнить свои функции. МасАрр и Bedrock вынуждены предоставить дос-
туп к этим операциям, поскольку класс View весьма тяжел. Было бы слишком
расточительно использовать полномасштабный объект этого класса только для
того, чтобы добавить рамку.
Поскольку паттерн декоратор изменяет лишь внешний облик компонента,
последнему ничего не надо «знать» о своих декораторах, то есть декораторы
прозрачны для компонента.
• Функциональность, расширенная декоратором-—*
В случае стратегий самому компоненту известно о возможных расширени-
ях. Поэтому он должен располагать информацией обо всех стратегиях и ссы-
латься на них.
Паттерн Decorato r
При использовании подхода, основанного на стратегиях, может возникнуть
необходимость модифицировать компонент, чтобы он соответствовал ново-
му расширению. С другой стороны, у стратегии может быть свой собствен-
ный специализированный интерфейс, тогда как интерфейс декоратора дол-
жен повторять интерфейс компонента. Например, стратегии рисования
рамки необходимо определить всего лишь интерфейс для этой операции
(DrawBorder, GetWidth и т.д.), то есть класс стратегии может быть лег-
ким, несмотря на тяжеловесность компонента.
Системы МасАрр и Bedrock применяют такой подход не только для оформ-
ления видов, но и для расширения особенностей поведения объектов, свя-
занных с обработкой событий. В обеих системах вид ведет список объектов
поведения, которые могут модифицировать и перехватывать события. Каж-
дому зарегистрированному объекту поведения вид предоставляет возмож-
ность обработать событие до того, как оно будет передано незарегистриро-
ванным объектам такого рода, за счет чего достигается переопределение
поведения. Можно, например, декорировать вид специальной поддержкой
работы с клавиатурой, если зарегистрировать объект поведения, который
перехватывает и обрабатывает события нажатия клавиш.
Пример кода
В следующем примере показано, как реализовать декораторы пользователь-
ского интерфейса в программе на C++. Мы будем предполагать, что класс компо-
нента называется VisualComponent:
class VisualComponent {
public:
VisualComponent();
virtual void Draw();
virtual void Resize();
// ...
};
Определим подкласс класса VisualComponent с именем Decorator, от ко-
торого затем породим подклассы, реализующие различные оформления:
class Decorator : public VisualComponent {
public:
Decorator(VisualComponent*);
virtual void Draw();
virtual void Res i ze( );
// ...
private:
VisualComponent * „component;
};
Объект класса Decorator декорирует объект VisualComponent, на который
ссылается переменная экземпляра _component, инициализируемая в конструкторе.
Структурные паттерны
Для каждой операции в интерфейс е VisualComponen t в классе Decorato r опре-
делена реализаци я по умолчанию, передающа я запросы объекту, на который ве-
дет ссылка „.component:
voi d Decorator::Dra w () {
_component->Draw();
}
voi d Decorator: :Resiz e () {
_component->Resiz e ( ) ;
}
Подкласс ы Decorato r определяют специализированны е операции. Напри-
мер, класс BorderDecorato r добавляе т к своему внутреннем у компонент у рам-
ку. BorderDecorator - это подкласс Decorator, где операция Draw замещена
так, что рисует рамку. В этом классе определен а также закрыта я вспомогательна я
операция DrawBorder, которая, собственно, и изображае т рамку. Реализаци и всех
остальных операций этот подклас с наследуе т от Decorator:
clas s BorderDecorato r : publi c Decorato r {
public:
BorderDecorato r (VisualComponent*, in t borderWidth ) ;
virtual void Draw();
private:
void DrawBorde r ( int ) ;
private:
int _width;
voi d BorderDecorato r :: Draw () {
Decorato r : : Dra w ( ) ;
DrawBorde r (_width ) ;
}
Подклассы ScrollDecorator и DropShadowDecorator, которые добавят
визуальному компонент у возможност ь прокрутк и и оттенения можно реализоват ь
аналогично.
Теперь нам удастс я скомпоноват ь экземпляр ы этих классов для получени я
различных оформлений. Ниже показано, как использоват ь декоратор ы для созда-
ния прокручиваемого компонента Text View с рамкой.
Во-первых, нужен какой-т о способ поместит ь визуальны й компонен т в окон-
ный объект. Предположим, что в нашем классе Windo w для этой цели имеется
операция SetContents:
void Window: : SetContent s (VisualComponent * contents ) {
// ...
}
Теперь можно создать поле для ввода текста и окно, в котором будет находить -
ся это поле:
Паттерн Decorator
Window * windo w = ne w Window;
TextView * textVie w = new TextView;
TextView - подкласс VisualComponent, значит, мы могли бы поместить его
в окно:
window->SetContents(textView);
Но нам нужно поле ввода с рамкой и возможность ю прокрутки. Поэтому пред-
варительн о мы его надлежащи м образом оформим:
window->SetContents (
new BorderDecorator (
ne w ScrollDecorator(textView), 1
)
);
Поскольк у класс Windo w обращаетс я к своему содержимом у только через ин-
терфейс VisualComponent, то ему неизвестно о присутстви и декоратора. Клиент
при желании может сохранит ь ссылку на само поле ввода, если ему нужно работать
с ним непосредственно, наприме р вызыват ь операции, не входящие в интерфей с
VisualComponent. Клиенты, которым важна идентичност ь объекта, также долж-
ны обращатьс я к нему напрямую.
Известные применения
Во многих библиотека х для построени я объектно-ориентированны х интерфей-
сов пользовател я декоратор ы применяютс я для добавлени я к виджета м графичес -
ких оформлений. В качестве примеро в можно назвать Interviews [LVC89, LCI+92],
ЕТ++ [WGM88] и библиотек у классов ObjectWorks\Smalltal k [РагЭО]. Другие ва-
рианты применени я паттерна декорато р - это класс DebuggingGlyp h из библио-
теки Interviews и PassivityWrapper из ParcPlace Smalltalk. DebuggingGlyph
печатает отладочну ю информаци ю до и после того, как переадресуе т запрос на
размещени е своему компоненту. Эта информаци я может быть полезна для анали-
за и отладк и стратеги и размещени я объекто в в сложно м контейнере. Класс
PassivityWrapper позволяет разрешить или запретить взаимодействие компо-
нента с пользователем.
Но применени е паттерна декорато р никоим образом не ограничиваетс я гра-
фическими интерфейсам и пользователя, как показывае т следующи й пример, ос-
нованный на потоковых классах из каркас а ЕТ++ [WGM88].
Поток - это фундаментальна я абстракци я в большинств е средств ввода/вы-
вода. Он может предоставлят ь интерфейс для преобразовани я объектов в после-
довательност ь байтов или символов. Это позволяе т записат ь объект в файл или
буфер в памят и и впоследстви и извлечь его оттуда. Самый очевидный способ сде-
лать это - определить абстрактный класс Stream с подклассами MemoryStream
и FileStream. Предположим-, однако, что нам хотелос ь бы еще уметь:
а компрессироват ь данные в потоке, применя я различные алгоритмы сжатия
(кодировани е повторяющихс я серий, алгорит м Лемпеля-Зив а и т.д.);
Структурные паттерны
а преобразовыват ь данные в 7-битные символы кода ASCII для передачи по
каналу связи.
Паттерн декоратор позволяе т весьма элегантно добавить такие обязанност и
потокам. На диаграмме ниже показано одно из возможных решений задачи.
Абстрактный класс Stream имеет внутренний буфер и предоставляе т опера-
ции для помещения данных в поток (Putlnt, PutString). Как только буфер за-
полняется, Stream вызывает абстрактну ю операцию HandleBufferFull, кото-
рая выполняе т реальное перемещение данных. В классе Fi leSt ream эта операция
замещается так, что буфер записываетс я в файл.
Ключевым здесь является класс StreamDecorator. Именно в нем хранится
ссылка на тот поток-компонент, которому переадресуютс я все запросы. Подклас-
сы StreamDecorator замещают операцию HandleBufferFull и выполняют до-
полнительные действия, перед тем как вызвать реализацию этой операции в клас-
се StreamDecorator.
Например, подкласс CompressingStrea m сжимает данные, a ASCII7Strea m
конвертируе т их в 7-битный код ASCII. Теперь, для того чтобы создать объект
FileStream, который одновременно сжимает данные и преобразуе т результат
в 7-битный код, достаточно просто декорироват ь FileStrea m с использование м
CompressingStream и ASCII7Stream:
Stream * aStrea m = ne w CompressingStrea m (
new ASCII7Stream (
new FileStrea m ( "aFileName")
)
);
aStream->Put!nt(12);
aStream->PutString("aString");
Родственные паттерны
Адаптер: если декоратор изменяет только обязанност и объекта, но не его ин-
терфейс, то адаптер придает объекту совершенно новый интерфейс.
Паттер н Facad e
Компоновщик: декорато р можн о считат ь вырожденны м случае м составног о
объекта, у которог о есть тольк о один компонент. Однак о декорато р добавляе т
новые обязанности, агрегировани е объекто в не являетс я его целью.
Стратегия: декорато р позволяе т изменит ь внешни й обли к объекта, страте -
гия - его внутренне е содержание. Это два взаимодополняющи х способ а измене -
ния объекта.
Паттер н Facad e
Название и классификация паттерна
Фасад - паттерн, структурирующий объекты.
Назначение
Предоставляе т унифицированны й интерфей с вмест о набор а интерфейсо в не-
которо й подсистемы. Фаса д определяе т интерфей с боле е высоког о уровня, кото-
рый упрощае т использовани е подсистемы.
Мотивация
Разбиени е на подсистем ы облегчае т проектировани е сложно й систем ы в целом.
Обща я цель всяког о проектировани я - свест и к минимум у зависимост ь подсисте м
друг от друг а и обме н информацие й межд у ними. Один из способо в решени я этой
задач и - введени е объект а фасад, предоставляющи й едины й упрощенны й интер -
фейс к боле е сложны м системны м средствам.
Рассмотрим, например, сред у программирования, котора я дает приложени -
ям досту п к подсистем е компиляции. В этой подсистем е имеютс я таки е классы,
как Scanner (лексический анализатор), Parser (синтаксический анализатор),
ProgramNod e (узе л программы), BytecodeStrea m (пото к байтовы х кодов )
и ProgramNodeBuilder (строитель узла программы). Все вместе они состав-
ляют компилятор. Некоторы м специализированны м приложениям, возможно,
понадобитс я прямо й досту п к этим классам. Но для большинств а клиенто в ком-
пилятор а таки е детали, как синтаксически й разбо р и генераци я кода, обычн о не
нужны; им прост о требуетс я откомпилироват ь некотору ю программу. Для таки х
клиенто в применени е мощного, но низкоуровневог о интерфейс а подсистем ы ком-
пиляци и тольк о усложняе т задачу.
Структурные паттерны
Чтобы предоставит ь интерфей с боле е высоког о уровня, изолирующи й клиен -
та от этих классов, в подсистем у компиляци и включе н также клас с Compile r
(компилятор). Он определяе т унифицированны й интерфей с ко всем возможнос -
тям компилятора. Клас с Compile r выступае т в роли фасада: предлагае т просто й
интерфей с к боле е сложно й подсистеме. Он «склеивает » классы, реализующи е
функциональност ь компилятора, но не скрывае т их полностью. Благодар я фаса -
ду компилятор а работ а большинств а программисто в облегчается. При этом те,
кому нуже н досту п к средства м низког о уровня, не лишаютс я его.
Применимость
Используйте паттерн фасад, когда:
а хотит е предоставит ь просто й интерфей с к сложно й подсистеме. Част о подсис -
темы усложняютс я по мере развития. Применени е большинств а паттерно в
приводи т к появлени ю меньши х классов, но в больше м количестве. Таку ю
подсистем у проще повторн о использоват ь и настраиват ь под конкретны е
нужды, но вмест е с тем применят ь подсистем у без настройк и становитс я
труднее. Фаса д предлагае т некоторы й вид систем ы по умолчанию, устраи -
вающи й большинств о клиентов. И лишь те объекты, которы м нужн ы боле е
широки е возможност и настройки, могу т обратитьс я напряму ю к тому, что
находитс я за фасадом;
а межд у клиентам и и классам и реализаци и абстракци и существуе т мног о за-
висимостей. Фаса д позволи т отделит ь подсистем у как от клиентов, так
и от други х подсистем, что, в свою очередь, способствуе т повышени ю степе -
ни независимост и и переносимости;
Паттер н Facad e
а вы хотите разложить подсистему на отдельные слои. Используйте фасад для
определения точки входа на каждый уровень подсистемы. Если подсисте-
мы зависят друг от друга, то зависимость можно упростить, разрешив под-
системам обмениваться информацией только через фасады.
Структура
Участники
a Facade (Compiler) - фасад:
- «знает», каким классам подсистемы адресовать запрос;
- делегирует запросы клиентов подходящим объектам внутри подсистемы;
а Классы подсистемы (Scanner, Parser, ProgramNode и т.д.):
- реализуют функциональност ь подсистемы;
- выполняют работу, порученную объектом Facade;
- ничего не «знают» о существовании фасада, то есть не хранят ссылок на
него.
Отношения
Клиенты общаются с подсистемой, посылая запросы фасаду. Он переадресу-
ет их подходящим объектам внутри подсистемы. Хотя основную работу выполня-
ют именно объекты подсистемы, фасаду, возможно, придется преобразовать свой
интерфейс в интерфейсы подсистемы.
Клиенты, пользующиеся фасадом, не имеют прямого доступа к объектам под-
системы.
Результаты
У паттерна фасад есть следующие преимущества:
а изолирует клиентов от компонентов подсистемы, уменьшая тем самым чис-
ло объектов, с которыми клиентам приходится иметь дело, и упрощая рабо-
ту с подсистемой;
Структурны е паттерн ы
а позволяе т ослабит ь связанност ь межд у подсистемо й и ее клиентами. Зачас -
тую компонент ы подсистем ы сильн о связаны. Слаба я связанност ь позволя -
ет видоизменят ь компоненты, не затрагива я при этом клиентов. Фасадь:
помогаю т разложит ь систем у на слои и структурироват ь зависимост и межд у
объектами, а также избежат ь сложны х и циклически х зависимостей. Это мо-
жет оказатьс я важным, если клиен т и подсистем а реализуютс я независим о
Уменьшени е числ а зависимосте й на стади и компиляци и чрезвычайн о важ-
но в больши х системах. Хочется, конечно, чтобы время, уходяще е на пере -
компиляци ю посл е изменени я классо в подсистемы, был о минимальны м
Сокращени е числ а зависимосте й за счет фасадо в може т уменьшит ь количе -
ство нуждающихс я в повторно й компиляци и файло в после небольшо й моди-
фикаци и какой-нибуд ь важно й подсистемы. Фаса д може т также упростит ь
процес с перенос а систем ы на други е платформы, поскольк у уменьшаетс я ве-
роятност ь того, что в результат е изменени я одно й подсистем ы понадобитс я
изменят ь и все остальные;
а фаса д не препятствуе т приложения м напряму ю обращатьс я к класса м под-
системы, если это необходимо. Таки м образом, у вас есть выбо р межд у прос -
тотой и общностью.
Реализация
При реализации фасад а следуе т обратит ь внимани е на следующи е вопросы:
а уменьшение степени связанности клиента с подсистемой. Степен ь связан -
ности можн о значительн о уменьшить, если сделат ь клас с Facad e абстракт -
ным. Его конкретны е подкласс ы буду т соответствоват ь различны м реали -
зация м подсистемы. Тогда клиент ы смогу т взаимодействоват ь с подсистемо й
через интерфей с абстрактног о класс а Facade. Это изолируе т клиенто в от ин-
формаци и о том, кака я реализаци я подсистем ы используется.
Вместо порождения подклассов можно сконфигурироват ь объект Facade
различным и объектам и подсистем. Для настройк и фасад а достаточн о заме -
нить один или нескольк о таки х объектов;
а открытые и закрытые классы подсистем. Подсистем а похож а на клас с в том
отношении, что у обои х есть интерфейс ы и оба что-т о инкапсулирую т
Класс инкапсулируе т состояни е и операции, а подсистем а - классы. И если
полезн о различат ь открыты й и закрыты й интерфейс ы класса, то не мене е ра-
зумно говорит ь об открыто м и закрыто м интерфейса х подсистемы.
Открыты й интерфей с подсистем ы состои т из классов, к которы м имеют до-
ступ все клиенты; закрыты й интерфей с доступе н тольк о для расширени я
подсистемы. Клас с Facade, конечн о же, являетс я часть ю открытог о интер -
фейса, но это не единственна я часть. Други е класс ы подсистем ы также мо-
гут быт ь открытыми. Например, в систем е компиляци и класс ы Parser
и Scanne r - част ь открытог о интерфейса.
Делат ь класс ы подсистем ы закрытым и иногд а полезно, но это поддерживает -
ся немногим и объектно-ориентированным и языками. И в C++, и в Smalltal k
для классо в традиционн о использовалос ь глобально е пространств о имен.
Паттер н Facad e
Однако комите т по стандартизаци и C++ добавил к языку пространств а имен
[Str94], и это позволил о разрешат ь доступ только к открытым классам под-
системы.
Пример кода
Рассмотрим более подробно, как возвести фасад вокруг подсистемы компиляции.
В подсистеме компиляци и определе н класс BytecodeStream, который реа-
лизует поток объекто в Bytecode. Объект Bytecod e инкапсулируе т байтовый
код, с помощь ю которог о описываютс я машинные команды. В этой же подсисте -
ме определе н еще класс Token для объектов, инкапсулирующи х лексемы языка
программирования.
Класс Scanne r принимае т на входе поток символо в и генерируе т поток лек-
сем, по одной каждый раз:
class Scanner {
public:
Scanner(istream&);
virtual -Scanner();
virtual Token & Scan();
private:
istream& _inputStream;
};
Класс Parser использует класс ProgramNodeBuilder для построения де-
рева разбора из лексем, возвращенны х классом Scanner:
clas s Parse r {
public:
Parser();
virtua l -Parser();
virtua l voi d Parse(Scanners, ProgramNodeBuilder&);
, };
Parser вызывает ProgramNodeBuilder для инкрементного построения де-
рева. Взаимодействие этих классов описывается паттерном строитель:
clas s ProgramNodeBuilde r {
public:
ProgramNodeBuilder();
virtua l ProgramNode * NewVariable (
cons t char * variableNam e
) const;
virtua l ProgramNode * NewAssignment (
ProgramNode * variable, ProgramNode * expressio n
) const;
Структурны е паттерн ы
virtual ProgramNode* NewReturnStatement(
ProgramNode* value
) const;
virtual ProgramNode* NewCondition(
ProgramNode* condition,
ProgramNode* truePart, ProgramNode* falsePart
) const;
// ...
ProgramNode * GetRootNode();
private:
ProgramNode * _node;
};
Дерево разбора состоит из экземпляров подклассов класса ProgramNode, таких
как StatementNode, ExpressionNode и т.д. Иерархия классов ProgramNode — это
пример паттерна компоновщик. Класс ProgramNode определяет интерфейс для
манипулирования узлом программы и его потомками, если таковые имеются:
clas s ProgramNod e {
public:
// манипулировани е узло м программ ы
virtua l voi d GetSourcePosition(int & line, int & index);
// ...
// манипулировани е потомкам и
virtua l voi d Add(ProgramNode*);
virtua l voi d Remove(ProgramNode*);
// .. .
virtua l voi d Traverse(CodeGeneratork);
protected:
ProgramNode();
};
Операция Traverse (обход) принимает объект CodeGenerator (кодогенера-
тор) в качестве параметра. Подклассы ProgramNode используют этот объект для ге-
нерации машинного кода в форме объектов Bytecode, которые помещаются в по-
ток BytecodeStream. Класс CodeGenerator описывается паттерном посетитель:
class CodeGenerator {
public:
virtual void Visit(StatementNode*);
virtual void Visit(ExpressionNode*);
// ...
protected:
CodeGenerator(BytecodeStreamk);
protected:
BytecodeStreamk _output;
};
Паттер н Facad e
У CodeGenerato r есть подклассы, наприме р StackMachineCodeGenerato r
и RISCCodeGenerator, генерирующи е машинны й код для различны х аппаратны х
архитектур.
Каждый подкласс ProgramNod e реализует операцию Travers e и обращает-
ся к ней для обход а свои х потомков. Кажды й потомо к рекурсивн о делае т то же са-
мое для свои х потомков. Например, в подкласс е ExpressionNod e (узе л выраже -
ния) операция Traverse определена так:
void ExpressionNode::Traverse (CodeGenerator& eg) {
eg.Vi s i t ( t hi s );
ListIterator<ProgramNode*> i(_chil dren);
for (i. First ( ); ! i . IsDone () ; i.Next O) {
i.Currentl tem()->Traverse(eg);
}
}
Классы, о которы х мы говорил и до сих пор, составляю т подсистем у компиля -
ции. А тепер ь введе м клас с Compiler, которы й буде т служит ь фасадом, позволяю -
щим собрат ь все эти фрагмент ы воедино. Клас с Compile r предоставляе т просто й
интерфей с для компилировани я исходног о текст а и генераци и кода для конкрет -
ной машины:
class Compiler {
public:
Compiler();
virtual void Compile(istream&, BytecodeStream&);
};
void Compiler::Compile (
istream& input, BytecodeStreamk output
) {
Scanner scanner(input);
ProgramNodeBuilder builder;
Parser parser;
parser.Parse(scanner, builder);
RISCCodeGenerator generator(output);
ProgramNode* parseTree = builder.GetRootNode();
parseTree->Traverse(generator);
}
В этой реализаци и жестк о «зашит » тип кодогенератора, поэтом у программист у
не нужн о явно задават ь целеву ю архитектуру. Это може т быт ь вполн е разумно, когд а
есть всег о одна така я архитектура. Если же это не так, можн о было бы изменит ь кон-
структо р класс а Compiler, чтоб ы он принима л объек т CodeGenerato r в качеств е
параметра. Тогд а программис т указыва л бы, каки м генераторо м пользоватьс я при
Структурные паттерны
инстанцировани и объекта Compiler. Фасад компилятор а можно параметризоват ь
и другими участниками, скажем, объектами Scanner и ProgramNodeBuilder, что
повышает гибкость, но в то же время сводит на нет основную цель фасада - предо-
ставление упрощенног о интерфейса для наиболее распространенног о случая.
Известные применения
Пример компилятор а в разделе «Пример кода» навеян идеями из системы
компиляции языка ObjectWorks\Smalltal k [РагЭО].
В каркасе ЕТ++ [WGM88] приложени е может иметь встроенные средства
инспектировани я объектов во время выполнения. Они реализуютс я в отдельной
подсистеме, включающе й класс фасада с именем ProgrammingEnvironment.
Этот фасад определяет такие операции, как InspectObject и InspectClass
для доступа к инспекторам.
Приложение, написанное в среде ЕТ++, может также запретить поддержку
инспектирования. В таком случае класс ProgrammingEnvironmen t реализуе т
соответствующи е запросы как пустые операции, не делающие ничего. Только под-
класс ETProgrammingEnvironmen t реализует эти операции так, что они отобра-
жают окна соответствующи х инспекторов. Приложени ю неизвестно, доступно
инспектировани е или нет. Здесь мы встречаем пример абстрактно й связанност и
между приложение м и подсистемо й инспектирования.
В операционно й системе Choices [CIRM93] фасады используютс я для состав-
ления одного каркаса из нескольких. Ключевыми абстракциями в системе Choices
являются процессы, память и адресные пространства. Для каждой из них есть соот-
ветствующа я подсистема, реализованна я в виде каркаса. Это обеспечивае т поддерж-
ку переноса Choices на разные аппаратные платформы. У двух таких подсистем есть
«представители», то есть фасады. Они называются FileSystemlnterface (па-
мять) и Domai n (адресные пространства).
Паттерн Flyweigh t
Например, для каркас а виртуально й памят и фасадо м служи т Domain. Клас с
Domai n представляе т адресно е пространство. Он обеспечивае т отображени е межд у
виртуальным и адресам и и смещениям и объекто в в памяти, файле или на устрой -
стве длительног о хранения. Базовы е операци и класс а Domai n поддерживаю т до-
бавлени е объект а в памят ь по указанном у адресу, удалени е объект а из памят и
и обработк у ошибо к отсутстви я страниц.
Как видно из вышеприведенно й диаграммы, внутр и подсистем ы виртуально й
памят и используютс я следующи е компоненты:
Q MemoryObjec t представляе т объект ы данных;
a MemoryOb j ectCache кэшируе т данные из объекто в MemoryOb j ects в фи-
зическо й памяти. MemoryOb j ectCach e - это не что иное, как объек т Стра -
тегия, в которо м локализован а политик а кэширования;
a AddressTransla t ion инкапсулируе т особенности оборудования трансля-
ции адресов.
Операци я RepairFaul t вызываетс я при возникновени и ошибк и из-за отсут -
ствия страницы. Domai n находи т объек т в памят и по адресу, где произошл а ошиб -
ка и делегируе т операци ю RepairFaul t кэшу, ассоциированном у с этим объек -
том. Поведени е объекто в Domai n можн о настроить, замени в их компоненты.
Родственные паттерны
Паттерн абстрактная фабрика допустимо использовать вместе с фасадом,
чтобы предоставит ь интерфей с для создани я объекто в подсисте м способом, не за-
висимы м от этих подсистем. Абстрактна я фабрик а може т выступат ь и как аль-
тернатив а фасаду, чтобы скрыт ь платформенно-зависимы е классы.
Паттер н посредни к аналогиче н фасад у в том смысле, что абстрагируе т функ -
циональност ь существующи х классов. Однак о назначени е посредник а - абстра -
гироват ь произвольно е взаимодействи е межд у «сотрудничающими » объектами.
Часто он централизуе т функциональность, не присущу ю ни одному из них. Кол-
леги посредник а обмениваютс я информацие й именн о с ним, а не напряму ю меж-
ду собой. Напротив, фасад прост о абстрагируе т интерфей с объекто в подсистемы,
чтобы ими было проще пользоваться. Он не определяе т новой функциональности,
и класса м подсистем ы ничег о неизвестн о о его существовании.
Обычно требуется только один фасад. Поэтому объекты фасадов часто бы-
вают одиночками.
Паттер н Flyweigh t
Название и классификация паттерна
Приспособлене ц - паттерн, структурирующи й объекты.
Назначение
Используе т разделени е для эффективно й поддержк и множеств а мелки х
объектов.
Структурны е паттерн ы
Мотивация
В некоторы х приложения х использовани е объекто в могло бы быть очень по-
лезным, но прямолинейна я реализаци я оказываетс я недопустим о расточительной.
Например, в большинств е редакторо в документо в имеютс я средств а форма -
тировани я и редактировани я текстов, в той или иной степени модульные. Объект -
но-ориентированны е редактор ы обычн о применяю т объект ы для представлени я
таких встроенны х элементов, как таблиц ы и рисунки. Но они не использую т
объект ы для представлени я каждог о символа, несмотр я на то что это увеличил о
бы гибкост ь на самых нижни х уровня х приложения. Ведь тогда к рисовани ю и фор-
матировани ю символо в и встроенны х элементо в можно былб бы применит ь еди-
нообразны й подход. И для поддержк и новых наборо в символо в не пришлос ь бы
как-либ о затрагиват ь остальны е функци и редактора. Да и общая структур а прило-
жения отражал а бы физическу ю структур у документа. На следующе й диаграмм е
показано, как редакто р документо в мог бы воспользоватьс я объектам и для пред-
ставлени я символов.
У таког о дизайн а есть один недостато к - стоимость. Даже в документ е скром-
ных размеро в было бы нескольк о сотен тысяч объектов-символов, а это привел о
бы к расходовани ю огромног о объема памят и и неприемлемы м затрата м во время
выполнения. Паттер н приспособлене ц показывает, как разделят ь очень мелки е
объект ы без недопустим о высоки х издержек.
Приспособлене ц - это разделяемы й объект, которы й можно использоват ь
одновременн о в нескольки х контекстах. В каждо м контекст е он выгляди т как не-
зависимы й объект, то есть неотличи м от экземпляра, который не разделяется.
Приспособленц ы не могут делать предположени й о контексте, в котором работают.
Паттер н Flyweigh t
Ключева я иде я здес ь - различи е межд у внутренним и внешним состояниями.
Внутренне е состояни е хранитс я в само м приспособленц е и состои т из информа -
ции, не зависяще й от его контекста. Именн о поэтом у он може т разделяться. Внеш-
нее состояни е зависи т от контекст а и изменяетс я вмест е с ним, поэтом у не подле -
жит разделению. Объекты-клиент ы отвечаю т за передач у внешнег о состояни я
приспособленцу, когд а в этом возникае т необходимость.
Приспособленц ы моделирую т концепци и или сущности, числ о которы х
слишко м велик о для представлени я объектами. Например, редакто р документо в
мог бы создат ь по одном у приспособленц у для каждо й букв ы алфавита. Кажды й
приспособлене ц храни т код символа, но координат ы положени я символ а в доку -
мент е и стил ь его начертани я определяютс я алгоритмам и размещени я текст а
и командам и форматирования, действующим и в том месте, где симво л появляет -
ся. Код символ а - это внутренне е состояние, а все остально е - внешнее.
Логическ и для каждог о вхождени я данног о символ а в докумен т существуе т
объект.
Физически, однако, есть лишь по одном у объекту-приспособленц у для каждо -
го символа, которы й появляетс я в различны х контекста х в структур е документа. Каж-
дое вхождени е данног о объекта-символ а ссылаетс я на один и тот же экземпля р в раз-
деляемо м пуле объектов-приспособленцев.
Структурны е паттерн ы
Ниже изображена структур а класс а для этих объектов. Glyp h - это абстракт -
ный клас с для представлени я графически х объекто в (некоторы е из них могу т
быть приспособленцами). Операции, которы е могу т зависет ь от внешнег о состоя -
ния, передаю т его в качеств е параметра. Например, операция м Dra w (рисование )
и Intersects (пересечение) должн о быть известно, в како м контекст е встреча -
ется глиф, инач е они не смогу т выполнит ь то, что от них требуется.
Приспособленец, представляющи й букв у «а», содержи т тольк о соответствую -
щий ей код; ни положение, ни шрифт букв ы ему хранит ь не надо. Клиент ы пере -
дают приспособленц у всю зависящу ю от контекст а информацию, котора я нужна,
чтобы он мог изобразит ь себя. Например, глифу Row известно, где его потомк и
должн ы себя показать, чтобы это выглядел о как горизонтальна я строка. Поэтом у
вмест е с запросо м на рисовани е он може т передават ь каждом у потомк у координаты.
Поскольк у числ о различны х объектов-символо в горазд о меньше, чем числ о
символо в в документе, то и обще е количеств о объекто в существенн о меньше, чем
было бы при просто й реализации. Документ, в которо м все символ ы изображают -
ся одни м шрифто м и цветом, создас т порядк а 100 объектов-символо в (это при-
мерно равно числ у кодо в в таблиц е ASCII ) независим о от своег о размера. А по-
скольк у в большинств е документо в применяетс я не боле е десятк а различны х
комбинаци й шрифт а и цвета, то на практик е эта величин а возрасте т несуществен -
но. Поэтом у абстракци я объект а становитс я применимо й и к отдельны м символам.
Применимость
Эффективност ь паттерн а приспособлене ц во много м зависи т от того, как
и где он используется. Применяйт е этот паттерн, когд а выполнен ы все нижепере -
численны е условия:
а в приложени и используетс я большо е числ о объектов;
а из-з а этог о накладны е расход ы на хранени е высоки;
а большу ю част ь состояни я объекто в можн о вынест и вовне;
а многи е групп ы объекто в можн о заменит ь относительн о небольши м количе -
ством разделяемы х объектов, поскольк у внешне е состояни е вынесено;
Паттерн Flyweight
а приложение не зависит от идентичности объекта. Поскольку объекты-при-
способленцы могут разделяться, то проверка на идентичность возвратит
«истину» для концептуально различных объектов.
Структура
На следующей диаграмме показано, как приспособленцы разделяются.
Участники
a Flyweight (Glyph) - приспособленец:
- объявляет интерфейс, с помощью которого приспособленцы могут полу-
чать внешнее состояние или как-то воздействовать на него;
a ConcreteFlyweight (Character) - конкретный приспособленец:
- реализует интерфейс класса Flyweight и добавляет при необходимости
внутреннее состояние. Объект класса ConcreteFlyweight должен быть
разделяемым. Любое сохраняемое им состояние должно быть внутрен-
ним, то есть не зависящим от контекста;
a UnsharedConcreteFlyweight (Row, Column) - неразделяемый конкретный
приспособленец:
- не все подклассы Flyweight обязательно должны быть разделяемыми.
Интерфейс Flyweight допускает разделение, но не навязывает его. Часто
у объектов UnsharedConcreteFlyweight на некотором уровне структуры
Структурны е паттерн ы
приспособленца есть потомки в виде объектов класса Concret eFlyweight,
как, например, у объекто в классо в Row и Column;
a FlyweightFactory - фабрика приспособленцев:
- создае т объекты-приспособленц ы и управляе т ими;
- обеспечивае т должно е разделени е приспособленцев. Когда клиен т запра -
шивает приспособленца, объект FlyweightFactory предоставляет су-
ществующи й экземпля р или создае т новый, если готовог о еще нет;
a Clien t - клиент:
- храни т ссылк и на одног о или нескольки х приспособленцев;
- вычисляе т или храни т внешне е состояни е приспособленцев.
Отношения
а состояние, необходимо е приспособленц у для нормально й работы, можно
охарактеризоват ь как внутренне е или внешнее. Перво е хранитс я в самом
объект е ConcreteFlyweight. Внешне е состояни е хранитс я или вычисля -
ется клиентами. Клиен т передае т его приспособленц у при вызов е операций;
а клиент ы не должн ы создават ь экземпляр ы класс а ConcreteFlyweigh t
напрямую, а могу т получат ь их тольк о от объект а FlyweightFactory. Это
позволи т гарантироват ь корректно е разделение.
Результаты
При использовани и приспособленце в не исключен ы затрат ы на передачу, по-
иск или вычислени е внутреннег о состояния, особенн о если раньше оно хранилос ь
как внутреннее. Однак о такие расход ы с лихво й компенсируютс я экономие й па-
мяти за счет разделени я объектов-приспособленцев.
Экономи я памят и возникае т по ряду причин:
а уменьшени е общег о числа экземпляров;
а сокращени е объема памяти, необходимог о для хранени я внутреннег о состо-
яния;
а вычисление, а не хранени е внешнег о состояни я (если это действительн о так).
Чем выше степен ь разделени я приспособленцев, тем существенне е экономия.
С увеличение м объема разделяемог о состояни я экономи я также возрастает. Са-
мого большог о эффект а удаетс я добиться, когда суммарны й объем внутренне й
и внешне й информаци и о состояни и велик, а внешне е состояни е вычисляется, а не
хранится. Тогда разделени е уменьшае т стоимост ь хранени я внутреннег о состояния,
а за счет вычислени й сокращаетс я память, отводима я под внешне е состояние.
Паттер н приспособлене ц часто применяетс я вмест е с компоновщико м для
представлени я иерархическо й структур ы в виде графа с разделяемым и листовым и
узлами. Из-за разделени я указател ь на родител я не може т хранитьс я в листово м
узле-приспособленце, а долже н передаватьс я ему как часть внешнег о состояния.
Это оказывае т заметно е влияни е на способ взаимодействи я объекто в иерархи и
между собой.
Реализация
При реализаци и приспособленц а следуе т обратит ь внимани е на следующи е
вопросы:
Паттерн Flyweight
а вынесение внешнего состояния. Применимост ь паттерн а в значительно й сте-
пени зависи т от того, наскольк о легк о идентифицироват ь внешне е состоя -
ние и вынест и его за предел ы разделяемы х объектов. Вынесени е внешнег о
состояни я не уменьшае т стоимост и хранения, если различны х внешни х со-
стояни й так же много, как и объекто в до разделения. Лучши й вариан т -
внешне е состояни е вычисляетс я по объекта м с друго й структурой, требую -
щей значительн о меньше й памяти.
Например, в наше м редактор е документо в мы може м поместит ь карт у с ти-
пографско й информацие й в отдельну ю структуру, а не хранит ь шрифт и на-
чертани е вмест е с кажды м символом. Данна я карт а буде т отслеживат ь не-
прерывны е серии символо в с одинаковым и типографским и атрибутами.
Когд а объект-симво л изображае т себя, он получае т типографски е атрибут ы
от алгоритм а обхода. Поскольк у обычн о в документа х используетс я немно -
го разны х шрифто в и начертаний, то хранит ь эту информаци ю отдельн о от
объекта-символ а горазд о эффективнее, чем непосредственн о в нем;
а управление разделяемыми объектами. Так как объект ы разделяются, клиен -
ты не должн ы инстанцироват ь их напрямую. Фабрик а FlyweightFactor y
позволяе т клиента м найт и подходящег о приспособленца. В объекта х этог о
класс а част о есть хранилище, организованно е в виде ассоциативног о масси -
ва, с помощь ю которог о можн о быстр о находит ь приспособленца, нужног о
клиенту. Так, в пример е редактор а документо в фабрик а приспособленце в
може т содержат ь внутр и себя таблицу, индексированну ю кодо м символа,
и возвращат ь нужног о приспособленц а по его коду. А если требуемы й при-
способлене ц отсутствует, он тут же создается.
Разделяемост ь подразумевае т также, что имеетс я некотора я форма подсче -
та ссыло к или сбора мусор а для освобождени я занимаемо й приспособлен -
цем памяти, когд а необходимост ь в нем отпадает. Однак о ни то, ни друго е
необязательно, если числ о приспособленце в фиксирован о и невелик о (на-
пример, если речь иде т о представлени и набор а символо в кода ASCII).
В тако м случа е имее т смыс л хранит ь приспособленце в постоянно.
Пример кода
Возвращаяс ь к пример у с редакторо м документов, определи м базовы й клас с
Glyp h для графически х объектов-приспособленцев. Логическ и глифы - это со-
ставны е объекты, которы е обладаю т графическим и атрибутам и и умеют изображат ь
себя (см. описани е паттерн а компоновщик). Сейча с мы ограничимс я тольк о шриф-
том, но тот же подхо д примени м и к любым други м графически м атрибутам:
class Glyph {
public:
virtual ~Glyph();
virtual void Draw(Window*, GlyphContext&);
virtual void SetFont(Font*, GlyphContextk);
virtual Font* GetFont(GlyphContextk);
Структурные паттерны
virtual void First(GlyphContext&);
virtual void Next(GlyphContext&);
virtual bool IsDone(GlyphContext&);
virtual Glyph* Current(GlyphContextk);
virtual void Insert(Glyph*, GlyphContextu);
virtual void Remove(GlyphContext&};
protected:
Glyph();
};
В подкласс е Characte r хранитс я прост о ко д символа:
class Character : public Glyph {
public:
Character(char);
virtual void Draw(Window*, GlyphContext&);
private:
char _charcode;
};
Чтобы не выделять память для шрифта каждого глифа, будем хранить этот
атрибут во внешнем объекте класса GlyphContext. Данный объект поддерживае т
соответствие между глифом и его шрифтом (а также любыми другими графически-
ми атрибутами) в различных контекстах. Любой операции, у которой должна быть
информация о шрифте глифа в данном контексте, в качестве параметра будет пере-
даваться экземпляр GlyphContext. У него операция и может запросить нужные
сведения. Контекст определяетс я положением глифа в структуре. Поэтому опера-
циями обхода и манипулировани я потомками обновляется GlyphContext:
class GlyphContex t {
public:
GlyphContext();
virtual -GlyphContext();
virtual void Next(int step = 1);
virtual void Insert(int quantity = 1);
virtual Font* GetFont();
virtual void SetFont(Font*, int span = 1);
private:
int _index;
BTree* _fonts;
};
Объекту GlyphContex t должно быть известно о текущем положении в струк-
туре глифов во время ее обхода. Операция GlyphContext: .-Next увеличивае т
переменную _index по мере обхода структуры. Подклассы класса Glyph, имею-
щие потомков (например, Row и Column), должны реализовыват ь операцию Next
так, чтобы она вызывала GlyphContext: :Next в каждой точке обхода.
Паттерн Flyweight
Операция GlyphContext: :GetFont использует переменную _index в ка-
честве ключ а для структур ы ВТгее, в которо й хранитс я отображени е межд у гли-
фами и шрифтами. Каждый узел дерева помече н длино й строки, для которо й он
предоставляе т информаци ю о шрифте. Листь я дерева указываю т на шрифт, а внут-
ренние узлы разбиваю т строк у на подстрок и - по одной для каждог о потомка.
Рассмотри м фрагмен т текста, представляющи й собой композици ю глифов.
Структур а ВТгее, в которо й хранитс я информаци я о шрифтах, може т выгля -
деть так:
Внутренни е узлы определяю т диапазон ы индексо в глифов. Дерев о обновля -
ется в ответ на изменени е шрифта, а также при каждо м добавлени и и удалени и
глифов из структуры. Например, если предположить, что текуще й точке обход а
соответствуе т индек с 102, то следующи й код установи т шрифт каждог о символ а
в слове «expect » таким же, как у близлежащег о текст а (то есть time s 12 - экземп -
ляр класс а Font для шрифт а Times Roma n размеро м 12 пунктов):
Структурные паттерны
GlyphContex t gc;
Font * timesl 2 = ne w Font("Times-Roman-12");
Font * timesltalic!2 = ne w Font("Times-Italic-12");
// ...
gc.SetFont(times12, 6);
gc.SetFont (times!2, 6);
Новая структура ВТгее выглядит так (изменения выделены более
цветом):
ярким
Добавим перед «expect» слово «don't » (включая пробел после него), написан-
ное шрифтом Times Italic размером 12 пунктов. В предположении, что текущей
позиции все еще соответствует индекс 102, следующий код проинформирует
объект gc об этом:
gc.Insert(6) ;
gc.SetFont(timesltalicl2, б);
Теперь структура ВТгее выглядит так:
При запрашивании шрифта текущего глифа объект GlyphContext спускает-
ся вниз по дереву, суммируя индексы, пока не будет найден шрифт для текущего
Паттерн Flyweight
индекса. Поскольку шрифт меняется нечасто, размер дерева мал по сравнению
с размером структуры глифов. Это позволяет уменьшить расходы на хранение без
заметного увеличения времени поиска.1
И наконец, нам нужна еще фабрика FlyweightFactory, которая создает гли-
фы и обеспечивает их корректное разделение. Класс GlyphFactory создает
объекты Character и глифы других видов. Разделению подлежат только объек-
ты Character. Составных глифов гораздо больше, и их существенное состояние
(то есть множество потомков) в любом случае является внутренним:
const int NCHARCODES = 128;
class GlyphFactory {
public:
GlyphFactory ( ) ;
virtual -GlyphFactory ();
virtual Character* CreateCharacter (char) ;
virtual Row* CreateRowO ;
virtual Column* CreateColumnO ;
// ...
private:
Character * _character [NCHARCODES ] ;
};
Массив „character содержит указатели на глифы Character, индексиро-
ванные кодом символа. Конструктор инициализируе т этот массив нулями:
GlyphFactory: : GlyphFactory () {
fo r (in t i = 0; i < NCHARCODES; ++i ) {
„character [i] = 0;
}
}
Операция CreateCharacte r ищет символ в массиве и возвращает соответ-
ствующий глиф, если он существует. В противном случае CreateCharacte r со-
здает глиф, помещает его в массив и затем возвращает:
Character* GlyphFactory::CreateCharacter (char с) {
if (!_character[с]) {
_character[с] = new Character(с);
}
return _character[c];
}
Остальные операции просто создают новый объект при каждом обращении,
так как несимвольные глифы не разделяются:
1 Время поиска в этой схеме пропорционально частоте смены шрифта. Наименьшая производитель-
ность бывает, когда смена шрифта происходит на каждом символе, но на практике это бывает редко.
Структурны е паттерн ы
Row* GlyphFactory:rCreateRow () {
return new Row;
}
Column* GlyphFactory::CreateColumn () {
return new Column;
}
Эти операци и можн о было бы опустит ь и позволит ь клиента м инстанциро -
вать неразделяемы е глифы напрямую. Но если позже мы решим сделат ь разделяе -
мыми и их тоже, то придетс я изменят ь клиентски й код, в которо м они создаются.
Известные применения
Концепци я объектов-приспособленце в впервы е была описан а и использован а
как техник а проектировани я в библиотек е Interview s 3.0 [CL90]. Ее разработчи -
ки построил и мощны й редакто р документо в Doc, чтобы доказат ь практическу ю
полезност ь подобно й идеи. В Doc объекты-глиф ы используютс я для представле -
ния любог о символ а документа. Редакто р строит по одному экземпляр у глифа для
каждог о сочетани я символ а и стиля (в которо м определен ы все графически е ат-
рибуты). Таким образом, внутренне е состояни е символ а состои т из его кода и ин-
формаци и о стиле (индек с в таблиц у стилей).1 Следовательно, внешне й оказывает -
ся тольк о позиция, -поэтом у Doc работае т быстро. Документ ы представляютс я
классом Document, которы й выполняе т функци и фабрик и FlyweightFactory.
Измерени я показали, что реализованно е в Doc разделени е символов-приспособ -
ленце в весьма эффективно. В типично м случа е для документ а из 180 тыся ч зна-
ков необходим о создат ь тольк о 480 объектов-символов.
В каркас е ЕТ++ [WGM88 ] приспособленц ы используютс я для поддержк и не-
зависимост и от внешнег о облика.2 Его стандар т определяе т расположени е элемен -
тов пользовательског о интерфейс а (полос прокрутки, кнопок, меню и пр., в сово-
купност и именуемы х виджетами ) и их оформлени я (тени и т.д.). Видже т делегируе т
забот у о свое м расположени и и изображени и отдельном у объект у Layout. Из-
менени е этог о объект а ведет к изменени ю внешнег о облик а даже во время вы-
полнения.
Для каждог о класс а виджет а имеетс я соответствующи й класс Layou t (напри -
мер, ScrollbarLayout, MenubarLayout и т.д.). В данном случае очевидная про-
блема состои т в том, что удваиваетс я числ о объекто в пользовательског о интер -
фейса, ибо для каждог о интерфейсног о объект а есть дополнительны й объек т
Layout. Чтобы избавитьс я от расходов, объект ы Layou t реализован ы в виде при-
способленцев. Они прекрасн о подходя т на эту роль, так как занят ы преимуще -
ственн о определение м поведени я и им легко передат ь тот небольшо й объем внеш-
ней информаци и о состоянии, которы й необходи м для изображени я объекта.
1 В приведенно м выше пример е кода информаци я о стиле вынесен а наружу, так что внутренне е состо-
яние - это только код символа.
2 Друго й подхо д к обеспечени ю независимост и от внешнег о облик а см. в описани и паттерн а абстракт -
ная фабрика.
Паттерн Proxy
Объекты Layout создаются и управляются объектами класса Look. Класс
Look - это абстрактна я фабрика, котора я производи т объект ы Layou t с помощь ю
таких операций, как GetButtonLayout, GetMenuBarLayou t и т.д. Для каждог о
стандарт а внешнег о облик а у класс а Look есть соответствующи й подклас с
(Motif Look, OpenLook и т.д.).
Кстати говоря, объект ы Layou t - это, по существу, стратеги и (см. описани е
паттерна стратегия). Таким образом, мы имеем приме р объекта-стратегии, реа-
лизованны й в виде приспособленца.
Родственные паттерны
Паттерн приспособлене ц часто используетс я в сочетани и с компоновщико м
для реализаци и иерархическо й структур ы в виде ациклическог о направленног о
графа с разделяемым и листовым и вершинами.
Часто наилучши м способом реализаци и объекто в состояни я и стратеги и яв-
ляется паттер н приспособленец.
Паттер н Prox y
Название и классификация паттерна
Заместител ь - паттерн, структурирующи й объекты.
Назначение
Являетс я суррогато м другог о объект а и контролируе т доступ к нему.
Известен также под именем
Surrogate (суррогат).
Мотивация
Разумно управлят ь доступо м к объекту, поскольк у тогда можно отложит ь рас-
ходы на создани е и инициализаци ю до момента, когда объект действительн о по-
надобится. Рассмотри м редакто р документов, который допускае т встраивани е в до-
кумент графически х объектов. Затрат ы на создани е некоторы х таких объектов,
наприме р больши х растровых изображений, могут быть весьма значительны. Но
документ долже н открыватьс я быстро, поэтому следуе т избегат ь создани я всех
«тяжелых » объекто в на стадии открыти я (да и вообще это излишне, поскольк у не
все они будут видны одновременно).
В связи с такими ограничениям и кажетс я разумны м создават ь «тяжелые »
объект ы по требованию. Это означае т «когда изображени е становитс я видимым».
Но что поместит ь в докумен т вместо изображения? И как, не усложня я реализа -
ции редактора, скрыт ь то, что изображени е создаетс я по требованию? Например,
оптимизаци я не должна отражатьс я на коде, отвечающе м за рисовани е и форма-
тирование.
Решени е состоит в том, чтобы использоват ь друго й объект - заместитель
изображения, который временн о подставляетс я вмест о реальног о изображения.
Заместител ь ведет себя точно так же, как само изображение, и выполняе т при не-
обходимост и его инстанцирование.
Структурные паттерны
Заместитель создает настоящее изображение, только если редактор докумен-
та вызовет операцию Draw. Все последующие запросы заместитель переадресует
непосредственно изображению. Поэтому после создания изображения он должен
сохранить ссылку на него.
Предположим, что изображения хранятся в отдельных файлах. В таком слу-
чае мы можем использовать имя файла как ссылку на реальный объект. Замести-
тель хранит также размер изображения, то есть длину и ширину. «Зная» ее, заме-
ститель может отвечать на запросы форматера о своем размере, не инстанцируя
изображение.
На следующей диаграмме классов этот пример показан более подробно.
Редактор документов получает доступ к встроенным изображениям только че-
рез интерфейс, определенный в абстрактном классе Graphic. ImageProxy - это
класс для представления изображений, создаваемых по требованию. В ImageProxy
хранится имя файла, играющее роль ссылки на изображение, которое находится
на диске. Имя файла передается конструктору класса ImageProxy.
В объекте ImageProxy находятся также ограничивающий прямоугольник
изображения и ссылка на экземпляр реального объекта Image. Ссылка остается
недействительной, пока заместитель не инстанцирует реальное изображение.
Операцией Draw гарантируется, что изображение будет создано до того, как за-
меститель переадресует ему запрос. Операция Get Extent переадресует запрос
Паттерн Proxy
изображению, только если оно уже инстанцировано; в противном случае ImageProxy
возвращает размеры, которые хранит сам.
Применимость
Паттерн заместитель применим во всех случаях, когда возникает необходи-
мость сослаться на объект более изощренно, чем это возможно, если использовать
простой указатель. Вот несколько типичных ситуаций, где заместитель оказыва-
ется полезным:
а удаленный заместитель предоставляет локального представителя вместо
объекта, находящегося в другом адресном пространстве. В системе NEXTSTEP
[Add94] для этой цели применяется класс NXProxy. Заместителя такого
рода Джеймс Коплиен [Сор92] называет «послом»;
а виртуальный заместитель создает «тяжелые» объекты по требованию. При-
мером может служить класс ImageProxy, описанный в разделе «Мотивация»;
а защищающий заместитель контролирует доступ к исходному объекту. Та-
кие заместители полезны, когда для разных объектов определены различ-
ные права доступа. Например, в операционной системе Choices [CIRM93]
объекты Kernel Proxy ограничивают права доступа к объектам операци-
онной системы;
а «умная» ссылка - это замена обычного указателя. Она позволяет выполнить
дополнительные действия при доступе к объекту. К типичным применени-
ям такой ссылки можно отнести:
- подсчет числа ссылок на реальный объект, с тем чтобы занимаемую им па-
мять можно было освободить автоматически, когда не останется ни одной
ссылки (такие ссылки называют еще «умными» указателями [Ede92]);
- загрузку объекта в память при первом обращении к нему;
- проверку и установку блокировки на реальный объект при обращении
к нему, чтобы никакой другой объект не смог в это время изменить его.
Структура
Вот как может выглядеть диаграмма объектов для структуры с заместителем
во время выполнения.
Структурные паттерны
Участники
a Proxy (imageProxy ) - заместитель:
- хранит ссылку, которая позволяе т заместител ю обратитьс я к реальному
субъекту. Объект класса Proxy может обращатьс я к объекту класса
Subj ect, если интерфейсы классов RealSubj ect и Subj ect одинаковы;
- предоставляе т интерфейс, идентичны й интерфейс у Subj ect, так что за-
меститель всегда может быть подставле н вместо реальног о субъекта;
- контролируе т доступ к реальному субъект у и может отвечать за его соз-
дание и удаление;
- прочие обязанност и зависят от вида заместителя:
- удаленный заместитель отвечает за кодировани е запроса и его аргумен-
тов и отправлени е закодированног о запроса реальному субъекту в дру-
гом адресном пространстве;
- виртуальный заместитель может кэшироват ь дополнительну ю инфор-
мацию о реальном субъекте, чтобы отложит ь его создание. Например,
класс ImageProx y из раздела «Мотивация » кэшируе т размеры реаль-
ного изображения;
- защищающий заместитель проверяет, имеет ли вызывающи й объект
необходимые для выполнени я запроса права;
a Subject (Graphic ) - субъект:
- определяет общий для RealSubject и Proxy интерфейс, так что класс
Proxy можно использоват ь везде, где ожидаетс я RealSubject;
a RealSubject (Image) - реальный субъект:
- определяе т реальный объект, представленны й заместителем.
Отношения
Proxy при необходимост и переадресуе т запросы объект у RealSubject. Де-
тали зависят от вида заместителя.
Результаты
С помощь ю паттерна заместител ь при доступе к объекту вводитс я дополни-
тельный уровень косвенности. У этого подхода есть много вариантов в зависимо-
сти от вида заместителя:
а удаленный заместител ь может скрыть тот факт, что объект находитс я в дру-
гом адресном пространстве;
а виртуальны й заместител ь может выполнят ь оптимизацию, наприме р созда-
ние объекта по требованию;
а защищающи й заместител ь и «умная» ссылка позволяют решать дополни-
тельные задачи при доступе к объекту.
Паттер н Proxy
Есть еще одна оптимизация, которую паттерн заместитель иногда скрывает
от клиента. Она называется копированием при записи (copy-on-write) и имеет мно-
го общего с созданием объекта по требованию. Копирование большого и сложно-
го объекта - очень дорогая операция. Если копия не модифицировалась, то нет
смысла эту цену платить. Если отложить процесс копирования, применив замес-
титель, то можно быть уверенным, что эта операция произойдет только тогда, ког-
да он действительно был изменен.
Чтобы во время записи можно было копировать, необходимо подсчитывать
ссылки на субъект. Копирование заместителя просто увеличивает счетчик ссы-
лок. И только тогда, когда клиент запрашивает операцию, изменяющую субъект,
заместитель действительно выполняет копирование. Одновременно заместитель
должен уменьшить счетчик ссылок. Когда счетчик ссылок становится равным
нулю, субъект уничтожается.
Копирование при записи может существенно уменьшить плату за копирова-
ние «тяжелых» субъектов.
Реализация
При реализации паттерна заместитель можно использовать следующие воз-
можности языка:
а перегрузку оператора доступа к членам в C++. Язык C++ поддерживает пе-
регрузку оператора доступа к членам класса ->. Это позволяет производить
дополнительные действия при любом разыменовании указателя на объект.
Для реализации некоторых видов заместителей это оказывается полезно,
поскольку заместитель ведет себя аналогично указателю.
В следующем примере показано, как воспользоваться данным приемом для
реализации виртуального заместителя imagePtr:
clas s Image;
exter n Image * LoadAnImageFile ( cons t char*);
// внешня я функци я
class ImagePt r {
public:
ImagePt r (cons t char * imageFile ) ;
virtua l -ImagePt r ();
virtua l Image * operator- > () ;
virtua l Image & operator * ( ) ;
private:
Image * Loadlmag e ( ) ;
private:
Image * _image;
cons t char * _imageFile;
};
ImagePtr::ImagePt r (cons t char * theImageFile )
_imageFil e = theImageFile;
_imag e = 0;
}
Структурны е паттерн ы
Image * ImagePtr::LoadImag e () {
if (_imag e ==0 ) {
_imag e = LoadAnlmageFile(_imageFile);
}
retur n _image;
}
Перегруженные операторы -> и * используют операцию Loadlmage для
возврата клиенту изображения, хранящегося в переменной _image (при не-
обходимости загрузив его):
Image * ImagePtr::operator- > () {
retur n Loadlmage();
}
Image & ImagePtr::operator * () {
retur n *LoadImage();
}
Такой подход позволяет вызывать операции объекта Image через объекты
ImagePtr, не заботясь о том, что они не являются частью интерфейса дан-
ного класса:
ImagePt r imag e = ImagePtr("anlmageFileName");
image->Draw ( Poin t (50, 100));
// (image.operator->())->Draw(Point(50, 100) )
Обратите внимание, что заместитель изображения ведет себя подобно ука-
зателю, но не объявлен как указатель на Image. Это означает, что использо-
вать его в точности как настоящий указатель на Image нельзя. Поэтому при
таком подходе клиентам следует трактовать объекты Image и ImagePtr по-
разному.
Перегрузка оператора доступа - лучшее решение далеко не для всех видов
заместителей. Некоторым из них должно быть точно известно, какая опера-
ция вызывается, а в таких случаях перегрузка оператора доступа не работает.
Рассмотрим пример виртуального заместителя, обсуждавшийся в разделе
«Мотивация». Изображение нужно загружать в точно определенное вре-
мя - при вызове операции Draw, а не при каждом обращении к нему. Пере-
грузка оператора доступа не позволяет различить подобные случаи. В такой
ситуации придется вручную реализовать каждую операцию заместителя,
переадресующую запрос субъекту.
Обычно все эти операции очень похожи друг на друга, как видно из приме-
ра кода в одноименном разделе. Они проверяют, что запрос корректен, что
объект-адресат существует и т.д., а потом уже перенаправляют ему запрос.
Писать этот код снова и снова надоедает. Поэтому нередко для его aвтoмa^
тической генерации используют препроцессор;
а метод doesNotUnderstand в Smalltalk. В языке Smalltalk есть возможность, по-
зволяющая автоматически поддержать переадресацию запросов. При отправ-
лении клиентом сообщения, для которого у получателя нет соответствующе-
го метода, Smalltalk вызывает метод doesNotUnderstand: aMessage.
Паттер н Prox y
Заместитель может переопределить doesNotUnderstand так, что сообще-
ние буде т переадресован о субъекту.
Дабы гарантировать, что запро с буде т перенаправле н субъекту, а не прост о
тихо поглоще н заместителем, клас с Prox y можн о определит ь так, что он не
стане т понимат ь никаких сообщений. Smalltal k позволяе т это сделать, надо
лишь, чтоб ы у Prox y не было суперкласса 1.
Главны й недостато к метод а doesNotUnderstand: в том, что в большин -
стве Smalltalk-систе м имеетс я нескольк о специальны х сообщений, обраба -
тываемы х непосредственн о виртуально й машиной, а в этом случа е стандарт -
ный механиз м поиск а методо в обходится. Правда, единственно й тако й
операцией, написанно й в класс е Ob j ect (следовательно, могуще й затронут ь
заместителей), являетс я тождеств о ==.
Если вы собираетес ь применят ь doesNotUnderstand: для реализаци я
заместителя, то должн ы как-т о решит ь вышеописанну ю проблему. Нельз я
же ожидать, что совпадени е заместителе й - это то же самое, что и совпаде -
ние реальны х субъектов. К сожалению, doesNotUnderstand: изначальн о
создавалс я для обработк и ошибок, а не для построени я заместителей, по-
этому его быстродействи е оставляе т желат ь лучшего;
а заместителю не всегда должен быть известен тип реального объекта. Если
класс Prox y може т работат ь с субъекто м тольк о чере з его абстрактны й ин-
терфейс, то не нужн о создават ь Prox y для каждог о класс а реальног о субъек -
та Real Sub j ect; заместитель может обращаться к любому из них единооб-
разно. Но если заместител ь долже н инстанцироват ь реальны х субъекто в
(как обстои т дело в случа е виртуальны х заместителей), то знани е конкрет -
ного класс а обязательно.
К проблема м реализаци и можн о отнест и и решени е вопрос а о том, как обра -
щатьс я к еще не инстанцированном у субъекту. Некоторы е заместител и должн ы
обращатьс я к свои м субъекта м вне зависимост и от того, где они находятс я - дис-
ке или в памяти. Это означает, что нужн о использоват ь какую-т о форму не зави-
сящи х от адресног о пространств а идентификаторо в объектов. В раздел е «Моти -
вация » для этой цели использовалос ь имя файла.
Пример кода
В коде реализован о два вида заместителей: виртуальный, описанны й в разде -
ле «Мотивация», и реализованный с помощью метода doesNotUnderstand:.2
а виртуальный заместитель. В класс е Graphi c определе н интерфей с для гра-
фически х объектов:
class Graphic {
public:
virtual -Graphic();
1 Эта техник а используетс я при реализаци и распределенны х объекто в в систем е NEXTSTE P [Add94 ]
(точнее, в класс е NXProxy). Тольк о там переопределяетс я мето д forwar d - эквивален т описанног о
тольк о что прием а в Smalltalk.
2 Еще один вид заместител я дает паттер н итератор.
Структурны е паттерн ы
virtua l voi d Draw(cons t Point & at ) = 0;
virtua l voi d HandleMous e (Event & event ) = 0;
virtua l cons t Point & GetExtent( ) = 0;
virtua l voi d Load(istream & from ) = 0;
virtua l voi d Save(ostream & to ) = 0;
protected:
Graphic();
};
Класс Image реализует интерфейс Graphi c для отображения файлов изоб-
ражений. В нем замещена операция HandleMouse, посредством которой
пользователь может интерактивно изменять размер изображения:
clas s Imag e : publi c Graphi c {
public:
Image(cons t char * file); // загрузк а изображени я из файл а
virtua l ~Image();
virtua l voi d Draw(cons t Point & at);
virtua l voi d HandleMouse(Event & event);
virtua l cons t Point & GetExtent();
virtua l voi d Load(istream & from);
virtua l voi d Save(ostream & to);
private:
// ...
};
Класс imageProxy имеет тот же интерфейс, что и Image:
clas s ImageProx y : publi c Graphi c {
public:
ImageProxy(cons t char * imageFile);
virtua l ~ImageProxy();
virtua l voi d Draw(cons t Point & at);
virtua l voi d HandleMouse(Event & event);
virtua l cons t Point s GetExtent();
virtua l voi d Load(istream & from);
virtua l voi d Save(ostream & to);
protected:
Image * GetlmageO;
private:
Image * _image;
Poin t _extent;
char * _fileName;
};
Паттер н Prox y
Конструкто р сохраняе т локальну ю копи ю имен и файла, в которо м хранит -
ся изображение, и инициализируе т член ы _exten t и _image:
ImageProxy::ImageProx y (cons t char * fileName ) {
_fileNam e = strdup(fileName ) ;
_exten t = Point::Zero; // размер ы пок а не известн ы
_imag e = 0;
}
Image * ImageProxy::GetImag e () {
if (_imag e ==0 ) {
_imag e = ne w Image(_fileNam e );
}
retur n _image;
}
Реализация операции GetExtent возвращает кэшированный размер, если
это возможно. В противном случае изображение загружается из файла. Опе-
рация Draw загружает изображение, a HandleMouse перенаправляет собы-
тие реальному изображению:
cons t Point & ImageProxy::GetExten t () {
if (_exten t == Point::Zero ) {
_exten t = GetImage()->GetExten t () ;
}
retur n _extent;
}
voi d ImageProx y :: Dra w (cons t Point & at ) {
Get Imag e () ->Draw(at ) ;
}
voi d ImageProxy::HandleMous e (Event & event ) {
Getlmage()->HandleMouse(event);
}
Операция Save записывает кэшированный размер изображения и имя фай-
ла в поток, a Load считывает эту информацию и инициализирует соответ-
ствующие члены:
voi d ImageProxy::Sav e (ostream & to ) {
to « _exten t « _fileName;
}
voi d ImageProxy::Loa d (istream & from ) {
fro m » _exten t » _fileName;
}
Наконец, предположим, чт о ест ь клас с Tex t Documen t дл я представлени я
документа, которы й може т содержат ь объект ы класс а Graphic:
clas s TextDocumen t {
public:
TextDocument();
Структурные паттерны
void Insert(Graphic*);
// . . .
};
Мы можем вставить объект ImageProxy в документ следующим образом:
TextDocument* text = new TextDocument;
// ...
text->Insert(new ImageProxy("anl mageFi l eName"));
а заместители, использующие метод doesNotUnderstand. В языке Smalltalk
можно создавать обобщенных заместителей, определяя классы, для которых
нет суперкласса1, а в них - метод doesNotUnderstand: для обработки со-
общений.
В показанном ниже фрагменте предполагается, что у заместителя есть ме-
тод realSubject, возвращающий связанный с ним реальный субъект. При
использовании ImageProxy этот метод должен был бы проверить, создан
ли объект Image, при необходимости создать его и затем вернуть. Для об-
работки перехваченного сообщения, которое было адресовано реальному
субъекту, используется метод perf orm: withArguments :.
doesNotUnderstand: aMessage
Л self realSubject
perform: aMessage selector
withArguments: aMessage arguments
Аргументом doesNotUnderstand: является экземпляр класса Message,
представляющий сообщение, не понятое заместителем. Таким образом, при
ответе на любое сообщение заместитель сначала проверяет, что реальный
субъект существует, а потом уже переадресует ему сообщение.
Одно из преимуществ метода doesNotUnderstand: - он способен выпол-
нить произвольную обработку. Например, можно было бы создать защища-
ющего заместителя, определив набор legalMessages-сообщений, которые
следует принимать, и снабдив заместителя следующим методом:
doesNotUnderstand: aMessage
Л (legalMessages includes: aMessage selector)
i fTrue: [self realSubject
perform: aMessage selector
withArguments: aMessage arguments]
i f Fal se: [self error: 'Illegal operator']
Прежде чем переадресовать сообщение реальному субъекту, указанный ме-
тод проверяет, что оно допустимо. Если это не так, doesNotUnderstand:
посылает сообщение error: самому себе, что приведет к зацикливанию,
если в заместителе не определен метод error:. Следовательно, определе-
ние error: должно быть скопировано из класса Object вместе со всеми
методами, которые в нем используются.
Практически для любого класса Object является суперклассом самого верхнего уровня. Поэтому вы-
ражени е «нет суперкласса » означае т то же самое, что «Objec t не являетс я суперклассом».
Обсуждение структурных паттернов
Известные применения
Пример виртуальног о заместител я из раздела «Мотивация » заимствова н из
классов строительног о блока текста, определенных в каркасе ЕТ++.
В системе NEXTSTEP [Add94] заместител и (экземпляр ы класса NXProxy )
используютс я как локальные представител и объектов, которые могут быть распре-
деленными. Сервер создает заместителе й для удаленных объектов, когда клиент их
запрашивает. Заместитель кодирует полученное сообщение вместе со всеми аргу-
ментами, после чего отправляе т его удаленному субъекту. Аналогично субъект ко-
дирует возвращенные результаты и посылает их обратно объекту NXProxy.
В работе McCulloug h [McC87] обсуждаетс я применение заместителе й в Small -
talk для доступа к удаленным объектам. Джефри Пэско (Geoffrey Pascoe) [Pas86]
описывает, как обеспечит ь побочные эффекты при вызове методов и реализоват ь
контроль доступа с помощью «инкапсуляторов».
Родственные паттерны
Паттерн адаптер предоставляе т другой интерфейс к адаптируемом у объекту.
Напротив, заместител ь в точности повторяет интерфейс своего субъекта. Одна-
ко, если заместител ь используетс я для ограничения доступа, он может отказаться
выполнят ь операцию, которую субъект выполнил бы, поэтому на самом деле ин-
терфейс заместител я может быть и подмножество м интерфейс а субъекта.
Несколько замечаний относительн о декоратора. Хотя его реализация и по-
хожа на реализацию заместителя, но назначение совершенно иное. Декоратор до-
бавляет объекту новые обязанности, а заместитель контролируе т доступ к объекту.
Степень схожести реализации заместителе й и декораторо в может быть раз-
личной. Защищающи й заместител ь мог бы быть реализован в точности как деко-
ратор. С другой стороны, удаленный заместител ь не содержит прямых ссылок на
реальный субъект, а лишь косвенную ссылку, что-то вроде «идентификато р хоста
и локальный адрес на этом хосте». Вначале виртуальный заместитель имеет толь-
ко косвенну ю ссылку (скажем, имя файла), но в конечном итоге получает и ис-
пользует прямую ссылку.
Обсуждени е структурны х паттерно в
Возможно, вы обратили внимание на то, что структурные паттерны похожи
между собой, особенно когда речь идет об их участника х и взаимодействиях. Ве-
роятное объяснение такому явлению: все структурные паттерны основаны на не-
большом множеств е языковых механизмо в структурировани я кода и объектов
(одиночном и множественно м наследовани и для паттернов уровня класса и ком-
позиции для паттернов уровня объектов). Но имеющеес я сходство может быть
обманчиво, ибо с помощью разных паттернов можно решать совершенно разные
задачи. В этом разделе сопоставлены группы структурных паттернов, и вы сможе-
те яснее почувствоват ь их сравнительные достоинств а и недостатки.
Адаптер и мост
У паттернов адаптер и мост есть несколько общих атрибутов. Тот и другой
повышают гибкость, вводя дополнительны й уровень косвенност и при обращении
Структурны е паттерн ы
к другому объекту. Оба перенаправляю т запрос ы другому объекту, использу я иной
интерфейс.
Основно е различи е между адаптеро м и мостом в их назначении. Цель пер-
вого - устранит ь несовместимост ь между двумя существующим и интерфейсами.
При разработке адаптера не учитывается, как эти интерфейсы реализованы
и то, как они могут независим о развиватьс я в будущем. Он долже н лишь обеспе -
чить совместну ю работу двух независим о разработанны х классов, так чтобы ни
один из них не пришлос ь переделывать. С другой стороны, мост связывае т абстрак -
цию с ее, возможно, многочисленным и реализациями. Данный паттер н предостав -
ляет клиента м стабильны й интерфейс, позволя я в то же время изменят ь классы,
которые его реализуют. Мост также подстраиваетс я под новые реализации, появ-
ляющиес я в процесс е развити я системы.
В связи с описанным и различиям и адапте р и мост часто используютс я в раз-
ные момент ы жизненног о цикла системы. Когда выясняется, что два несовмести -
мых класса должны работат ь вместе, следуе т обратитьс я к адаптеру. Тем самым
удастся избежат ь дублировани я кода. Заране е такую ситуаци ю предвидет ь нельзя.
Наоборот, пользовател ь моста с самог о начала понимает, что у абстракци и может
быть нескольк о реализаци й и развити е того и другог о будет идти независимо.
Адапте р обеспечивае т работ у после того, как нечто спроектировано; мост - до
того. Это доказывает, что адапте р и мост предназначен ы для решени я именно
своих задач.
Фасад можно представлять себе как адаптер к набору других объектов. Но
при такой интерпретаци и легко не заметит ь такой нюанс: фасад определяе т но-
вый интерфейс, тогда как адапте р повторн о используе т уже имеющийся. Подчерк -
нем, что адаптер заставляет работать вместе два существующих интерфейса, а не
определяе т новый.
Компоновщик, декоратор и заместитель
У компоновщик а и декоратор а аналогичны е структурны е диаграммы, сви-
детельствующи е о том, что оба паттерн а основан ы на рекурсивно й композици и
и предназначен ы для организаци и заране е неопределенног о числа объектов. При
обнаружени и данног о сходств а може т возникнут ь искушени е посчитат ь объект -
декорато р вырожденны м случае м компоновщика, но при этом будет искаже н сам
смысл паттерн а декоратор. Сходств о и заканчиваетс я на рекурсивно й компози -
ции, и снова из-за различи я задач, решаемы х с помощь ю паттернов.
Назначени е декоратор а - добавит ь новые обязанност и объект а без порожде -
ния подклассов. Этот паттер н позволяе т избежат ь комбинаторног о роста числа
подклассов, если проектировщи к пытаетс я статическ и определит ь все возможны е
комбинации. У компоновщик а другие задачи. Он долже н так структурироват ь
классы, чтобы различны е взаимосвязанны е объект ы удавалос ь трактоват ь едино-
образно, а нескольк о объекто в рассматриват ь как один. Акцен т здесь делаетс я не
на оформлении, а на представлении.
Указанны е цели различны, но дополняю т друг друга. Поэтому компоновщи к
и декорато р часто используютс я совместно. Оба паттерн а позволяю т спроекти -
роват ь систему так, что приложени я можно будет создавать, прост о соединя я
Обсуждени е структурны х паттерно в
объект ы межд у собой, без определени я новых классов. Появитс я неки й абстракт -
ный класс, одни подкласс ы которог о - компоновщики, други е - декораторы,
а треть и - реализаци и фундаментальны х строительны х блоко в системы. В тако м
случа е у компоновщико в и декораторо в буде т общи й интерфейс. Для декорато -
ра компоновщи к являетс я конкретны м компонентом. А для компоновщик а
декорато р - это листовы й узел. Разумеется, их необязательн о использоват ь вмес -
те, и, как мы видели, цели данны х паттерно в различны.
Заместител ь - еще один паттерн, структур а которог о напоминае т декоратор.
Оба они описывают, как можн о предоставит ь косвенны й досту п к объекту, и в ре-
ализаци и объектов-декораторо в и заместителе й хранитс я ссылк а на друго й объ-
ект, котором у переадресуютс я запросы. Но и здес ь цели различаются.
Как и декоратор, заместител ь предоставляе т клиент у интерфейс, совпадаю -
щий с интерфейсо м замещаемог о объекта. Но в отличи е от декоратор а замести -
телю не нужн о динамическ и добавлят ь и отбират ь свойства, он не предназначе н
для рекурсивно й композиции. Заместител ь долже н предоставит ь стандартну ю
замен у субъекту, когд а прямо й досту п к нему неудобе н или нежелателен, напри -
мер потому, что он находитс я на удаленно й машине, хранитс я на диск е или досту -
пен лишь ограниченном у круг у клиентов.
В паттерн е заместител ь субъек т определяе т основну ю функциональность,
а заместител ь разрешае т или запрещае т досту п к ней. В декоратор е компонен т
обладае т лишь часть ю функциональности, а остально е привнося т один или не-
скольк о декораторов. Декорато р позволяе т справитьс я с ситуацией, когд а пол-
ную функциональност ь объект а нельз я определит ь на этапе компиляци и или это
по тем или иным причина м неудобно. Така я неопределенност ь делае т рекурсив -
ную композици ю неотъемлемо й часть ю декоратора. Для заместител я дело об-
стоит не так, ибо ему важн о лишь одно отношени е - межд у собой и своим субъек -
том, а данно е отношени е можн о выразит ь статически.
Указанны е различи я существенны, поскольк у в них абстрагирован ы решени я
конкретны х проблем, снова и снов а возникающи х при объектно-ориентированно м
проектировании. Но это не означает, что сочетани е разны х паттерно в невозмож -
но. Можн о представит ь себе заместителя-декоратора, которы й добавляе т нову ю
функциональност ь заместителю, или декоратора-заместителя, которы й оформля -
ет удаленны й объект. Таки е гибрид ы теоретическ и могут быт ь полезн ы (у нас,
правда, не нашлос ь реальног о примера), а вот паттерны, из которы х они составле -
ны, полезн ы наверняка.
Глава 5. Паттерн ы поведени я
Паттерн ы поведени я связаны с алгоритмам и и распределение м обязанносте й меж-
ду объектами. Руечь в них идет не только о самих объекта х и классах, но и о типичных
способа х взаимодействия. Паттерн ы поведени я характеризую т сложный поток
управления, который трудно проследит ь во время выполнени я программы. Вни-
мание акцентирован о не на потоке управлени я как таковом, а на связях между
объектами.
В паттерна х поведени я уровня класса используетс я наследовани е - чтобы рас-
пределит ь поведени е между разными классами. В этой главе описано два таких
паттерна. Из них более простым и широко распространенны м являетс я шаблонны й
метод, который представляе т собой абстрактно е определени е алгоритма. Алго-
ритм здесь определяетс я пошагово. На каждом шаге вызываетс я либо примитив -
ная, либо абстрактна я операция. Алгорит м «обрастае т мясом» за счет подклассов,
где определен ы абстрактны е операции. Друго й паттер н поведени я уровня клас-
са - интерпретатор, который представляе т грамматик у языка в виде иерархи и клас-
сов и реализуе т интерпретато р как последовательност ь операци й над экземпля -
рами этих классов.
В паттерна х поведени я уровня объекто в используетс я не наследование, а ком-
позиция. Некоторы е из них описывают, как с помощь ю коопераци и множеств о
равноправны х объекто в справляетс я с задачей, котора я ни одному из них не под
силу. Важно здесь то, как объект ы получают информаци ю о существовани и друг
друга. Объекты-коллег и могут хранит ь ссылки друг на друга, но это увеличи т сте-
пень связанност и системы. При максимально й степен и связанност и каждом у
объект у пришлос ь бы иметь информаци ю обо всех остальных. Эту проблем у ре-
шает паттерн посредник. Посредник, находящийс я между объектами-коллегами,
обеспечивае т косвенност ь ссылок, необходиму ю для разрывани я лишни х связей.
Паттерн цепочк а обязанносте й позволяе т и дальше уменьшат ь степень свя-
занности. Он дает возможност ь посылат ь запрос ы объект у не напрямую, а по це-
почке «объектов-кандидатов». Запрос может выполнит ь любой «кандидат», если
это допустим о в текуще м состояни и выполнени я программы. Число кандидато в
заране е не определено, а подбират ь участнико в можно во время выполнения.
Паттерн наблюдател ь определяе т и отвечае т за зависимост и межд у объекта -
ми. Классически й приме р наблюдател я встречаетс я в схеме модель/вид/кон -
тролле р языка Smalltalk, где все виды модели уведомляютс я о любых изменени -
ях ее состояния.
Прочие паттерн ы поведени я связаны с инкапсуляцие й поведени я в объект е
и делегирование м ему запросов. Паттерн стратеги я инкапсулируе т алгорит м объекта,
Паттер н Chai n of Responsibilit y
упроща я его спецификаци ю и замену. Паттер н команд а инкапсулируе т запро с
в виде объекта, которы й можн о передават ь как параметр, хранит ь в списк е исто-
рии или использоват ь как-т о иначе. Паттер н состояни е инкапсулируе т состоя -
ние объекта таким образом, что при изменени и состояни я объек т може т изменят ь
поведение. Паттер н посетител ь инкапсулируе т поведение, которо е в противно м
случае пришлось бы распределять между классами, а паттерн итератор абстраги-
рует спосо б доступ а и обход а объекто в из некоторог о агрегата.
Паттер н Chai n of Responsibilit y
Название и классификация паттерна
Цепочк а обязанносте й - паттер н поведени я объектов.
Назначение
Позволяе т избежат ь привязк и отправител я запрос а к его получателю, дава я
шанс обработат ь запро с нескольки м объектам. Связывае т объекты-получател и
в цепочк у и передае т запро с вдол ь этой цепочки, пока его не обработают.
Мотивация
Рассмотри м контекстно-зависиму ю оперативну ю справк у в графическо м ин-
терфейс е пользователя, которы й може т получит ь дополнительну ю информаци ю
по любо й част и интерфейса, прост о щелкну в на ней мышью. Содержани е справк и
зависи т от того, кака я част ь интерфейс а и в како м контекст е выбрана. Например,
справк а по кнопк е в диалогово м окне може т отличатьс я от справк и по аналогич -
ной кнопк е в главно м окне приложения. Если для некоторо й част и интерфейс а
справк и нет, то систем а должн а показат ь информаци ю о ближайше м контексте,
в которо м она находится, наприме р о диалогово м окне в целом.
Поэтом у естественн о было бы организоват ь справочну ю информаци ю от бо-
лее конкретны х раздело в к более общим. Кроме того, ясно, что запро с на получе -
ние справк и обрабатываетс я одни м из нескольки х объекто в пользовательског о
интерфейса, каки м именн о - зависи т от контекст а и имеющейс я в наличи и инфор -
мации.
Проблем а в том, что объект, инициирующий запро с (например, кнопка), не рас-
полагае т информацие й о том, како й объек т в конечно м итог е предостави т справку.
Нам необходи м какой-т о спосо б отделит ь кнопку-инициато р запрос а от объектов,
владеющи х справочно й информацией. Как этог о добиться, показывае т паттер н
цепочк а обязанностей.
Идея заключаетс я в том, чтоб ы разорват ь связ ь межд у отправителям и и полу -
чателями, дав возможност ь обработат ь запро с нескольки м объектам. Запро с пе-
ремещаетс я по цепочк е объектов, пока один из них не обработае т его.
Первый объек т в цепочк е получае т запро с и либо обрабатывае т его сам, либо
направляе т следующем у кандидат у в цепочке, которы й веде т себя точно так же.
У объекта, отправившег о запрос, отсутствуе т информаци я об обработчике. Мы го-
ворим, что у запрос а есть анонимный получатель (implici t receiver).
Паттерн ы поведени я
Частное
Предположим, что пользовател ь запрашивае т справк у по кнопк е Print (пе-
чать). Она находитс я в диалогово м окне Pr intDialog, содержаще м информаци ю
об объект е приложения, котором у принадлежи т (см. предыдущу ю диаграмм у
объектов). На представленно й диаграмм е взаимодействи й показано, как запрос
на получени е справк и перемещаетс я по цепочке.
В данно м случа е ни кнопк а aPrintButton, ни окно aPrintDialo g не обра-
батываю т запрос, он достигае т объект а anApplication, которы й може т его об-
работат ь или игнорировать. У клиента, инициировавшег о запрос, нет прямо й
ссылк и на объект, который его в конце концо в выполнит.
Чтобы отправит ь запро с по цепочк е и гарантироват ь анонимност ь получате -
ля, все объект ы в цепочк е имеют единый интерфей с для обработк и запросо в и для
доступа к своему преемнику (следующем у объект у в цепочке). Например, в систе -
ме оперативно й справк и можно было бы определит ь класс HelpHandle r (предо к
классо в всех объектов-кандидато в или подмешиваемы й клас с (mixi n class) )
с операцие й HandleHelp. Тогда классы, которы е буду т обрабатыват ь запрос, смо-
гут его передат ь своему родителю.
Для обработк и запросо в на получени е справк и класс ы Button, Dialo g
и Applicatio n пользуютс я операциям и HelpHandler. По умолчани ю операци я
HandleHel p прост о перенаправляе т запрос своему преемнику. В подкласса х эта
операци я замещается, так что при благоприятны х обстоятельства х може т выда -
ваться справочна я информация. В противно м случа е запрос отправляетс я дальше
посредство м реализаци и по умолчанию.
Паттерн Chain of Responsibility
Применимость
Используйт е цепочк у обязанностей, когда:
а есть более одног о объекта, способног о обработат ь запрос, приче м настоя -
щий обработчи к заране е неизвесте н и долже н быть найде н автоматически;
а вы хотит е отправит ь запрос одному из нескольки х объектов, не указыва я
явно, какому именно;
а набор объектов, способны х обработат ь запрос, долже н задаватьс я динами -
чески.
Структура
Типична я структур а объектов.
Паттерн ы поведени я
Участники
a Handler (HelpHandler) - обработчик:
- определяе т интерфей с для обработк и запросов;
- (необязательно ) реализуе т связ ь с преемником;
a ConcreteHandler (PrintButton, PrintDialog) - конкретный обработчик:
- обрабатывае т запрос, за которы й отвечает;
- имее т досту п к своему преемнику;
- если ConcreteHandle r способе н обработат ь запрос, то так и делает, если
не может, то направляе т его - его своем у преемнику;
a Clien t - клиент:
- отправляет запрос некоторому объекту ConcreteHandler в цепочке.
Отношения
Когда клиен т инициируе т запрос, он продвигаетс я по цепочке, пока некоторы й
объект ConcreteHandler не возьмет на себя ответственность за его обработку.
Результаты
Паттер н цепочк а обязанносте й имее т следующи е достоинств а и недостатки:
а ослабление связанности. Этот паттер н освобождае т объек т от необходимост и
«знать», кто конкретн о обработае т его запрос. Отправител ю и получател ю ни-
чего неизвестн о дру г о друге, а включенном у в цепочк у объект у - о структу -
ре цепочки.
Таким образом, цепочк а обязанносте й помогае т упростит ь взаимосвяз и
межд у объектами. Вмест о того чтобы хранит ь ссылк и на все объекты, кото -
рые могу т стат ь получателям и запроса, объек т долже н располагат ь инфор -
мацие й лишь о своем ближайше м преемнике;
а дополнительная гибкость при распределении обязанностей между объекта-
ми. Цепочк а обязанносте й позволяе т повысит ь гибкост ь распределени я обя-
занносте й межд у объектами. Добавит ь или изменит ь обязанност и по обра -
ботке запрос а можно, включи в в цепочк у новых участнико в или измени в ее
каким-т о други м образом. Этот подхо д можн о сочетат ь со статически м по-
рождение м подклассо в для создани я специализированны х обработчиков;
а получение не гарантировано. Поскольк у у запрос а нет явног о получателя,
то нет и гарантий, что он вообще буде т обработан: он може т достич ь конц а
цепочк и и пропасть. Необработанны м запро с може т оказатьс я и в случа е не-
правильно й конфигураци и цепочки.
Реализация
При рассмотрени и цепочк и обязанносте й следуе т обратит ь внимани е на
следующи е моменты:
а реализация цепочки преемников. Есть два способ а реализоват ь таку ю цепочку:
- определит ь новые связ и (обычн о это делаетс я в класс е Handler, но можн о
и в ConcreteHandler);
- использоват ь существующи е связи.
Паттерн Chain of Responsibilit y
До сих пор в наших примерах определялись новые связи, однако можно вос-
пользоваться уже имеющимися ссылками на объекты для формирования
цепочки преемников. Например, ссылка на родителя в иерархии «часть-це-
лое» может заодно определять и преемника «части». В структуре виджетов
такие связи тоже могут существовать. В разделе, посвященном паттерну
компоновщик, ссылки на родителей обсуждаются более подробно.
Существующие связи можно использовать, когда они уже поддерживают
нужную цепочку. Тогда мы избежим явного определения новых связей и сэко-
номим память. Но если структура не отражает устройства цепочки обязан-
ностей, то уйти от определения избыточных связей не удастся;
а соединение преемников. Если готовых ссылок, пригодных для определения
цепочки, нет, то их придется ввести. В таком случае класс Handler не толь-
ко определяет интерфейс запросов, но еще и хранит ссылку на преемника.
Следовательно у обработчика появляется возможность определить реали-
зацию операции HandleRequest по умолчанию - перенаправление запро-
са преемнику (если таковой существует). Если подкласс ConcreteHandler
не заинтересован в запросе, то ему и не надо замещать эту операцию, по-
скольку по умолчанию запрос как раз и отправляется дальше.
Вот пример базового класса HelpHandler, в котором хранится указатель
на преемника:
clas s HelpHandle r {
public:
HelpHandler(HelpHandler * s) : _successor(s ) { }
virtua l voi d HandleHelp();
private:
HelpHandler * _successor;
};
void HelpHandler::HandleHel p () {
if (_successor ) {
_successor->HandleHelp();
}
}
а представление запросов. Представлять запросы можно по-разному. В простей-
шей форме, например в случае класса HandleHelp, запрос жестко кодирует-
ся как вызов некоторой операции. Это удобно и безопасно, но переадресовы-
вать тогда можно только фиксированный набор запросов, определенных
в классе Handler.
Альтернатива - использовать одну функцию-обработчик, которой переда-
ется код запроса (скажем, целое число или строка). Так можно поддержать
заранее неизвестное число запросов. Единственное требование состоит в том,'
что отправитель и получатель должны договориться о способе кодирования
запроса.
Это более гибкий подход, но при реализации нужно использовать услов-
ные операторы для раздачи запросов по их коду. Кроме того, не существует
Паттерны поведения
безопасного с точки зрения типов способа передачи параметров, поэтому
упаковывать и распаковывать их приходится вручную. Очевидно, что это
не так безопасно, как прямой вызов операции.
Чтобы решить проблему передачи параметров, допустимо использовать от-
дельные объекты-запросы, в которых инкапсулированы параметры запроса.
Класс Request может представлять некоторые запросы явно, а их новые
типы описываются в подклассах. Подкласс может определить другие пара-
метры. Обработчик должен иметь информацию о типе запроса (какой именно
подкласс Request используется), чтобы разобрать эти параметры.
Для идентификации запроса в классе Request можно определить функцию
доступа, которая возвращает идентификатор класса. Вместо этого получа-
тель мог бы воспользоваться информацией о типе, доступной во время вы-
полнения, если язык программирования поддерживает такую возможность.
Приведем пример функции диспетчеризации, в которой используются
объекты для идентификации запросов. Операция GetKind, указанная в ба-
зовом классе Request, определяет вид запроса:
void Handler::HandleRequest (Request* theRequest) {
switch (theRequest->GetKind()) {
case Help:
// привести аргумент к походящему типу
HandleHelp((HelpRequest* ) theRequest);
break;
case Print:
HandlePrint((PrintRequest* ) theRequest);
// ...
break;
default:
// ...
break;
}
}
Подклассы могут расширить схему диспетчеризации, переопределив опера-
цию HandleRequest. Подкласс обрабатывает лишь те запросы, в которых
заинтересован, а остальные отправляет родительскому классу. В этом слу-
чае подкласс именно расширяет, а не замещает операцию HandleRequest.
Подкласс ExtendedHandler расширяет операцию HandleRequest, опре-
деленную в классе Handler, следующим образом:
class ExtendedHandler : public Handler {
public:
virtual void HandleRequest(Request* theRequest);
clas s ExtendedHandle r : publi c Handle r {
public:
virtua l voi d HandleRequest(Request * theRequest);
// . . .
};
void ExtendedHandler::HandleReques t (Request * theRequest ) {
switc h (theRequest->GetKind() ) {
Паттер н Clhamoftespnsibilit y
cas e Preview:
// обработат ь запро с Previe w
break;
default:
// дат ь класс у Handle r возможност ь обработат ь
// остальны е запрос ы
Handler::HandleRequest(theRequest);
}
}
а автоматическое перенаправление запросов в языке Smalltalk. С этой целью мож-
но использоват ь механизм doesNotUnderstand. Сообщения, не имеющие со-
ответствующих методов, перехватываютс я реализацие й doesNotUnderstand,
которая может быть замещена для перенаправлени я сообщения объекту-пре -
емнику. Поэтому осуществлят ь перенаправлени е вручную необязательно.
Класс обрабатывае т только запросы, в которых заинтересован, и ожидает, что
механизм doesNotUnderstan d выполнит все остальное.
Пример кода
В следующе м примере иллюстрируется, как с помощью цепочки обязаннос-
тей можно обработать запросы к описанной выше системе оперативно й справки.
Запрос на получение справки - это явная операция. Мы воспользуемс я уже имею-
щимися в иерархии виджетов ссылками для перемещения запросов по цепочке от
одного виджета к другому и определим в классе Handler отдельную ссылку, чтобы
можно было передать запрос включенным в цепочку объектам, не являющимс я вид-
жетами.
Класс HelpHandle r определяет интерфейс для обработки запросов на получе-
ние справки. В нем хранится раздел справки (по умолчанию пустой) и ссылка на
преемника в цепочке обработчиков. Основной операцией является HandleHelp,
которая замещается в подклассах. HasHel p - это вспомогательна я операция, про-
веряющая, ассоциирова н ли с объектом какой-нибуд ь раздел:
typede f in t Topic;
cons t Topi c NO_HELP_TOPI C = -1;
clas s HelpHandle r {
public:
HelpHandle r (HelpHandler * = 0, Topi c = NO_HELP_TOPIC ) ;
virtua l boo l HasHelpO;
virtua l voi d SetHandle r (HelpHandler*, Topic);
virtua l voi d HandleHel p ( ) ;
private:
HelpHandler * _successor;
Topi c _topic;
};
HelpHandler::HelpHandle r (
HelpHandler * h, Topi c t
) : _successor(h), _topic(t ) { }
Паттерн ы поведени я
bool HelpHandler::HasHel p () {
retur n _topi c != NO_HELP_TOPIC;
}
void HelpHandler::HandleHel p () {
if („successor != 0) {
_successor->HandleHelp();
}
}
Все виджеты - подклассы абстрактного класса Widget, который, в свою оче-
редь, является подклассом HelpHandler, так как со всеми элементами пользова-
тельского интерфейса может быть ассоциирована справочная информация. (Мож-
но было, конечно, построить реализацию и на основе подмешиваемого класса.)
clas s Widge t : publi c HelpHandle r {
protected:
Widget(Widget * parent, Topic t = NO_HELP_TOPIC);
private:
Widget * _parent;
};
Widget::Widge t (Widget * w, Topi c t) : HelpHandler(w, t) {
_paren t = w;
}
В нашем примере первым обработчиком в цепочке является кнопка. Класс
Button - это подкласс Widget. Конструктор класса Button принимает два па-
раметра - ссылку на виджет, в котором он находится, и раздел справки:
class Button : public Widget {
public:
Button (Widget* d, Topic t = NO_HELP_TOPIC) ;
virtual void HandleHelp ();
// операции класса Widget, которые Button замещает...
};
Реализация HandleHelp в классе Button сначала проверяет, есть ли для
кнопки справочная информация. Если разработчик не определил ее, то запрос
отправляется преемнику с помощью операции HandleHelp класса HelpHandler.
Если же информация есть, то кнопка ее отображает и поиск заканчивается:
Button::Butto n (Widget * h, Topi c t ) : Wi dget(h, t ) { }
void Button::HandleHelp () {
i f (HasHelp() ) {
// предложит ь справк у по кнопк е
} else {
HelpHandler::HandleHelp();
}
}
Паттерн Chain of Responsibility
Класс Dialog реализует аналогичную схему, только его преемником являет-
ся не виджет, а произвольный обработчик запроса на справку. В нашем приложе-
нии таким преемником выступает экземпляр класса Application:
clas s Dialo g : publi c Widge t {
public:
Dialog(HelpHandler * h, Topi c t = NO_HELP_TOPIC);
virtua l voi d HandleHelp();
// операци и класс а Widget, которы е Dialo g замещает...
// . . .
};
Dialog::Dialo g (HelpHandler * h, Topi c t) : Widget(0 ) {
SetHandler(h, t);
}
void Dialog::HandleHel p () {
if (HasHelpO ) {
// предложит ь справк у по диалоговом у окн у
} els e {
HelpHandler::HandleHelp();
}
}
В конце цепочки находится экземпляр класса Appl icat ion. Приложение - это
не виджет, поэтому Application - прямой потомок класса HelpHandler. Если
запрос на получение справки дойдет до этого уровня, то класс Appl icat ion может
выдать информацию о приложении в целом или предложить список разделов:
clas s Applicatio n : publi c HelpHandle r {
public:
Application(Topi c t) : HelpHandler(0, t) { }
virtua l voi d HandleHelp();
// операции, относящиес я к самом у приложению...
};
void Application::HandleHel p () {
// показат ь списо к раздело в справк и
}
Следующий код создает и связывает эти объекты. В данном случае рассмат-
ривается диалоговое окно Print, поэтому с объектами связаны разделы справки,
касающиеся печати:
const Topic PRINT_TOPIC = 1;
const Topic PAPER_ORIENTATION_TOPIC = 2 ;
const Topic APPLICATIONJTOPIC = 3;
Application* application = new Application (APPLICATIONJTOPIC) ;
Dialog* dialog = new Dialog (application, PRINTJTOPIC) ;
Button* button = new Button (dialog, PAPER_ORIENTATION_TOPIC) ;
Паттерны поведения
Мы можем инициировать запрос на получение справки, вызвав операцию
HandleHel p для любого объекта в цепочке. Чтобы начать поиск с объекта кноп-
ки, достаточно выполнить его операцию HandleHelp:
button->HandleHelp();
В этом примере кнопка обрабатывает запрос сразу же. Заметим, что класс
HelpHandler можно было бы сделать преемником Dialog. Более того, его пре-
емника можно изменять динамически. Вот почему, где бы диалоговое окно ни
встретилось, вы всегда получите справочную информацию с учетом контекста.
Известные применения
Паттерн цепочка обязанностей используется в нескольких библиотеках
классов для обработки событий, инициированных пользователем. Класс Handler
в них называется по-разному, но идея всегда одна и та же: когда пользователь щел-
кает кнопкой мыши или нажимает клавишу, генерируется некоторое событие,
которое распространяется по цепочке. В МасАрр [Арр89] и ЕТ++ [WGM88]
класс называется Event Handler, в библиотеке TCL фирмы Symantec [Sym93b]
Bureaucrat, а в библиотеке из системы NeXT [Add94] Responder.
В каркасе графических редакторов Unidraw определены объекты Command,
которые инкапсулируют запросы к объектам Component и Component View
[VL90]. Объекты Command - это запросы, которые компонент или вид компонен-
та могут интерпретировать как команду на выполнение определенной операции.
Это соответствует подходу «запрос как объект», описанному в разделе «Реализа-
ция». Компоненты и виды компонентов могут быть организованы иерархически.
Как компонент, так и его вид могут перепоручать интерпретацию команды своему
родителю, тот - своему родителю и так далее, то есть речь идет о типичной цепоч-
ке обязанностей.
В ЕТ++ паттерн цепочка обязанностей применяется для обработки запро-
сов на обновление графического изображения. Графический объект вызывает опе-
рацию InvalidateRect всякий раз, когда возникает необходимость обновить
часть занимаемой им области. Но выполнить эту операцию самостоятельно гра-
фический объект не может, так как не имеет достаточной информации о своем
контексте, например из-за того, что окружен такими объектами, как Scroller
(полоса прокрутки) или Zoomer (лупа), которые преобразуют его систему коор-
динат. Это означает, что объект может быть частично невидим, так как он оказался
за границей области прокрутки или изменился его масштаб. Поэтому реализация
InvalidateRect по умолчанию переадресует запрос контейнеру, где находится
соответствующий объект. Последний объект в цепочке обязанностей — экземпляр
класса Window. Гарантируется, что к тому моменту, как Window получит запрос,
недействительный прямоугольник будет трансформирован правильно. Window
обрабатывает InvalidateRect, послав запрос интерфейсу оконной системы и тре-
буя тем самым выполнить обновление.
Родственные паттерны
Паттерн цепочка обязанностей часто применяется вместе с паттерном ком-
поновщик. В этом случае родитель компонента может выступать в роли его пре-
емника.
Паттер н Comman d
Паттер н Comman d
Название и классификация паттерна
Команд а - паттер н поведени я объектов.
Назначение
Инкапсулируе т запро с как объект, позволя я тем самым задават ь параметр ы
клиенто в для обработк и соответствующи х запросов, ставит ь запрос ы в очеред ь
или протоколироват ь их, а также поддерживат ь отмен у операций.
Известен также под именем
Actio n (действие), Transactio n (транзакция).
Мотивация
Иногд а необходим о посылат ь объекта м запросы, ничег о не зная о том, выпол -
нение како й операци и запрошен о и кто являетс я получателем. Например, в биб-
лиотеках для построения пользовательски х интерфейсов встречаютс я такие объек-
ты, как кнопк и и меню, которы е посылаю т запро с в ответ на действи е пользователя.
Но в саму библиотек у не заложен а возможност ь обрабатывать этот запрос, так как
тольк о приложение, использующе е ее, располагае т информацие й о том, что следуе т
сделать. Проектировщи к библиотек и не владее т никако й информацие й о получате -
ле запрос а и о том, какие операци и тот долже н выполнить.
Паттер н команд а позволяе т библиотечны м объекта м отправлят ь запрос ы не-
известным объектам приложения, преобразова в сам запрос в объект. Этот объект
можн о хранит ь и передавать, как и любо й другой. В основ е списываемог о паттер -
на лежи т абстрактны й клас с Command, в которо м объявле н интерфей с для выпол -
нения операций. В простейше й свое й форме этот интерфей с состои т из одно й аб-
страктно й операци и Execute. Конкретны е подкласс ы Comman d определяю т пару
«получатель-действие», сохраня я получател я в переменно й экземпляра, и реали -
зуют операци ю Execute, так чтоб ы она посылал а запрос. У получател я есть ин-
формация, необходима я для выполнени я запроса.
С помощь ю объекто в Comman d легк о реализуютс я меню. Кажды й пунк т
меню - это экземпля р класс а Menultem. Сами меню и все их пункт ы создае т клас с
Applicatio n наряд у со всеми остальным и элементам и пользовательског о интер -
фейса. Клас с Appl icat ion отслеживае т также открыты е пользователе м документы.
Паттерны поведения
Приложение конфигурируе т каждый объект Menu It em экземпляром кон-
кретного подкласса Command. Когда пользователь выбирает некоторый пункт меню,
ассоциированный с ним объект Menultem вызывает Execut e для своего объекта-
команды, a Execut e выполняет операцию. Объекты Menultem не имеют инфор-
мации, какой подкласс класса Command они используют. Подклассы Command хра-
нят информацию о получателе запроса и вызывают одну или несколько операций
этого получателя.
Например, подкласс PasteComman d поддерживает вставку текста из буфера
обмена в документ. Получателем для PasteComman d является Document, кото-
рый был передан при создании объекта. Операция Execut e вызывает операцию
Paste документа-получателя.
Для подкласса OpenCommand операция Execute ведет себя по-другому: она
запрашивает у пользователя имя документа, создает соответствующи й объект
Document, извещает о новом документе приложение-получател ь и открывает этот
документ.
Иногда объект Menulte m должен выполнит ь последовательность команд.
Например, пункт меню для центрирования страницы стандартног о размера мож-
но было бы сконструироват ь сразу из двух объектов: CenterDocumentComman d
и Normals!zeCommand. Поскольку такое комбинировани е команд- явление
Паттерн Comman d
обычное, то мы можем определить класс MacroCommand, позволяющий объекту
Menultem выполнять произвольное число команд. MacroCommand - это конкрет-
ный подкласс класса Command, который просто выполняет последовательност ь
команд. У него нет явного получателя, поскольку для каждой команды определен
свой собственный.
Обратите внимание, что в каждом из приведенных примеров паттерн коман-
да отделяет объект, инициирующий операцию, от объекта, который «знает», как
ее выполнить. Это позволяет добиться высокой гибкости при проектировании
пользовательског о интерфейса. Пункт меню и кнопка одновременно могут быть
ассоциированы в приложении с некоторой функцией, для этого достаточно при-
писать обоим элементам один и тот же экземпляр конкретного подкласса класса
Command. Мы можем динамически подменять команды, что очень полезно для
реализации контекстно-зависимы х меню. Можно также поддержать сценарии,
если компоновать простые команды в более сложные. Все это выполнимо потому,
что объект, инициирующий запрос, должен располагать информацией лишь о том,
как его отправить, а не о том, как его выполнить.
Применимость
Используйте паттерн команда, когда хотите:
а параметризоват ь объекты выполняемым действием, как в случае с пункта-
ми меню Menultem. В процедурном языке такую параметризацию можно
выразить с помощью функции обратного вызова, то есть такой функции, ко-
торая регистрируется, чтобы быть вызванной позднее. Команды представ-
ляют собой объектно-ориентированну ю альтернативу функциям обратного
вызова;
а определять, ставить в очередь и выполнять запросы в разное время. Время
жизни объекта Command необязательно должно зависеть от времени жизни
исходного запроса. Если получателя запроса удается реализовать так, что-
бы он не зависел от адресного пространства, то объект-команду можно пе-
редать другому процессу, который займется его выполнением;
Паттерн ы поведени я
а поддержат ь отмен у операций. Операци я Execut e объект а Comman d може т
сохранит ь состояние, необходимо е для откат а действий, выполненны х ко-
мандой. В этом случа е в интерфейс е класс а Comman d должн а быть допол -
нительна я операци я Unexecute, котора я отменяе т действия, выполненны е
предшествующи м обращение м к Execute. Выполненны е команд ы хранят -
ся в списк е истории. Для реализаци и произвольног о числ а уровне й отмен ы
и повтор а коман д нужн о обходит ь этот списо к соответственн о в обратно м
и прямо м направлениях, вызыва я при посещени и каждог о элемент а коман -
ду Unexecute или Execute;
а поддержат ь протоколировани е изменений, чтоб ы их можн о было выпол -
нить повторн о посл е аварийно й остановк и системы. Дополни в интерфей с
класс а Comman d операциям и сохранени я и загрузки, вы сможет е вест и про-
токол изменени й во внешне й памяти. Для восстановлени я после сбоя нуж-
но буде т загрузит ь сохраненны е команд ы с диск а и повторн о выполнит ь их
с помощью операции Execute;
а структурироват ь систем у на основ е высокоуровневы х операций, построен -
ных из примитивных. Така я структур а типичн а для информационны х сис-
тем, поддерживающи х транзакции. Транзакци я инкапсулируе т набор изме -
нений данных. Паттер н команд а позволяе т моделироват ь транзакции. У всех
коман д есть общи й интерфейс, что дает возможност ь работат ь одинаков о
с любым и транзакциями. С помощь ю этог о паттерн а можн о легко добавлят ь
в систем у новые виды транзакций.
Структура
Участники
a Command - команда:
- объявляе т интерфей с для выполнени я операции;
a ConcreteComman d (PasteCommand, OpenCommand ) - конкретна я команда:
- определяет связь между объектом-получателе м Receiver и действием;
- реализуе т операци ю Execut e путе м вызов а соответствующи х операци й
объекта Receiver;
a Client (Application) - клиент:
- создае т объек т класс а ConcreteComman d и устанавливае т его получателя;
Паттерн Comman d
a Invoker (Menultem) - инициатор:
- обращается к команде для выполнения запроса;
a Receiver (Document, Application) - получатель:
- располагает информацией о способах выполнения операций, необходи-
мых для удовлетворения запроса. В роли получателя может выступать
любой класс.
Отношения
а клиент создает объект ConcreteCommand и устанавливает для него полу-
чателя;
а инициатор Invoker сохраняет объект ConcreteCommand;
а инициатор отправляет запрос, вызывая операцию команды Execute. Если
поддерживается отмена выполненных действий, то ConcreteCommand пе-
ред вызовом Execute сохраняет информацию о состоянии, достаточную
для выполнения отката;
а объект ConcreteCommand вызывает операции получателя для выполнения
запроса.
На следующей диаграмме видно, как Command разрывает связь между иници-
атором и получателем (а также запросом, который должен выполнить последний).
Результаты
Результаты применения паттерна команда таковы:
а команда разрывает связь между объектом, инициирующим операцию, и объ-
ектом, имеющим информацию о том, как ее выполнить;
а команды - это самые настоящие объекты. Допускается манипулировать ими
и расширять их точно так же, как в случае с любыми другими объектами;
а из простых команд можно собирать составные, например класс MacroCommand,
рассмотренный выше. В общем случае составные команды описываются
паттерном компоновщик;
а добавлять новые команды легко, поскольку никакие существующие классы
изменять не нужно.
Паттерн ы поведени я
Реализация
При реализаци и паттерн а команд а следуе т обратит ь внимани е на следующи е
аспекты:
а насколько «умной» должна быть команда. У команд ы може т быть широки й
круг обязанностей. На одном полюс е стоит просто е определени е связ и межд у
получателе м и действиями, которы е нужн о выполнит ь для удовлетворени я
запроса. На друго м - реализаци я всего самостоятельно, без обращени я за по-
мощь ю к получателю. Последни й вариан т полезен, когда вы хотит е опреде -
лить команды, не зависящи е от существующи х классов, когд а подходящег о
получател я не существуе т или когда получател ь команд е точно не известен.
Например, команда, создающа я новое окно приложения, може т не понимать,
что именн о она создает, а трактоват ь окно, как любо й друго й объект. Где-т о
посередин е межд у двумя крайностям и находятс я команды, обладающи е до-
статочно й информацие й для динамическог о обнаружени я своег о получателя;
и поддержка отмены и повтора операций. Команд ы могу т поддерживат ь отмен у
и повто р операций, если имеетс я возможност ь отменит ь результат ы выполне -
ния (например, операци ю Unexecut e или Undo). В класс е ConcreteComman d
може т сохранятьс я необходима я для этог о дополнительна я информация,
в том числе:
- объект-получател ь Receiver, которы й выполняе т операци и в отве т на
запрос;
- аргумент ы операции, выполненно й получателем;
- исходны е значени я различны х атрибуто в получателя, которы е могл и из-
менитьс я в результат е обработк и запроса. Получател ь долже н предоста -
вить операции, позволяющи е команд е вернутьс я в исходно е состояние.
Для поддержк и всег о одног о уровн я отмен ы приложени ю достаточн о сохра -
нять тольк о последню ю выполненну ю команду. Если же нужн ы многоуров -
невые отмен а и повто р операций, то придетс я вест и список истории выпол -
ненны х команд. Максимальна я длин а этог о списк а и определяе т числ о
уровне й отмен ы и повтора. Прохо д по списк у в обратно м направлени и и от-
кат результато в всех встретившихс я ito пут и коман д отменяе т их действие;
прохо д в прямо м направлени и и выполнени е встретившихс я коман д приво -
дит к повтор у действий.
Команду, допускающу ю отмену, возможно, придетс я скопироват ь пере д по-
мещение м в списо к истории. Дело в том, что объек т команды, использован -
ный для доставк и запроса, скаже м от пункт а меню Men u It em, позже мог
быть использова н для други х запросов. Поэтом у копировани е необходимо,
чтобы определит ь разные вызов ы одно й и той же команды, если ее состоя -
ние при любо м вызов е може т изменяться.
Например, команд а DeleteCoinmand, котора я удаляе т выбранны е объекты,
при каждо м вызов е должн а сохранят ь разны е набор ы объектов. Поэтом у
объек т DeleteComman d необходим о скопироват ь посл е выполнения, а ко-
пию поместит ь в списо к истории. Если в результат е выполнени я состояни е ко-
манд ы никогд а не изменяется, то копироват ь не нужн о - в списо к достаточн о
Паттерн Comman d
поместить лишь ссылку на команду. Команды, которые обязательно нужно
копировать перед помещением в список истории, ведут себя подобно про-
тотипам (см. описание паттерна прототип);
а как избежать накопления ошибок в процессе отмены. При обеспечении на-
дежного, сохраняющего семантику механизма отмены и повтора может воз-
никнуть проблема гистерезиса. При выполнении, отмене и повторе команд
иногда накапливаются ошибки, в результате чего состояние приложения
оказывается отличным от первоначального. Поэтому порой необходимо со-
хранять в команде больше информации, дабы гарантировать, что объекты
будут целиком восстановлены. Чтобы предоставить команде доступ к этой
информации, не раскрывая внутреннего устройства объектов, можно вос-
пользоваться паттерном хранитель;
а применение шаблонов в C++. Для команд, которые не допускают отмену
и не имеют аргументов, в языке C++ можно воспользоваться шаблонами, что-
бы не создавать подкласс класса Command для каждой пары действие-полу-
чатель. Как это сделать, мы продемонстрируем в разделе «Пример кода».
Пример кода
Приведенный ниже код на языке C++ дает представление о реализации клас-
сов Command, обсуждавшихся в разделе «Мотивация». Мы определим классы
OpenCommand, PasteCommand и MacroCommand. Сначала абстрактный класс
Command:
clas s Comman d {
public:
virtua l ~Comman d ();
virtua l void Execut e () = 0;
protected:
Comman d ( ) ;
};
Команда OpenCommand открывает документ, имя которому задает пользова-
тель. Конструктору OpenCommand передается объект Application. Функция
AskUser запрашивает у пользователя имя открываемого документа:
class OpenComman d : public Comman d {
public :
OpenCommand (Application*) ;
virtual void Execute ( );
protected:
virtual const char* AskUser ();
private:
Application* _application;
char* _response;
};
OpenCommand::OpenComman d (Application * a) {
_applicatio n = a;
}
Паттерны поведения
void OpenCommand::Execut e () {
cons t char * nam e = AskUser();
if (nam e != 0) {
Document * documen t = new Document(name);
_application->Add(document);
document->0pen( ) ;
}
}
Команде PasteCommand в конструкторе передается объект Document, явля-
ющийся получателем:
class PasteComman d : public Comman d {
public:
PasteCommand(Document*);
virtual void ExecuteO;
private:
Document * „document;
};
PasteCommand::PasteComman d (Document * doc ) {
_documen t = doc;
}
void PasteCommand::Execute () {
_document->Paste();
}
В случае с простыми командами, не допускающими отмены и не требующими
аргументов, можно воспользоваться шаблоном класса для параметризации по-
лучателя. Определим для них шаблонный подкласс SimpleCoiranand, который па-
раметризуется типом получателя Receiver и хранит связь между объектом-полу-
чателем и действием, представленным указателем на функцию-член:
templat e <clas s Receiver >
clas s SimpleCoiranan d : publi c Comman d {
public:
typede f voi d (Receiver::* Action)();
SimpleCommand(Receiver * r, Actio n a) :
_receiver(r), _action(a ) { }
virtua l voi d ExecuteO;
private:
Actio n _action;
Receiver * _receiver;
};
Конструктор сохраняет информацию о получателе и действии в соответству-
ющих переменных экземпляра. Операция Execute просто выполняет действие
по отношению к получателю:
templat e <clas s Receiver >
void SimpleCommand<Receiver>::Execut e () {
(_receiver->*_action)();
}
Паттер н Comman d
Чтобы создать команду, которая вызывает операцию Action для экземпляра
класса MyClass, клиент пишет следующий код:
MyClass* receiver = new MyClass;
// ...
Command * aComman d =
new SimpleCommand<MyClass>(receiver, &MyCl ass::Act i on);
// ...
aCommand->Execute();
Имейте в виду, что такое решение годится только для простых команд. Для
более сложных команд, которые отслеживают не только получателей, но и аргу-
менты и, возможно, состояние, необходимое для отмены операции, приходится
порождать подклассы от класса Command.
Класс Macr©Command управляет выполнением последовательности подко-
манд и предоставляет операции для добавления и удаления подкоманд. Задавать
получателя не требуется, так как в каждой подкоманде уже определен свой полу-
чатель:
class MacroComman d : public Comman d {
public:
MacroCommand();
virtual -MacroCommand();
virtual void Add(Command*);
virtual void Remove(Command*);
virtual void Execut e();
private:
List<Command*>* _cmds;
};
Основой класса MacroCommand является его функция-член Execute. Она об-
ходит все подкоманды и для каждой вызывает ее операцию Execute:
void MacroCommand::Execute () {
ListIterator<Command*> i (_cmds);
for (i. First { ); !i.IsDone(); i.Next O) {
Command* с = i.Currentl tem();
c->Execute();
}
}
Обратите внимание, что если бы в классе MacroCommand была реализована
операция отмены Unexecute, то при ее выполнении подкоманды должны были
бы отменяться в порядке, обратном тому, который применяется в реализации
Execute.
Наконец, в классе MacroCommand должны быть операции для добавления и уда-
ления подкоманд:
void MacroCommand::Ad d (Command * с) {
_cmds->Append(c) ;
}
Паттерны поведения
void MacroCommand::Remov e (Command * c) {
_cmds->Remove(c);
}
Известные применения
Быть может, впервые паттерн команда появился в работе Генри Либермана
(Henry Lieberman) [Lie85]. В системе МасАрр [Арр89] команды широко приме-
няются для реализации допускающих отмену операций. В ЕТ++ [WGM88], Inter-
Views [LCI+92] и Unidraw [VL90] также имеются классы, описываемые паттерном
команда. Так, в библиотеке Interviews определен абстрактный класс Action, ко-
торый определяет всю функциональность команд. Есть и шаблон ActionCallback,
параметризованный действием Action, который автоматически инстанцирует
подклассы команд.
В библиотеке классов THINK [Sym93b] также используются команды для под-
держки отмены операций. В THINK команды называются задачами (Tasks). Объек-
ты Task передаются по цепочке обязанностей, пока не будут кем-то обработаны.
Объекты команд в каркасе Unidraw уникальны в том отношении, что могут вес-
ти себя подобно сообщениям. В Unidraw команду можно послать другому объекту
для интерпретации, результат которой зависит от объекта-получателя. Более того,
сам получатель может делегировать интерпретацию следующему объекту, обычно
своему родителю. Это напоминает паттерн цепочка обязанностей. Таким образом,
в Unidraw получатель вычисляется, а не хранится. Механизм интерпретации в Uni-
draw использует информацию о типе, доступную во время выполнения.
Джеймс Коплиен описывает, как в языке C++ реализуются функторы - объек-
ты, ведущие себя, как функции [Сор92]. За счет перегрузки оператора вызова
operator () он становится более понятным. Смысл паттерна команда в другом -
он устанавливает и поддерживает связь между получателем и функцией (то есть
действием), а не просто функцию.
Родственные паттерны
Паттерн компоновщик можно использовать для реализации макрокоманд.
Паттерн хранитель иногда проектируется так, что сохраняет состояние ко-
манды, необходимое для отмены ее действия.
Команда, которую нужно копировать перед помещением в список истории, ве-
дет себя, как прототип.
Паттер н Interprete r
Название и классификация паттерна
Интерпретатор - паттерн поведения классов.
Назначение
Для заданного языка определяет представление его грамматики, а также ин-
терпретатор предложений этого языка.
Паттер н Interprete r
Мотивация
Если некотора я задач а возникае т часто, то имее т смыс л представит ь ее кон-
кретны е проявлени я в виде предложени й на просто м языке. Зате м можн о буде т
создат ь интерпретатор, которы й решае т задачу, анализиру я предложени я этог о
языка.
Например, поис к стро к по образц у - весьм а распространенна я задача. Регуляр -
ные выражени я - это стандартны й язык для задани я образцо в поиска. Вмест о того
чтобы программироват ь специализированны е алгоритм ы для сопоставлени я стро к
с кажды м образцом, не проще ли построит ь алгорит м поиск а так, чтоб ы он мог ин-
терпретироват ь регулярно е выражение, описывающе е множеств о строк-образцов?
Паттер н интерпретато р определяе т грамматик у простог о языка, представля -
ет предложени я на этом язык е и интерпретируе т их. Для приведенног о пример а
паттер н описывае т определени е грамматик и и интерпретаци и язык а регулярны х
выражений.
Предположим, что они описан ы следующе й грамматикой:
expression ::= literal | alternation | sequence | repetition |
1 (' expression ')'
alternation ::= expression ' | ' expression
sequence ::= expression '&' expression
repetition ::= expression '*'
literal ::= 'a1 | 'b' | 'c' | ... { 'a1 | 'b' | 'c1 | ... }*
где expression - это начальны й символ, a literal - терминальны й символ,
определяющи й просты е слова.
Паттер н интерпретато р используе т клас с для представлени я каждог о пра -
вила грамматики. Символ ы в право й част и правил а - это переменны е экземпля -
ров таки х классов. Для представлени я приведенно й выше грамматик и требуетс я
пят ь классов: абстрактны й клас с RegularExpressio n и четыр е его подкласс а
LiteralExpression, AlternationExpression, SequenceExpression
и RepetitionExpression. В последни х тре х подкласса х определен ы перемен -
ные для хранени я подвыражений.
Паттерны поведения
Каждое регулярное выражение, описываемо е этой грамматикой, представля -
ется в виде абстрактного синтаксического дерева, в узлах которого находятся эк-
земпляры этих классов. Например, дерево
представляет выражение
rainin g & (dog s | cats ) *
Мы можем создать интерпретато р регулярных выражений, определив в каждом
подклассе RegularExpression операцию Interpret, принимающу ю в качестве
аргумента контекст, где нужно интерпретироват ь выражение. Контекст состоит
из входной строки и информации о том, как далеко по ней мы уже продвинулись.
В каждом подклассе RegularExpression операция Interpret производит со-
поставление с оставшейс я частью входной строки. Например:
a LiteralExpression проверяет, соответствует ли входная строка литера-
лу, который хранится в объекте подкласса;
Q AlternationExpressio n проверяет, соответствуе т ли строка одной из
альтернатив;
Q RepetitionExpression проверяет, если в строке повторяющиеся вхож-
дения выражения, совпадающег о с тем, что хранится в объекте.
И так далее.
Применимость
Используйт е паттерн интерпретатор, когда есть язык для интерпретации,
предложения которого можно представит ь в виде абстрактных синтаксически х
деревьев. Лучше всего этот паттерн работает, когда:
Паттерн Interprete r
а грамматик а проста. Для сложны х граммати к иерархи я классо в становитс я
слишко м громоздко й и неуправляемой. В таких случая х лучше применят ь
генератор ы синтаксически х анализаторов, поскольк у они могу т интерпре -
тироват ь выражения, не строя абстрактны х синтаксически х деревьев, что
экономи т память, а возможно, и время;
а эффективност ь не являетс я главны м критерием. Наиболе е эффективны е
интерпретатор ы обычн о не работают непосредственн о с деревьями, а снача -
ла транслирую т их в другу ю форму. Так, регулярно е выражени е часто пре-
образуют в конечны й автомат. Но даже в этом случа е сам транслятор мож-
но реализовать с помощью паттерна интерпретатор.
Структур а
Участники
a AbstractExpressio n (RegularExpression ) - абстрактно е выражение:
- объявляе т абстрактну ю операци ю Interpret, общу ю для всех узлов в аб-
страктно м синтаксическо м дереве;
a TerminalExpressio n (LiteralExpression ) - терминально е выражение:
- реализует операци ю Interpret для терминальны х символо в грамматики;
- необходи м отдельны й экземпля р для каждог о терминальног о символ а
в предложении;
Q NonterminaIExpression(AlternationExpression,RepetitionExpression,
SequenceExpressions) - нетерминальное выражение:
- по одному такому класс у требуетс я для каждог о грамматическог о прави -
ла R :: = Rl R2... Rп;
- хранит переменные экземпляра типа AbstractExpression для каждо-
го символ а от Rl до Rп;
- реализует операци ю Interpret для нетерминальны х символов грамма -
тики. Эта операци я рекурсивн о вызывае т себя же для переменных, пред-
ставляющи х ./?,,... /?п;
р Context - контекст:
- содержи т информацию, глобальну ю по отношени ю к интерпретатору;
Паттерн ы поведени я
a Clien t - клиент:
- строи т (или получае т в готово м виде ) абстрактно е синтаксическо е дерево,
представляюще е отдельно е предложени е на язык е с данно й грамматикой.
Дерев о составлен о из экземпляро в классо в Nonterminal-Expressio n
и Terminal-Expression;
- вызывае т операци ю Interpret.
Отношения
а клиен т строи т (или получае т в готово м виде ) предложени е в виде абстракт -
ного синтаксическог о дерева, в узла х которог о находятс я объект ы классо в
NonterminalExpressio n и Terminal-Expression. Зате м клиен т иници -
ализируе т контекс т и вызывае т операци ю Interpret;
а в каждо м узле вида NonterminalExpression чере з операци и Interpret
определяетс я операци я Interpret для каждог о подвыражения. Для класс а
TerminalExpression операци я Interpret определяе т базу рекурсии;
а операци и Interpret в каждо м узле использую т контекс т для сохранени я
и доступ а к состояни ю интерпретатора.
Результаты
У паттерн а интерпретато р есть следующи е достоинств а и недостатки:
а грамматику легко изменять и расширять. Поскольк у для представлени я
грамматически х прави л в паттерн е используютс я классы, то для изменени я
- или расширени я грамматик и можн о применят ь наследование. Существую -
щие выражени я можн о модифицироват ь постепенно, а новые определят ь
как вариаци и старых;
а простая реализация грамматики. Реализаци и классов, описывающи х узлы
абстрактног о синтаксическог о дерева, похожи. Такие класс ы легко кодиро -
вать, а зачасту ю их може т автоматическ и сгенерироват ь компилято р или
генерато р синтаксически х анализаторов;
а сложные грамматики трудно сопровождать. В паттерн е интерпретато р
определяетс я по меньше й мере один клас с для каждог о правил а граммати -
ки (для правил, определенны х с помощь ю формы Бэкуса-Наур а - BNF, мо-
жет понадобитьс я и боле е одног о класса). Поэтом у сопровождени е грамма -
тики с больши м число м прави л иногд а оказываетс я трудно й задачей. Для
ее решени я могу т быт ь применен ы други е паттерн ы (см. разде л «Реализа -
ция»). Но если грамматик а очен ь сложна, лучше прибегнут ь к други м мето-
дам, наприме р воспользоватьс я генераторо м компиляторо в или синтакси -
ческих анализаторов;
а добавление новых способов интерпретации выражений. Паттер н интерпре -
татор позволяе т легко изменит ь спосо б вычислени я выражений. Например,
реализоват ь красиву ю печат ь выражени я вмест о проверк и входящи х в него
типо в можно, прост о определи в нову ю операци ю в класса х выражений.
Если вам приходитс я част о создават ь новые способ ы интерпретаци и выра -
жений, подумайт е о применени и паттерн а посетитель. Это поможе т избе -
жать изменени я классов, описывающи х грамматику.
Паттер н Interprete r
Реализация
У реализаци й паттерно в интерпретато р и компоновщи к есть мног о общего.
Следующи е вопрос ы относятс я тольк о к интерпретатору:
а создание абстрактного синтаксического дерева. Паттер н интерпретато р не
поясняет, как создавать дерево, то есть разбо р выражени я не входи т в его
задачу. Создат ь дерев о разбор а може т таблично-управляемы й или написан -
ный вручну ю (обычн о методо м рекурсивног о спуска ) анализатор, а также
сам клиент;
а определение операции Interpret. Определят ь операци ю Interpret в класса х
выражени й необязательно. Если создават ь новые интерпретатор ы прихо -
дитс я часто, то лучше воспользоватьс я паттерно м посетител ь и поместит ь
операци ю Interpret в отдельны й объект-посетитель. Например, для грам-
матик и язык а программировани я буде т нужн о определить мног о операци й
над абстрактным и синтаксическим и деревьями: проверк у типов, оптимиза -
цию, генераци ю кода и т.д. Лучше, конечно, использоват ь посетител я и не
определят ь эти операци и в каждо м класс е грамматики;
а разделение терминальных символов с помощью паттерна приспособленец.
Для грамматик, предложени я которы х содержа т мног о вхождени й одног о
и того же терминальног о символа, може т оказатьс я полезны м разделени е
этого символа. Хороши м примеро м служа т грамматик и компьютерны х про-
грамм, поскольк у в них кажда я переменна я встречаетс я в коде многократ -
но. В пример е из раздел а «Мотивация » терминальны й симво л dog (для мо-
делировани я которог о используетс я клас с LiteralExpression ) може т
попадатьс я мног о раз.
В терминальны х узла х обычн о не хранитс я информаци я о положени и в абст-
рактно м синтаксическо м дереве. Необходимы й для интерпретаци и контекс т
предоставляю т им родительски е узлы. Налиц о различи е межд у разделяемы м
(внутренним ) и передаваемы м (внешним ) состояниями, так что вполн е при-
меним паттер н приспособленец.
Например, кажды й экземпля р класс а LiteralExpression для dog полу -
чает контекст, состоящи й из уже просмотренно й част и строки. И кажды й
такой экземпля р делае т в свое й операци и Interpret одно и то же - прове -
ряет, содержи т ли остато к входно й строк и слов о dog, - безотносительн о
к тому, в како м мест е дерев а этот экземпля р встречается.
Пример кода
Мы приведе м два примера. Первы й - законченна я программ а на Smalltal k для
проверк и того, соответствуе т ли данна я последовательност ь регулярном у выра -
жению. Второ й - программ а на C++ для вычислени я булевы х выражений.
Программ а сопоставлени я с регулярны м выражение м проверяет, являетс я ли
строк а корректны м предложение м языка, определяемог о этим выражением. Регу -
лярно е выражени е определен о следующе й грамматикой:
expression ::= literal I alternation I sequence I repetition I
'(' expression ')'
Паттерны поведения
.alternatio n ::= expressio n 'Г expressio n
sequenc e ::= expressio n '&' expressio n
repetitio n ::= expressio n 'repeat'
litera l ::= 'a 1 | 'b' | 'c 1 | ... { 'a 1 I 'b 1 I 'c' I ... }*
Между этой грамматикой и той, что приведена в разделе «Мотивация», есть
небольшие отличия. Мы слегка изменили синтаксис регулярных выражений, по-
скольку в Smalltal k символ * не может быть постфиксной операцией. Поэтому
вместо него мы употребляе м слово repeat. Например, регулярное выражение
(('do g ' | 'cat ') repeat & 'weather')
соответствует входной строке 'dog dog cat weather'.
Для реализации программы сопоставлени я мы определим пять классов, упо-
мянутых на стр. 237. В классе SequenceExpressio n есть переменные экземпля-
ра expression1 и expression2 для хранения ссылок на потомков в дереве.
Класс AlternationExpressio n хранит альтернатив ы в переменных экземпляра
alternativel и alternative2, а класс RepetitionExpression - повторяемое
выражение в переменной экземпляра repetition. В классе LiteralExpression
есть переменная экземпляр а component s для хранения списка объектов (скорее
всего, символов), представляющи х литеральну ю строку, которая должна соответ-
ствовать входной строке.
Операция match: реализует интерпретато р регулярных выражений. В каж-
дом из классов, входящих в абстрактное синтаксическо е дерево, эта операция ре-
ализована. Ее аргументом является переменна я inputState, описывающа я те-
кущее состояние процесса сопоставления, то есть уже прочитанну ю часть входной
строки.
Текущее состояние характеризуетс я множеством входных потоков, представ-
ляющим множество тех входных строк, которое регулярное выражение могло бы
к настоящему моменту принять. (Это примерно то же, что регистрация всех со-
стояний, в которых побывал бы эквивалентный конечный автомат, распознавши й
входной поток до данного места.)
Текущее состояние наиболее важно для операции repeat. Например, регу-
лярному выражению
1 а' repeat
интерпретато р сопоставил бы строки "а", "аа", "ааа" и т.д. А регулярному вы-
ражению
1 а' repeat & ' be '
строки " abc", " aabc", " aaabc" и т.д. Но при наличии регулярног о выражения
'а' repeat & 'abc'
сопоставление входной строки "aabc" с подвыражением " 'a' repeat" дало
бы два потока, один из которых соответствуе т одному входному символу, а дру-
гой - двум. Из них лишь поток, принявший один символ, может быть сопостав-
лен с остатком строки " abc".
Паттер н Interprete r
Теперь рассмотрим определения match: для каждог о класса, описывающег о
регулярное выражение. SequenceExpressio n производит сопоставлени е с каж-
дым подвыражение м в определенно й последовательности. Обычно потоки ввода
не входят в его состояние input State.
match: inputState
^ expression2 match: (expressionl match: inputState).
AlternationExpression возвращает результат, объединяющий состояния
всех альтернатив. Вот определение match: для этого случая:
match: inputState
| finalState |
finalState := alternative1 match: inputState.
finalState addAll: (alternative2 match: inputState).
^ finalState
Операция match: для RepetitionExpressio n пытается найти максималь -
ное количество состояний, допускающи х сопоставление:
match: inputState
| aState final State |
aState := inputState.
finalState := inputState copy.
[aState isEmpty]
whileFalse:
[aState := repetition match: aState.
finalState addAll: aState].
^ fi nal State
На выходе этой операции мы обычно получаем больше состояний, чем на вхо-
де, поскольку RepetitionExpressio n может быть сопоставлено с одним, дву-
мя или более вхождениями повторяющегос я выражения во входную строку. В вы-
ходном состоянии представлены все возможные варианты, а решение о том, какое
из состояний правильно, принимаетс я последующими элементами регулярног о
выражения.
Наконец, операция match: для LiteralExpression сравнивает свои ком-
поненты с каждым возможным входным потоком и оставляет только те из них,
для которых попытка завершилась удачно:
match: inputStat e
I finalStat e tStrea m I
finalStat e := Se t new.
inputStat e
do:
[:strea m I tStrea m := strea m copy.
(tStrea m nextAvailable:
component s siz e
) = component s
ifTrue: [finalStat e add: tStream ]
].
^ finalStat e
Паттерны поведения
Сообщение next Available: выполняет смещение вперед по входному по-
току. Это единственна я операци я match:, котора я сдвигаетс я по потоку. Обрати -
те внимание: возвращаемо е состояни е содержи т его копию, поэтом у можн о быт ь
уверенным, что сопоставлени е с литерало м никогд а не изменяе т входно й поток.
Это существенно, поскольку все альтернативы в Alternat ionExpression долж-
ны «видеть » одинаковы е копи и входног о потока.
Таки м образом, мы определил и классы, составляющи е абстрактно е синтакси -
ческо е дерево. Тепер ь опишем, как его построить. Вмест о тог о чтоб ы создават ь
анализато р регулярны х выражений, мы определи м некоторы е операци и в класса х
RegularExpression, так чт о вычислени е выражени я язык а Smalltal k приведе т
к создани ю абстрактног о синтаксическог о дерев а для соответствующег о регуляр -
ног о выражения. Тем самы м мы буде м использоват ь встроенны й компилято р
Smalltalk, ка к если бы это был анализато р синтаксис а регулярны х выражений.
Для построени я дерев а нам понадобятс я операци и " I ", "repeat" и "&" над регу -
лярным и выражениями. Определи м эти операци и в класс е RegularExpression:
& aNode
^ SequenceExpression new
expressionl: self expression2: aNode asRExp
repeat
^ RepetitionExpression new repetition: self
| aNode
^ AlternationExpression new
alternativel: self alternative2: aNode asRExp
asRExp
^ self
Операция asRExp преобразует литералы в RegularExpression. Следующие
операци и определен ы в класс е String:
& aNode
^ SequenceExpression new
expressionl: self asRExp expression2: aNode asRExp
repeat
^ RepetitionExpression new repetition: self
| aNode
^ AlternationExpression new
alternativel: self asRExp al ternative2: aNode asRExp
asRExp
^ LiteralExpression new components: self
Если бы мы определил и эти операци и выше в иерархи и классов, наприме р
SequenceableCollection в Smalltalk-80, IndexedCollection в Smalltalk/V,
то они появились бы и в таких классах, как Array и OrderedCollection. Это
позволил о бы сопоставлят ь регулярны е выражени я с последовательностям и
объекто в любог о вида.
Второ й приме р - это систем а для манипулировани я и вычислени я булевы х
выражений, реализованна я на C++. Терминальным и символам и в этом язык е яв-
ляютс я булев ы переменные, то есть констант ы true и false. Нетерминальны е
Паттер н Interprete r
символ ы представляю т выражения, содержащи е оператор ы and, or и not. При -
веде м определени е грамматики:1
BooleanEx p ::= VariableEx p I Constan t I OrEx p I AndEx p I NotEx p I
' ( ' BooleanEx p ' ) '
AndEx p ::= BooleanEx p 'and' BooleanEx p
OrEx p : : = BooleanEx p ' or ' BooleanEx p
NotEx p ::= 'not' BooleanEx p
Constan t ::= 'true' I 'false 1
VariableEx p ::= 'A' I 'B' I ... |.'X' | 'Y1 I 'Z'
Определим две операции над булевыми выражениями. Первая - Evaluate -
вычисляет выражение в контексте, где каждой переменной присваивается истин-
ное или ложное значение. Вторая - Replace - порождает новое булево выраже-
ние, заменяя выражением некоторую переменную. Эта операция демонстрирует,
что паттерн интерпретатор можно использовать не только для вычисления вы-
ражений; в данном случае он манипулирует самим выражением.
Здесь мы подробно опишем только классы BooleanExp, VariableExp
и AndExp. Классы OrExp и NotExp аналогичны классу AndExp. Класс Constant
представляет булевы константы.
В классе BooleanExp определен интерфейс всех классов, которые описыва-
ют булевы выражения:
clas s BooleanEx p {
public:
BooleanEx p ( ) ;
virtua l -BooleanEx p ();
virtua l boo l Evaluat e (Contextk ) = 0;
virtua l BooleanExp * Replac e (cons t char*, BooleanExp& ) = 0;
virtual BooleanExp * Copy ( ) const = 0;
};
Класс Context определяет отображение между переменными и булевыми зна-
чениями, которые в C++ представляются константами true и false. Интерфейс
этого класса следующий:
clas s Contex t {
public:
bool Looku p (cons t char* ) const;
void Assig n (VariableExp*, bool);
};
Класс VariableExp представляет именованную переменную:
class VariableExp : public BooleanExp {
public:
VariableExp(const char*);
virtual -Vari abl eExp();
' Упрощая задачу, мы игнорируем приоритеты операторов и предполагаем, что их учет возложен на
объект, строящий дерево разбора.
Паттерн ы поведени я
virtua l boo l Evaluate(Contexts);
virtua l BooleanExp * Replace(cons t char*, BooleanExp&);
virtua l BooleanExp * Copy( ) const;
private:
char * _name;
};
Конструктор класса принимае т в качестве аргумента имя переменной:
VariableExp::VariableEx p (cons t char* name) {
_name = strdup(name);
}
Вычисление переменной возвращае т ее значение в текущем контексте:
bool VariableExp::Evaluat e (Context s aContext ) {
return aContext.Lookup(_name);
}
Копирование переменной возвращае т новый объект класса VariableExp:
BooleanExp * VariableExp::Cop y () const {
return new VariableExp(_name);
}
Чтобы заменить переменну ю выражением, мы сначала проверяем, что у пере-
менной то же имя, что было передано ранее в качестве аргумента:
BooleanExp * VariableExp::Replac e (
cons t char * name, BooleanExp & ex p
) {
if (strcmptname, _name ) == 0) {
retur n exp.Copy();
} els e {
retur n ne w VariableExp(_name);
}
}
Класс AndExp представляе т выражение, получающеес я в результате примене-
ния операции логическог о И к двум булевым выражениям:
clas s AndEx p : publi c BooleanEx p {
public:
AndExp(BooleanExp*, BooleanExp*);
virtua l -AndExp();•
virtua l boo l Evaluate(Contexts);
virtua l BooleanExp * Replace(cons t char*, BooleanExpS);
virtua l BooleanExp * Copy O const;
private:
BooleanExp * _operandl;
BooleanExp * _operand2;
};
Паттер н Interprete r
AndExp::AndEx p (BooleanExp * opl , BooleanExp * op2 ) {
_operand l = opl;
_operand 2 = op2;
}
При решении AndExp вычисляются его операнды и результат применения
к ним операции логического И возвращается:
bool AndExp::Evaluate (Context^ aContext) {
return
_operandl->Evaluate (aContext) &&
_operand2->Evaluat e (aContext ) ;
}
В классе AndExp операции Copy и Replace реализованы с помощью рекур-
сивных обращений к операндам:
BooleanExp * AndExp: : Copy () const {
retur n
new AndEx p ( _operandl->Copy(), _operand2->Cop y ( ) ) ;
}
BooleanExp * AndExp::Replac e (cons t char * name, BooleanExp k exp ) {
retur n
new AndExp (
_operandl->Replace(name, exp),
_operand2->Replace(name, exp)
);
}
Определим теперь булево выражение
(true and x) or (у and (not x ) )
и вычислим его для некоторых конкретных значений булевых переменных х и у:
BooleanExp* expression;
Context context;
VariableExp * x = new Vari abl eExp("X");
VariableExp * у = new Vari abl eExp("Y");
expression = new OrExpt
new AndExp(ne w Constant(true), x),
new AndExp(y, ne w NotExp(x) )
);
context.Assign(x, f al se);
context.Assign(y, true);
bool result = expression->Evaluate(context);
С такими значениями х и у выражение равно true. Чтобы вычислить его при
других значениях переменных, достаточно просто изменить контекст.
Паттерны поведения
И наконец, мы можем заменит ь переменну ю у новым выражение м и повто-
рить вычисление:
VariableExp * z = ne w VariableExpf"Z") ;
NotEx p not_z(z ) ;
BooleanExp * replacemen t = expression->Replace("Y", not_z);
context.Assign(z, true);
resul t = replacement->Evaluate(context);
На этом примере проиллюстрирован а важная особенност ь паттерна интер-
претатор: «интерпретация » предложени я может означать самые разные действия.
Из трех операций, определенных в классе BooleanExp, Evaluate наиболее близ-
ка к нашему интуитивном у представлени ю о том, что интерпретато р должен ин-
терпретироват ь программу или выражение и возвращат ь простой результат.
Но и операцию Replac e можно считать интерпретатором. Его контекстом яв-
ляется имя заменяемой переменной и подставляемо е вместо него выражение, а ре-
зультатом служит новое выражение. Даже операцию Сору допустимо рассматри-
вать как интерпретато р с пустым контекстом. Трактовка операций Replace и Сору
как интерпретаторо в может показатьс я странной, поскольку это всего лишь базо-
вые операции над деревом. Примеры в описании паттерна посетител ь демон-
стрируют, что все три операции разрешаетс я вынести в отдельный объект-посети -
тель «интерпретатор», тогда аналогия станет более очевидной.
Паттерн интерпретато р - это нечто большее, чем распределени е некоторой
операции по иерархии классов, составленно й с помощью паттерна компоновщик.
Мы рассматривае м операцию Evaluat e как интерпретатор, поскольку иерархию
классов BooleanEx p мыслим себе как представлени е некоторог о языка. Если бы
у нас была аналогична я иерархия для представлени я агрегатов автомобиля, то вряд
ли мы стали бы считать такие операции, как Weight (вес) и Сору (копирование),
интерпретаторами, несмотря на то что они распределены по всей иерархии клас-
сов, - просто мы не воспринимае м агрегаты автомобил я как язык. Тут все дело
в точке зрения: опубликуй мы грамматику агрегатов автомобиля, операции над ними
можно было трактоват ь как способы интерпретаци и соответствующег о языка.
Известные применения
Паттерн интерпретатор широко используется в компиляторах, реализован-
ных с помощью объектно-ориентированны х языков, наприме р в компилятора х
Smalltalk. В языке SPECTal k этот паттерн применяетс я для интерпретаци и фор-
матов входных файлов [Sza92]. В библиотеке QOCA для разрешения ограниче -
ний он применяетс я для вычислени я ограничени й [HHMV92].
Если рассматриват ь данный паттерн в самом общем виде (то есть как опера-
цию, распределенну ю по иерархии классов, основанно й на паттерне компонов -
щик), то почти любое применени е компоновщик а содержит и интерпретатор.
Но применят ь паттерн интерпретато р лучше в тех случаях, когда иерархию клас-
сов можно представлять себе как описание языка.
Родственные паттерны
Компоновщик: абстрактно е синтаксическо е дерево - это пример применения
паттерн а компоновщик.
Паттерн Iterator
Приспособлене ц показывае т вариант ы разделения терминальных символов
в абстрактно м синтаксическо м дереве.
Итератор: интерпретато р может пользоватьс я итераторо м для обхода струк-
туры-
Посетител я можно использоват ь для инкапсуляци и в одном классе поведе-
ния каждог о узла абстрактног о синтаксическог о дерева.
Паттер н Iterato r
Название и классификация паттерна
Итератор - паттерн поведения объектов.
Назначение
Предоставляе т способ последовательног о доступа ко всем элемента м состав-
ного объекта, не раскрыва я его внутреннег о представления.
Известен также под именем
Cursor (курсор).
Мотивация
Составной объект, скажем список, должен предоставлят ь способ доступа к сво-
им элементам, не раскрыва я их внутреннюю структуру. Более того, иногда требует-
ся обходить список по-разному, в зависимост и от решаемой задачи. Но вряд ли вы
захотите засорять интерфейс класса List операциями для различных вариантов
обхода, даже если все их можно предвидет ь заранее. Кроме того, иногда нужно, что-
бы в один и тот же момент было определено несколько активных обходов списка.
Все это позволяе т сделать паттерн итератор. Основная его идея в том, чтобы
за доступ к элементам и способ обхода отвечал не сам список, а отдельный объект-
итератор. В классе Iterator определен интерфейс для доступа к элемента м спис-
ка. Объект этого класса отслеживае т текущий элемент, то есть он располагае т ин-
формацией, какие элементы уже посещались.
Например, класс Li st мог бы предусмотрет ь класс Li stl terator.
Прежде чем создават ь экземпляр класса Li stl terator, необходимо иметь
список, подлежащи й обходу. С объектом Li stl terator вы можете последова -
тельно посетит ь все элементы списка. Операция Current It em возвращае т теку-
щий элемент списка, операция Fi rst инициализируе т текущий элемент первым
элементом списка, Next делает текущим следующий элемент, a IsDone проверя-
ет, не оказались ли мы за последним элементом, если да, то обход завершен.
Паттерны поведения
Отделение механизма обхода от объекта List позволяет определять итерато-
ры, реализующие различные стратегии обхода, не перечисляя их в интерфейсе
класса List. Например, FilteringListlterator мог бы предоставлять доступ
только к тем элементам, которые удовлетворяют условиям фильтрации.
Заметим: между итератором и списком имеется тесная связь, клиент должен
иметь информацию, что он обходит именно список, а не какую-то другую агреги-
рованную структуру. Поэтому клиент привязан к конкретному способу агрегиро-
вания. Было бы лучше, если бы мы могли изменять класс агрегата, не трогая код
клиента. Это можно сделать, обобщив концепцию итератора и рассмотрев поли-
морфную итерацию.
Например, предположим, что у нас есть еще класс SkipList, реализующий
список. Список с пропусками (skiplist) [Pug90] - это вероятностная структура
данных, по характеристикам напоминающая сбалансированное дерево. Нам нуж-
но научиться писать код, способный работать с объектами как класса List, так
и класса SkipList.
Определим класс AbstractList, в котором объявлен общий интерфейс для
манипулирования списками. Еще нам понадобится абстрактный класс Iterator,
определяющий общий интерфейс итерации. Затем мы смогли бы определить кон-
кретные подклассы класса Iterator для различных реализаций списка. В ре-
зультате механизм итерации оказывается не зависящим от конкретных агрегиро-
ванных классов.
Остается понять, как создается итератор. Поскольку мы хотим написать код, не
зависящий от конкретных подклассов List, то нельзя просто инстанцировать кон-
кретный класс. Вместо этого мы поручим самим объектам-спискам создавать для
себя подходящие итераторы, вот почему потребуется операция Createlterator,
посредством которой клиенты смогут запрашивать объект-итератор.
Createlterator - это пример использования паттерна фабричный метод.
В данном случае он служит для того, чтобы клиент мог запросить у объекта-спис-
ка подходящий итератор. Применение фабричного метода приводит к появле-
нию двух иерархий классов - одной для списков, другой для итераторов. Фабрич-
ный метод Createlterator «связывает» эти две иерархии.
Паттер н Iterato r
Применимость
Используйт е паттер н итератор:
а для доступ а к содержимом у агрегированны х объекто в без раскрыти я их
внутреннег о представления;
а для поддержк и нескольки х активны х обходо в одног о и тог о же агрегиро -
ванног о объекта;
а для предоставлени я единообразног о интерфейс а с цель ю обход а различны х
агрегированны х структу р (то есть для поддержк и полиморфно й итерации).
Структура
retur n new Concretelterator(this )
Участники
a Iterator - итератор:
- определяе т интерфей с для доступ а и обход а элементов;
a Concretelterato r - конкретны й итератор:
- реализуе т интерфей с класс а Iterator;
- следи т за текуще й позицие й при обход е агрегата;
a Aggregate - агрегат:
- определяе т интерфей с для создани я объекта-итератора;
a ConcreteAggregat e - конкретный агрегат:
- реализуе т интерфей с создани я итератор а и возвращае т экземпля р подхо -
дящего класса Concretelterator.
Отношения
Concretelterato r отслеживае т текущий объект в агрегат е и может вычис-
лит ь идущи й за ним. (
Результаты
У паттерн а итерато р есть следующи е важны е особенности:
а поддерживает различные виды обхода агрегата. Сложны е агрегат ы можн о об-
ходит ь по-разному. Например, для генераци и кода и семантически х проверо к
Паттерны поведения
нужно обходить деревья синтаксическог о разбора. Генератор кода может об-
ходить дерево во внутреннем или прямом порядке. Итераторы упрощают
изменение алгоритма обхода - достаточно просто заменить один экземпляр
итератора другим. Для поддержки новых видов обхода можно определить
и подклассы класса Iterator ;
а итераторы упрощают интерфейс класса Aggregate. Наличие интерфейса
для обхода в классе Iterator делает излишним дублирование этого ин-
терфейса в классе Aggregate. Тем самым интерфейс агрегата упрощается;
а одновременно для данного агрегата может быть активно несколько обходов.
Итератор следит за инкапсулированным в нем самом состоянием обхода. По-
этому одновременно разрешается осуществлять несколько обходов агрегата.
Реализация
Существует множество вариантов реализации итератора. Ниже перечисле-
ны наиболее употребительные. Решение о том, какой способ выбрать, часто за-
висит от управляющих структур, поддерживаемых языком программирования.
Некоторые языки (например, CLU [LG86]) даже поддерживают данный паттерн
напрямую.
а какой участник управляет итерацией. Важнейший вопрос состоит в том,
что управляет итерацией: сам итератор или клиент, который им пользуется.
Если итерацией управляет клиент, то итератор называется внешним, в про-
тивном случае - внутренним.1 Клиенты, применяющие внешний итератор,
должны явно запрашивать у итератора следующий элемент, чтобы двигать-
ся дальше по агрегату. Напротив, в случае внутреннего итератора клиент
передает итератору некоторую операцию, а итератор уже сам применяет эту
операцию к каждому посещенному во время обхода элементу агрегата.
Внешние итераторы обладают большей гибкостью, чем внутренние. Напри-
мер, сравнить две коллекции на равенство с помощью внешнего итератора
очень легко, а с помощью внутреннего - практически невозможно. Слабые
стороны внутренних итераторов наиболее отчетливо проявляются в таких
языках, как C++, где нет анонимных функций, замыканий (closure) и про-
должений (continuation), как в Smalltal k или CLOS. Но, с другой стороны,
внутренние итераторы проще в использовании, поскольку они вместо вас
определяют логику обхода;
а что определяет алгоритм обхода. Алгоритм обхода можно определить не
только в итераторе. Его может определить сам агрегат и использовать ите-
ратор только для хранения состояния итерации. Такого рода итератор мы
называем курсором, поскольку он всего лишь указывает на текущую пози-
цию в агрегате. Клиент вызывает операцию Next агрегата, передавая ей кур-
сор в качестве аргумента. Операция же Next изменяет состояние курсора.2
1 Грейди Буч (Grady Booch) называет внешние и внутренние итераторы соответственно активными
и пассивными [Воо94]. Термины «активный» и «пассивный» относятся к роли клиента, а не к дей-
ствиям, выполняемым итератором.
2 Курсоры — это простой пример применения паттерна хранитель; их реализации имеют много общих черт.
Паттерн Iterator
Если за алгоритм обхода отвечает итератор, то для одного и того же агрега-
та можно использовать разные алгоритмы итерации, и, кроме того, проще
применить один алгоритм к разным агрегатам. С другой стороны, алгорит-
му обхода может понадобиться доступ к закрытым переменным агрегата.
Если это так, то перенос алгоритма в итератор нарушает инкапсуляцию аг-
регата;
а насколько итератор устойчив. Модификация агрегата в то время, как со-
вершается его обход, может оказаться опасной. Если при этом добавляются
или удаляются элементы, то не исключено, что некоторый элемент будет по-
сещен дважды или вообще ни разу. Простое решение - скопировать агрегат
и обходить копию, но обычно это слишком дорого.
Устойчивый итератор (robust) гарантирует, что ни вставки, на удаления не
помешают обходу, причем достигается это без копирования агрегата. Есть
много способов реализации устойчивых итераторов. В большинстве из них
итератор регистрируется в агрегате. При вставке или удалении агрегат либо
подправляет внутреннее состояние всех созданных им итераторов, либо
организует внутреннюю информацию так, чтобы обход выполнялся пра-
вильно.
В работе Томаса Кофлера (Thomas Kofler) [Kof93] приводится подробное
обсуждение реализации итераторов в каркасе ЕТ++. Роберт Мюррей (Robert
Murray) [МигЭЗ] описывает реализацию устойчивых итераторов для класса
Li st из библиотеки USL Standard Components;
о дополнительные операции итератора. Минимальный интерфейс класса
Iterator состоит из операций First, Next, IsDone и Currentltem.1 Но
могут оказаться полезными и некоторые дополнительные операции. Напри-
мер, упорядоченные агрегаты могут предоставлят ь операцию Previous, по-
зиционирующу ю итератор на предыдущий элемент. Для отсортированных
или индексированных коллекций интерес представляет операция SkipTo,
которая позиционируе т итератор на объект, удовлетворяющий некоторому
критерию;
а использование полиморфных итераторов в C++. С полиморфными итерато-
рами связаны определенные накладные расходы. Необходимо, чтобы объект-
итератор создавался в динамической памяти фабричным методом. Поэтому
использовать их стоит только тогда, когда есть необходимост ь в полимор-
физме. В противном случае применяйте конкретные итераторы, которые
вполне можно распределять в стеке.
У полиморфных итераторов есть и еще один недостаток: за их удаление от-
вечает клиент. Здесь открывается большой простор для ошибок, так как
очень легко забыть об освобождении распределенног о из кучи объекта-ите-
ратора после завершения работы с ним. Особенно велика вероятность это-
го, если у операции есть несколько точек выхода. А в случае возбуждения
Этот интерфейс можно и еще уменьшить, если объединить операции Next, IsDone и Currentlte m
в одну, которая будет переходить к следующему объекту и возвращать его. Если обход завершен, то
эта операция вернет специальное значение (например, 0), обозначающее конец итерации.
Паттерны поведения
исключения память, занимаемая объектом-итератором, вообще никогда не
будет освобождена.
Эту ситуацию помогает исправить паттерн заместитель. Вместо настояще-
го итератора мы используем его заместителя, память для которого выделе-
на в стеке. Заместитель уничтожает итератор в своем деструкторе. Поэто-
му, как только заместитель выходит из области действия, вместе с ним
уничтожается и настоящий итератор. Заместитель гарантирует выполне-
ние надлежащей очистки даже при возникновении исключений. Это при-
мер применения хорошо известной в C++ техники, которая называется «вы-
деление ресурса - это инициализация» [ES90]. В разделе «Пример кода»
она проиллюстрирована подробнее;
а итераторы могут иметь привилегированный доступ. Итератор можно
рассматривать как расширение создавший его агрегат. Итератор и агрегат
тесно связаны. В C++ такое отношение можно выразить, сделав итератор дру-
гом своего агрегата. Тогда не нужно определять в агрегате операции, единствен-
ная цель которых - позволить итераторам эффективно выполнить обход.
Однако наличие такого привилегированного доступа может затруднить опре-
деление новых способов обхода, так как потребуется изменить интерфейс аг-
регата, добавив в него нового друга. Для того чтобы решить эту проблему,
класс Iterator может включать защищенные операции для доступа к важ-
ным, но не являющимся открытыми членам агрегата. Подклассы класса
Iterator (и только его подклассы) могут воспользоваться этими защищен-
ными операциями для получения привилегированного доступа к агрегату;
а итераторы для составных объектов. Реализовать внешние агрегаты для ре-
курсивно агрегированных структур (таких, например, которые возникают
в результате применения паттерна компоновщик) может оказаться затруд-
нительно, поскольку описание положения в структуре иногда охватывает
несколько уровней вложенности. Поэтому, чтобы отследить позицию теку-
щего объекта, внешний итератор должен хранить путь через составной объ-
ект Composite. Иногда проще воспользоваться внутренним итератором.
Он может запомнить текущую позицию, рекурсивно вызывая себя самого,
так что путь будет неявно храниться в стеке вызовов.
Если узлы составного объекта Composite имеют интерфейс для перемеще-
ния от узла к его братьям, родителям и потомкам, то лучшее решение дает
итератор курсорного типа. Курсору нужно следить только за текущим уз-
лом, а для обхода составного объекта он может положиться на интерфейс
этого узла.
Составные объекты часто нужно обходить несколькими способами. Самые
распространенные - это обход в прямом, обратном и внутреннем порядке,
а также обход в ширину. Каждый вид обхода можно поддержать отдельным
итератором;
а пустые итераторы. Пустой итератор Nul l l terator - это вырожденный
итератор, полезный при обработке граничных условий. По определению,
Nul l l t erat or всегда считает, что обход завершен, то есть его операция
IsDone неизменно возвращает истину.
Паттерн Iterator
Применение пустого итератора может упростить обход древовидных струк-
тур (например, объектов Composite). В каждой точке обхода мы запраши-
ваем у текущего элемента итератор для его потомков. Элементы-агрегаты,
как обычно, возвращают конкретный итератор. Но листовые элементы воз-
вращают экземпляр Nulllterator. Это позволяет реализовать обход всей
структуры единообразно.
Пример кода
Рассмотрим простой класс списка List, входящего в нашу базовую библиоте-
ку (см. приложение С) и две реализации класса Iterator: одну для обхода списка
от начала к концу, а другую - от конца к началу (в базовой библиотеке поддержан
только первый способ). Затем мы покажем, как пользоваться этими итераторами
и как избежать зависимости от конкретной реализации. После этого изменим ди-
зайн, дабы гарантировать корректное удаление итераторов. А в последнем примере
мы проиллюстрируем внутренний итератор и сравним его с внешним.
а интерфейсы классов List и Iterator. Сначала обсудим ту часть интерфейса
класса List, которая имеет отношение к реализации итераторов. Полный
интерфейс см. в приложении С:
templat e <clas s Item >
clas s Lis t {
public:
List(long size = DEFAULT_LIST_CAPACITY);
lon g Count( ) const;
Item & Get(lon g index ) const;
// ...
};
В открытом интерфейсе класса List предусмотрен эффективный способ
поддержки итераций. Его достаточно для реализации обоих видов обхода.
Поэтому нет необходимости предоставлять итераторам привилегированный
доступ к внутренней структуре данных. Иными словами, классы итераторов не
являются друзьями класса List. Определим абстрактный класс Iterator,
в котором будет объявлен интерфейс итератора:
templat e <clas s Item >
clas s Iterato r {
public:
virtua l voi d First( ) = 0;
virtua l voi d Next( ) = 0;
virtua l boo l IsDone O cons t = 0;
virtua l Ite m Currentltemf ) cons t = 0;
protected:
Iterator));
};
а реализации подклассов класса Iterator. Класс List Iterator является под-
классом Iterator:
Паттерны поведения
template <class Iten»
class Listlterator : public Iterator<Item> {
public:
Listlterator(const List<Item>* aList);
virtual void First();
virtual void Next();
virtual bool IsDoneO const;
virtual Item CurrentltemO const;
private:
const List<Item>* _list;
long _current;
};
Реализация класса Listlterator не вызывает затруднений. В нем хранит-
ся экземпляр List и индекс _current, указывающи й текущую позицию
в списке:
template <class Item>
Listlterator<ltem>::Listlterator (
const List<Item>* aList
) : _list(aList), _current(0) {
}
Операция First позиционируе т итератор на первый элемент списка:
template <class Item>
void Listlterator<ltem>::First () {
_current = 0;
}
Операция Next делает текущим следующий элемент:
template <class Item>
void Listlterator<ltem>::Next () {
_current++;
}
Операция IsDone проверяет, относится ли индекс к элементу внутри списка:
template <class Item>
bool Listlterator<ltem>::IsDone () const {
return _current >= _l i st ->Count ();
}
Наконец, операция Current Item возвращает элемент, соответствующий
текущему индексу. Если итерация уже завершилась, то мы возбуждае м ис-
ключение IteratorOutOfBounds:
templat e <clas s Item >
Item Listlterator<ltem>::CurrentItem () const {
if (IsDoneO) {
thro w IteratorOutOfBounds;
}
Паттер н Iterato r
return _l i st->Get(_current);
}
Реализация обратног о итератора ReverseListlterator аналогична рас-
смотренной, только его операция First позиционируе т _current на ко-
нец списка, а операция Next делает текущим предыдущий элемент;
а использование итераторов. Предположим, что имеется список объектов
Employee (служащий) и мы хотели бы напечатать информацию обо всех содер-
жащихся в нем служащих. Класс Employe e поддерживае т печать с помощью
операции Print. Для печати списка определим операцию PrintEmployees,
принимающу ю в качестве аргумент а итератор. Она пользуетс я этим итера-
тором для обхода и печати содержимог о списка:
void PrintEmployees (Iterator<Employee*>& i) {
for (i.Fi rst (); !i.IsDone() ; i.Next O) {
i.Current!t em()->Pri nt ();
}'
}
Поскольку у нас есть итераторы для обхода списка от начала к концу и от
конца к началу, то мы можем повторно воспользоватьс я той же самой опе-
рацией для печати списка служащих в обоих направлениях:
List<Employee*>* employees;
// ...
ListIter'ator<Employee*> forward (employees) ;
ReverseListIterator<Employee*> backward (employees) ;
PrintEmployees (forward) ;
PrintEmployees (backward) ,-
а как избежать зависимости от конкретной реализации списка. Рассмотрим,
как повлияла бы на код итератора реализация класса List в виде списка
с пропусками. Подкласс SkipList класса List должен предоставит ь ите-
ратор SkipList Iterator, реализующий интерфейс класса Iterator. Для
эффективно й реализации итерации у SkipListlterator должен быть не
только индекс. Но поскольку SkipListlterator согласуется с интерфей-
сом класса Iterator, то операцию PrintEmployees можно использоват ь
и тогда, когда служащие хранятся в списке типа SkipList:
SkipList<Employee*> * employees;
// ...
SkipListIterator<Employee* > iterator(employees);
PrintEmployees(iterator);
Оптимально е решение в данной ситуации - вообще не привязыватьс я к кон-
кретной реализации списка, например SkipList. Мы можем рассмотрет ь
абстрактный класс AbstractList ради стандартизаци и интерфейс а спис -
ка для различных реализаций. Тогда и List, и SkipList окажутся подклас-
сами AbstractList.
Паттерн ы поведени я
Для поддержк и полиморфно й итераци и клас с AbstractLis t определяе т
фабричны й мето д Createlterator, замещаемы й в подклассах, которы е
возвращаю т подходящи й для себя итератор:
templat e <clas s Item >
clas s AbstractLis t {
public:
virtua l Iterator<Item> * Createlterator( ) cons t = 0;
// ...
};
Альтернативны й вариан т - определени е общег о подмешиваемог о класс а
Traversable, в котором определен интерфейс для создания итератора. Для
поддержк и полиморфны х итераци й агрегированны е класс ы могу т являтьс я
потомками Traversable.
Клас с List замещае т Createlterator, для того чтоб ы возвратит ь объек т
Listlterator:
templat e <clas s Item >
Iterator<Item> * List<Item>::CreateIterato r ( ) cons t {
retur n ne w Listlterator<ltem>(this);
}
Тепер ь мы може м написат ь код для печат и служащих, которы й не буде т за-
висет ь от конкретног о представлени я списка:
//м ы знае м только, чт о существуе т клас с AbstractLis t
AbstractList<Employee*> * employees;
II ...
Iterator<Employee*> * iterato r = employees->Create!terator();
PrintEmployees(*iterator);
delet e iterator;
а как гарантировать удаление итераторов? Заметим, что Createlterator
возвращае т тольк о что созданны й в динамическо й памят и объект-итератор.
Ответственност ь за его удалени е лежи т на нас. Если мы забуде м это сде-
лать, то возникне т утечк а памяти. Для того чтоб ы упростит ь задач у клиен -
там, мы введем класс IteratorPtr, которы й замещае т итератор. Он унич -
тожит объек т Iterator при выход е из област и определения.
Объек т класс а IteratorPtr всегд а распределяетс я в стеке.1 C++ автома -
тическ и вызове т его деструктор, которы й уничтожи т реальны й итератор.
В классе IteratorPtr операторы operator-> и operator* перегруже-
ны так, что объек т этог о класс а можн о рассматриват ь как указател ь на ите-
ратор. Функции-член ы класс а IteratorPtr встраиваются, поэтом у при их
вызов е накладны х расходо в нет:
template <class Item>
class IteratorPtr {
1 Это можно проверять на этапе компиляции, если объявить операторы new и delete закрытыми. Ре-
ализовывать их при этом не надо.
Паттер н Iterato r
public:
IteratorPtr(Iterator<Item> * i): _i(i ) { }
~IteratorPtr( ) { delet e _i; }
Iterator<Item> * operator->( ) { retur n _i; }
Iterator<Item> & operator*!) { retur n *_i; }
private:
// запретит ь копировани е и присваивание, чтоб ы
// избежат ь многократны х удалени й _i
IteratorPtr(cons t IteratorPtrk);
IteratorPtr k operator=(cons t IteratorPtrk);
private:
Iterator<Item> * _i;
};
IteratorPtr позволяе т упростит ь код печати:
AbstractList<Employee*> * employees;
// ...
IteratorPtr<Employee*> iterator (employees->Create!terator () ) ;
PrintEmployees (*iterator) ;
а внутренний Listlterator. В последнем примере рассмотрим, как можно было
бы реализовать внутренний или пассивный класс Listlterator. Теперь
итератор сам управляет итерацией и применяет к каждому элементу неко-
торую операцию.
Нерешенным остается вопрос о том, как параметризовать итератор той опе-
рацией, которую мы хотим применить к каждому элементу. C++ не поддер-
живает ни анонимных функций, ни замыканий, которые предусмотрены для
этой цели в других языках. Существует, по крайней мере, два варианта: пере-
дать указатель на функцию (глобальную или статическую) или породить под-
классы. В первом случае итератор вызывает переданную ему операцию в каж-
дой точке обхода. Во втором случае итератор вызывает операцию, которая
замещена в подклассе и обеспечивает нужное поведение.
Ни один из вариантов не идеален. Часто во время обхода нужно аккумули-
ровать некоторую информацию, а функции для этого плохо подходят — при-
шлось бы использовать статические переменные для запоминания состоя-
ния. Подкласс класса Iterator предоставляет удобное место для хранения
аккумулированного состояния - переменную экземпляра. Но создавать
подкласс для каждого вида обхода слишком трудоемко.
Вот набросок реализации второго варианта с использованием подклассов.
Назовем внутренний итератор ListTraverser:
templat e <clas s Item >
clas s ListTraverse r {
public:
ListTraverse r (List<Item> * aList ) ;
bool Travers e () ;
Паттерн ы поведени я
protected:
virtua l boo l Processltem(cons t Item& ) = 0;
private:
Listlterator<ltem > „iterator;
};
ListTraverser принимает экземпляр List в качестве параметра. Внутри
себя он использует внешний итератор List Iterator для выполнения обхо-
да. Операция Traverse начинает обход и вызывает для каждого элемента
операцию Processltem. Внутренний итератор может закончит ь обход, вер-
нув f al se из Processltem. Traverse сообщает о преждевременно м завер-
шении обхода:
templat e <clas s Item >
ListTraverser<Item>::ListTraverse r (
List<Item> * aLis t
) : _iterator(aList ) { }
templat e <clas s Item >
boo l ListTraverser<Item>::Travers e ( ) {
boo l resul t = false;
for (
_iterator.First();
!_iterator.IsDone();
_iterator.Next( )
) {
resul t = ProcessItem(_iterator.CurrentItem());
if (resul t == false ) {
break;
}
}
retur n result;
}
Воспользуемс я итератором ListTraverser для печати первых десяти слу-
жащих из списка. С этой целью надо породить подкласс от ListTraverser
и определит ь в нем операцию Proces s Item. Подсчитыват ь число напеча-
танных служащих будем в переменной экземпляр а _count:
class PrintNEmployees : public ListTraverser<Employee*> {
public:
PrintNEmployees(List<Employee*>* aList, int n) :
ListTraverser<Employee*>(aList),
_t ot al ( n), _count(0) { }
protected:
bool ProcessltemtEmployee * const&);
private:
int _total;
int _count;
};
Паттер н Iterato r
boo l PrintNEmployees::ProcessIte m (Employee * const & e ) {
_count++;
e->Print();
retur n _coun t < _total;
}
Вот как PrintNEmployees печатает первые 10 служащих:
List<Employee*>* employees;
// ...
PrintNEmployees pa(employees, 10);
pa.Traverse();
Обратите внимание, что в коде клиента нет цикла итерации. Всю логику обхо-
да можно использоват ь повторно. В этом и состоит основное преимуществ о
внутреннег о итератора. Работы, правда, немног о больше, чем для внешнег о
итератора, так как нужно определят ь новый класс. Сравнит е с программой,
где применяетс я внешний итератор:
ListIterator<Employee* > i(employees);
int coun t = 0;
for (i. First (); !i.IsDone( ) ; i.NextO ) {
count++;
i.Currentltemf)->Print();
if (coun t >= 10 ) {
break;
}
}
Внутренние итераторы могут инкапсулировать разные виды итераций.
Например, FilteringListTraverser инкапсулируе т итерацию, при ко-
торой обрабатываютс я лишь элементы, удовлетворяющи е определенном у
условию:
templat e <clas s Item >
clas s FilteringListTraverse r {
public:
FilteringListTraverse r (List<Item> * aList ) ;
boo l Travers e ( ) ;
protected:
virtua l boo l Processltemfcons t Item& ) = 0;
virtua l boo l Testltem t cons t Item& ) = 0;
private:
Listlterator<ltem > _iterator;
};
Интерфейс такой же, как у ListTraverser, если не считат ь новой функ-
ции-члена Tes t Item, котора я и реализуе т проверк у условия. В подкласса х
Test Item замещается для задания конкретного условия.
Посредство м операции Travers e выясняется, нужно ли продолжат ь обход,
основываяс ь на результат е проверки:
Паттерны поведени я
templat e <clas s Item >
void FilteringListTraverser<Item>::Travers e () {
bool resul t = false;
for (
_iterator.First();
!_iterator.IsDone();
_iterator-Next( )
) {
if (TestItem(_iterator.CurrentItem()) ) {
result = ProcessItem(_iterator.CurrentItem());
if (resul t == false ) {
break;
}
}
}
return result;
}
В качестве варианта можно было определить функцию Traverse так, чтобы
она сообщала хотя бы об одном встретившемся элементе, который удовлет-
воряет условию.1
Известные применения
Итераторы широко распространены в объектно-ориентированных системах.
В том или ином виде они есть в большинстве библиотек коллекций классов.
Вот пример из библиотеки компонентов Грейди Буча [Воо94], популярной
библиотеки, поддерживающей классы коллекций. В ней имеется реализация
очереди фиксированной (ограниченной) и динамически растущей длины (неограни-
ченной). Интерфейс очереди определен в абстрактном классе Queue. Для поддержки
полиморфной итерации по очередям с разной реализацией итератор написан с ис-
пользованием интерфейса абстрактного класса Queue. Преимущество такого под-
хода очевидно - отсутствует необходимость в фабричном методе, который за-
прашивал бы у очереди соответствующий ей итератор. Однако, чтобы итератор
можно было реализовать эффективно, интерфейс абстрактного класса Queue дол-
жен быть мощным.
В языке Smalltalk необязательно определять итераторы так явно. В стандарт-
ных классах коллекций (Bag, Set, Dictionary, OrderedCollection, String
и т.д.) определен метод do:, выполняющий функции внутреннего итератора, ко-
торый принимает блок (то есть замыкание). Каждый элемент коллекции привязы-
вается к локальной переменной в блоке, а затем блок выполняется. Smalltalk так-
же включает набор классов Stream, которые поддерживают похожий на итератор
интерфейс. ReadStream - это, по существу, класс Iterator и внешний итератор
для всех последовательных коллекций. Для непоследовательных коллекций типа
Set и Dictionary нет стандартных итераторов.
Операция Traverse в этих примерах - это не что иное, как шаблонный метод с примитивными
операциями Testltem и Processltem..
Паттерн Mediator
Полиморфны е итератор ы и выполняющи е очистк у заместител и находятс я
в контейнерны х класса х ЕТ++ [WGM88]. Курсороподобны е итератор ы использу -
ются в класса х каркас а графически х редакторо в Unidra w [VL90].
В системе ObjectWindows 2.0 [Вог94] имеется иерархия классов итераторов
для контейнеров. Контейнер ы разны х типо в можн о обходит ь одним и тем же спо-
собом. Синтакси с итераторо в в ObjectWindow s основа н на перегрузк е постфикс -
ного оператор а инкремент а ++ для переход а к следующем у элементу.
Родственные паттерны
Компоновщик: итератор ы довольн о част о применяютс я для обход а рекур -
сивных структур, создаваемы х компоновщиком.
Фабричны й метод: полиморфны е итератор ы поручаю т фабричны м мето -
дам инстанцироват ь подходящи е подкласс ы класс а Iterator.
Итерато р може т использоват ь хранител ь для сохранени я состояни я итера -
ции и при этом содержи т его внутр и себя.
Паттер н Mediato r
Название и классификация паттерна
Посредни к - паттер н поведени я объектов.
Назначение
Определяе т объект, инкапсулирующи й спосо б взаимодействи я множеств а
объектов. Посредни к обеспечивае т слабу ю связанност ь системы, избавля я объек -
ты от необходимост и явно ссылатьс я друг на друг а и позволя я тем самым незави -
симо изменят ь взаимодействи я межд у ними.
Мотивация
Объектно-ориентированно е проектировани е способствуе т распределени ю не-
которог о поведени я межд у объектами. Но при этом в получившейс я структур е
объекто в може т возникнут ь мног о связе й или (в худше м случае ) каждом у объек -
ту придетс я имет ь информаци ю обо всех остальных.
Несмотр я на то что разбиени е систем ы на множеств о объекто в в обще м слу-
чае повышае т степен ь повторног о использования, однак о изобили е взаимосвязе й
приводи т к обратном у эффекту. Если взаимосвязе й слишко м много, тогд а систе -
ма подобн а монолит у и маловероятно, что объек т сможе т работат ь без поддержк и
други х объектов. Боле е того, существенн о изменит ь поведени е систем ы практи -
чески невозможно, поскольк у оно распределен о межд у многим и объектами. Если
вы предпримет е подобну ю попытку, то для настройк и поведени я систем ы вам
придетс я определят ь множеств о подклассов.
Рассмотри м реализаци ю диалоговы х окон в графическо м интерфейс е пользо -
вателя. Здес ь располагаетс я ряд виджетов: кнопки, меню, поля ввод а и т.д., как
показан о на рисунке.
Част о межд у разным и виджетам и в диалогово м окне существую т зависимос -
ти. Например, если одно из поле й ввода пустое, то определенна я кнопк а недоступ -
на. При выбор е из списк а може т изменитьс я содержимо е поля ввода. И наоборот,
Паттерн ы поведени я
ввод текст а в некоторо е поле може т автоматическ и привест и к выбор у одног о или
нескольки х элементо в списка. Если в поле ввод а присутствуе т какой-т о текст, то
могу т быт ь активизирован ы кнопки, позволяющи е произвест и определенно е дей-
ствие над этим текстом, наприме р изменит ь либо удалит ь его.
В разны х диалоговы х окна х зависимост и межд у виджетам и могу т быт ь различ -
ными. Поэтому, несмотр я на то что во всех окна х встречаютс я однотипны е видже -
ты, просто взять и повторно использовать готовые классы виджетов не удастся,
придетс я производит ь настройк у с цель ю учет а зависимостей. Индивидуальна я
настройк а каждог о виджет а - утомительно е занятие, ибо участвующи х классо в
слишко м много.
Всех этих пробле м можн о избежать, если инкапсулироват ь коллективно е по-
ведени е в отдельно м объекте-посреднике. Посредни к отвечае т за координаци ю
взаимодействи й межд у группо й объектов. Он избавляе т входящи е в групп у объек -
ты от необходимост и явно ссылатьс я дру г на друга. Все объект ы располагаю т ин-
формацие й тольк о о посреднике, поэтом у количеств о взаимосвязе й сокращается.
Так, класс FontDialogDirector может служить посредником между вид-
жетами в диалогово м окне. Объек т этог о класс а «знает » обо всех виджета х в окне
Паттерн Mediator
и координируе т взаимодействи е между ними, то есть выполняе т функци и центра
коммуникаций.
На следующе й диаграмм е взаимодействи й показано, как объект ы коопериру -
ются друг с другом, реагиру я на изменени е выбранног о элемент а списка.
Последовательност ь событий, в результат е которых информаци я о выбран -
ном элемент а списка передаетс я в поле ввода, следующая:
1. Списо к информируе т распорядител я о происшедши х в нем изменениях.
2. Распорядител ь получае т от списка выбранны й элемент.
3. Распорядител ь передае т выбранны й элемент полю ввода.
4. Теперь, когда поле ввода содержи т какую-т о информацию, распорядител ь ак-
тивизируе т кнопки, позволяющи е выполнит ь определенно е действие (напри-
мер, изменит ь шрифт на полужирны й или курсив).
Обратит е внимани е на то, как распорядител ь осуществляе т посредничеств о
между списком и полем ввода. Виджет ы общаютс я друг с другом не напрямую,
а через распорядитель. Им вообще не нужно владет ь информацие й друг о друге,
они осведомлен ы лишь о существовани и распорядителя. А коль скоро поведени е
локализован о в одном классе, то его несложн о модифицироват ь или сделат ь со-
вершенн о другим путем расширени я или замены этого класса.
Абстракцию FontDialogDirector можно было бы интегрировать в библио-
теку классов так, как показан о на рисунке.
Паттерн ы поведени я
DialogDirecto r - это абстрактны й класс, которы й определяе т поведени е
диалоговог о окна в целом. Клиент ы вызываю т его операци ю ShowDialo g для отоб-
ражени я окна на экране. CreateWidget s - это абстрактная операци я для созда -
ния виджето в в диалогово м окне. WidgetChange d - еще одна абстрактна я опе-
рация; с ее помощь ю виджет ы сообщаю т распорядител ю об изменениях. Подкласс ы
DialogDirector замещают операции CreateWidgets (для создания нужных
виджетов ) и WidgetChange d (для обработк и извещени й об изменениях).
Применимость
Используйт е паттер н посредник, когд а
о имеютс я объекты, связ и межд у которым и сложн ы и четко определены. По-
лучающиес я при этом взаимозависимост и не структурирован ы и трудн ы
для понимания;
а нельз я повторн о использоват ь объект, поскольк у он обмениваетс я информа -
цией со многим и другим и объектами;
а поведение, распределенно е межд у нескольким и классами, должн о подда -
ватьс я настройк е без порождени я множеств а подклассов.
Структура
Типична я структур а объектов.
Участники
Mediator (DialogDirector) - посредник;
- определяе т интерфей с для обмен а информацие й с объектам и Col league;
Паттерн Mediato r
a ConcreteMediator (FontDialogDirector) - конкретный посредник:
— реализуе т кооперативно е поведение, координиру я действи я объекто в
Colleague;
- владее т информацие й о коллега х и подсчитывае т их;
а Классы Colleague (ListBox, EntryField) - коллеги:
— каждый класс Colleague «знает» о своем объекте Mediator;
- все коллег и обмениваютс я информацие й тольк о с посредником, так как
при его отсутстви и им пришлос ь бы общатьс я межд у собой напрямую.
Отношения
Коллег и посылаю т запрос ы посредник у и получаю т запрос ы от него. Посред -
ник реализуе т кооперативно е поведени е путе м переадресаци и каждог о запрос а
подходящем у коллег е (или нескольки м коллегам).
Результаты
У паттерн а посредни к есть следующи е достоинств а и недостатки:
а снижает число порождаемых подклассов. Посредни к локализуе т поведение,
которо е в противно м случа е пришлос ь бы распределят ь межд у нескольки -
ми объектами. Для изменени я поведени я нужно породит ь подкласс ы толь-
ко от класс а посредник а Mediator, класс ы колле г Colleagu e можн о ис-
пользоват ь повторн о без каких бы то ни было изменений;
а устраняет связанность между коллегами. Посредни к обеспечивае т слабу ю
связанност ь коллег. Изменят ь класс ы Colleagu e и Mediato r можн о неза-
висимо друг от друга;
а упрощает протоколы взаимодействия объектов. Посредни к заменяе т дис-
циплин у взаимодействи я «все со всеми » дисциплино й «один со всеми», то
есть один посредни к взаимодействуе т со всеми коллегами. Отношени я вида
«один ко многим » проще для понимания, сопровождени я и расширения;
а абстрагирует способ кооперирования объектов. Выделени е механизм а по-
средничеств а в отдельну ю концепци ю и инкапсуляци я ее в одном объект е
позволяет сосредоточиться именно на взаимодействии объектов, а не на их
индивидуально м поведении. Это дает возможност ь прояснит ь имеющиес я
в системе взаимодействия;
а централизует управление. Паттер н посредни к переноси т сложност ь взаи-
модействи я в класс-посредник. Поскольк у посредни к инкапсулируе т про-
токолы, то он може т быть сложне е отдельны х коллег. В результат е сам по-
средни к становитс я монолитом, которы й трудн о сопровождать.
Реализация
Имейт е в виду, что при реализаци и паттерн а посредни к може т происходить:
а избавление от абстрактного класса Mediator. Если коллег и работаю т тольк о
с одним посредником, то нет необходимост и определят ь абстрактны й класс
Mediator. Обеспечиваемая классом Mediator абстракция позволяет кол-
легам работат ь с разным и подклассам и класс а Mediato r и наоборот;
Паттерны поведения
а обмен информацие й между коллегами и посредником. Коллеги должны обме-
ниваться информацие й со своим посреднико м только тогда, когда возника-
ет представляюще е интерес событие. Одним из подходов к реализации по-
средника является применение паттерна наблюдатель. Тогда классы коллег
действуют как субъекты, посылающие извещения посредник у о любом из-
менении своего состояния. Посредник реагирует на них, сообщая об этом
другим коллегам.
Другой подход: в классе Mediator определяетс я специализированны й ин-
терфейс уведомления, который позволяет коллегам обмениватьс я инфор-
мацией более свободно. В Smalltalk/V для Windows применяетс я некоторая
форма делегирования: общаясь с посредником, коллега передает себя в ка-
честве аргумента, давая посреднику возможност ь идентифицироват ь отпра-
вителя. Об этом подходе рассказываетс я в разделе «Пример кода», а о реа-
лизации в Smalltalk/V - в разделе «Известные применения».
Пример кода
Для создания диалоговог о окна, обсуждавшегос я в разделе «Мотивация»,
воспользуемся классом DialogDirector. Абстрактный класс DialogDirector
определяет интерфейс распорядителей:
};
Changed вызывает операцию распорядител я WidgetChanged. С ее помощью
виджеты информируют своего распорядител я о происшедши х с ними изменениях:
class DialogDirector {
public:
virtual -DialogDirector();
virtual void ShowDialogf);
virtua l voi d WidgetChanged(Widget* ) = 0;
protected:
DialogDirector();
virtua l voi d CreateWidgets( ) = 0;
};
Widge t - это абстрактный базовый класс для всех виджетов. Он располагае т
информацие й о своем распорядителе:
class Widget {
public:
Widget(DialogDirector*);
virtual void Changed();
virtual void HandleMouse(MouseEventk event);
// ...
private:
DialogDirector* _director;
Паттер н Mediato r
voi d Widget::Change d () {
_director->WidgetChanged(this),
В подкласса х DialogDirecto r переопределен а операция WidgetChange d
для воздействия на нужные виджеты. Виджет передает ссылку на самого себя в ка-
честве аргумент а WidgetChanged, чтобы распорядител ь имел информацию об
изменившемся виджете. Подклассы DialogDirector переопределяют исключи-
тельно виртуальну ю функцию CreateWidget s для размещения в диалоговом
окне нужных виджетов.
ListBox, Entry-Field и Button - это подклассы Widget для специализиро-
ванных элементов интерфейса. В классе ListBox есть операция GetSelec t ion для
получения текущего множества выделенных элементов, а в классе Entry-Fiel d - опе-
рация SetText для помещения текста в поле ввода:
clas s ListBo x : publi c Widge t {
public:
ListBox(DialogDirector*);
virtua l cons t char * GetSelectionf);
virtua l voi d SetList(List<char*> * listltems);
virtua l voi d HandleMouse(MouseEvent & event);
// ...
};
clas s EntryFiel d : publi c Widge t {
public:
EntryField(DialogDirector*);
virtua l voi d SetText(cons t char * text);
virtua l cons t char * GetText();
virtua l voi d HandleMouse(MouseEvent & event),
// ...
};
Операци я Change d вызываетс я пр и нажати и кнопк и Butto n (просто й вид -
жет). Это происходи т в операци и обработк и событи й мыш и HandleMouse:
clas s Butto n : publi c Widge t {
public:
Button(DialogDirector*);
virtua l voi d SetText(cons t char * text);
virtua l voi d HandleMouse(MouseEvent & event);
// ...
};
voi d Button::HandleMous e (MouseEvent & event ) {
// ...
Changed();
}
Паттерн ы поведени я
Класс FontDialogDirector является посредником между всеми виджетами
в диалоговом окне. FontDialogDirector - это подкласс класса DialogDirector:
class FontDialogDirector : public DialogDirector {
public:
FontDialogDirector();
virtual -FontDialogDirector();
virtual void WidgetChanged(Widget*);
protected:
virtua l void CreateWidgets();
private:
Button * _ok;
Button * _cancel;
ListBox * _fontList;
EntryField * _fontNarae;
};
FontDialogDirector отслеживает все виджеты, которые ранее поместил в ди-
алогово е окно. Переопределенна я в нем операци я CreateWidget s создае т вид-
жеты и инициализирует ссылки на них:
void FontDialogDirector::CreateWidget s (} {
_ok = ne w Button(this);
_cance l = new Button(this);
_fontLis t = new ListBox(this);
_fontNam e = new EntryField(this);
// поместит ь в списо к названи я шрифто в
// разместит ь все виджет ы в диалогово м окне
Операци я WidgetChange d обеспечивае т правильну ю совместну ю работ у
виджетов:
void FontDialogDirector::WidgetChange d (
Widget * theChangedWidge t
if (theChangedWidge t == _fontList ) {
_fontName->SetText(_fontList->GetSelection() )
} else if (theChangedWidge t == _ok) {
// изменит ь шрифт и уничтожит ь диалогово е окно
// ...
} else if (theChangedWidge t == _cancel ) {
// уничтожит ь диалогово е окно
}
,}
Паттерн Mediato r
Сложность операции WidgetChange d возрастает пропорционально сложнос-
ти окна диалога. Создание очень больших диалоговых окон нежелательно и по
другим причинам, но в других приложениях сложность посредника может свести
на нет его преимущества.
Известные применения
И в ЕТ++ [WGM88], и в библиотеке классов THINK С [Sym93b] применяются
похожие на нашего распорядителя объекты для осуществления посредничества
между виджетами в диалоговых окнах.
Архитектура приложения в Smalltalk/V для Windows основана на структуре
посредника [LaL94]. В этой среде приложение состоит из окна Window, которое
содержит набор панелей. В библиотеке есть несколько предопределенных объек-
тов-панелей Рапе, например: TextPane, ListBox, Button и т.д. Их можно ис-
пользовать без подклассов. Разработчик приложения порождает подклассы только
от класса ViewManage r (диспетчер видов), отвечающего за обмен информацией
между панелями. ViewManage r - это посредник, каждая панель «знает» своего
диспетчера, который считается «владельцем» панели. Панели не ссылаются друг
на друга напрямую.
На изображенной диаграмме объектов показан мгновенный снимок работаю-
щего приложения.
В Smalltalk/V для обмена информацией между объектами Рапе и ViewManager
используется механизм событий. Панель генерирует событие для получения дан-
ных от своего посредника или для информирования его о чем-то важном. С каж-
дым событием связан символ (например, #select), который однозначно его
идентифицирует. Диспетчер видов регистрирует вместе с панелью селектор метода,
который является обработчиком события. Из следующего фрагмента кода видно, как
объект ListPane создается внутри подкласса ViewManage r и как ViewManage r
регистрирует обработчик события #select:
self addSubpane: (ListPan e new
paneName: 'myListPane';
owner: self;
when: #selec t perform: #listSelect:).
Паттерн ы поведени я
При координаци и сложны х обновлени й также требуетс я паттер н посредник.
Примеро м може т служит ь клас с ChangeManager, упомянуты й в описани и пат-
терна наблюдатель. Этот клас с осуществляе т посредничеств о межд у субъектам и
и наблюдателями, чтоб ы не делат ь лишни х обновлений. Когд а объек т изменяет -
ся, он извещае т ChangeManager, которы й координируе т обновлени е и информи -
рует все необходимы е объекты.
Аналогичны м образо м посредни к применяетс я в графически х редактора х
Unidraw [VL90], где используется класс CSolver, следящий за соблюдением огра-
ничени й связанност и межд у коннекторами. Объект ы в графически х редактора х
могут быть визуальн о соединен ы межд у собой различным и способами. Коннекто -
ры полезн ы в приложениях, которы е автоматическ и поддерживаю т связанность,
наприме р в редактора х диаграм м и в система х проектировани я электронны х схем.
Клас с CSolve r являетс я посреднико м межд у коннекторами. Он разрешае т огра -
ничени я связанност и и обновляе т позици и коннекторо в так, чтобы отразит ь изме -
нения.
Родственные паттерны
Фаса д отличаетс я от посредник а тем, что абстрагируе т некотору ю подсисте -
му объекто в для предоставлени я боле е удобног о интерфейса. Его протоко л одно -
направленный, то есть объект ы фасад а направляю т запрос ы класса м подсисте -
мы, но не наоборот. Посредни к же обеспечивае т совместно е поведение, которо е
объекты-коллег и не могу т или не «хотят » реализовывать, и его протоко л двуна -
правленный.
Коллег и могу т обмениватьс я информацие й с посреднико м посредство м пат-
терна наблюдатель.
Паттер н Mement o
Название и классификация паттерна
Хранител ь - паттер н поведени я объектов.
Назначение
Не наруша я инкапсуляции, фиксируе т и выноси т за предел ы объект а его внут -
ренне е состояни е так, чтоб ы поздне е можн о было восстановит ь в нем объект.
Известен также под именем
Toke n (лексема).
Мотивация
Иногд а необходим о тем или иным способо м зафиксироват ь внутренне е состоя -
ние объекта. Така я потребност ь возникает, например, при реализаци и контрольны х
точек и механизмо в отката, позволяющи х пользовател ю отменит ь пробну ю опера -
цию или восстановит ь состояни е после ошибки. Его необходим о где-т о сохранить,
чтобы поздне е восстановит ь в нем объект. Но обычн о объект ы инкапсулирую т все
Паттерн Memento
свое состояние или хотя бы его часть, делая его недоступным для других объектов,
так что сохранить состояние извне невозможно. Раскрытие же состояния явилось
бы нарушением принципа инкапсуляции и поставило бы под угрозу надежность
и расширяемост ь приложения.
Рассмотрим, например, графический редактор,
который поддерживает связанность объектов. Поль-
зователь может соединить два прямоугольника ли-
нией, и они останутся в таком положении при лю-
бых перемещениях. Редактор сам перерисовывает
линию, сохраняя связанность конфигурации.
Система разрешения ограничений - хорошо известный способ поддержания
связанности между объектами. Ее функции могут выполняться объектом класса
ConstraintSolver, который регистрирует вновь создаваемые соединения и ге-
нерирует описывающие их математические уравнения. А когда пользователь ка-
ким-то образом модифицирует диаграмму, объект решает эти уравнения. Результа-
ты вычислений объект ConstraintSolve r использует для перерисовки графики
так, чтобы были сохранены все соединения.
Поддержка отката операций в приложениях не
так проста, как может показаться на первый взгляд.
Очевидный способ откатить операцию перемеще-
ния - это сохранить расстояние между старым
и новым положением, а затем переместить объект
на такое же расстояние назад. Однако при этом не
гарантируется, что все объекты окажутся там же,
где находились. Предположим, что в способе расположения соединительной ли-
нии есть некоторая свобода. Тогда, переместив прямоугольник на прежнее место,
мы можем не добиться желаемого эффекта.
Открытого интерфейса ConstraintSolver иногда не хватает для точного от-
ката всех изменений смежных объектов. Механизм отката должен работать в тес-
ном взаимодействии с ConstraintSolver для восстановления предыдущего
состояния, но необходимо также позаботиться о том, чтобы внутренние детали
ConstraintSolver не были доступны этому механизму.
Паттерн хранитель поможет решить данную проблему. Хранитель — это объект,
в котором сохраняется внутреннее состояния другого объекта - хозяина храните-
ля. Для работы механизма отката нужно, чтобы хозяин предоставил хранитель,
когда возникнет необходимост ь записать контрольную точку состояния хозяина.
Только хозяину разрешено помещать в хранитель информацию и извлекать ее от-
туда, для других объектов хранитель непрозрачен.
В примере графического редактора, который обсуждался выше, в роли хозяи-
на может выступать объект ConstraintSolver. Процесс отката характеризует -
ся такой последовательность ю событий:
1. Редактор запрашивает хранитель у объекта ConstraintSolver в процессе
выполнения операции перемещения.
2. ConstraintSolver создает и возвращает хранитель, в данном случае эк-
земпляр класса SolverState..Хранитель SolverState содержит структуры
Паттерн ы поведени я
данных, описывающи е текуще е состояни е внутренни х уравнени й и перемен -
ных ConstraintSolver.
3. Позже, когда пользовател ь отменяе т операци ю перемещения, редакто р воз-
вращае т SolverState объект у ConstraintSolver.
4. Основываясь на информации, которая хранится в объекте SolverState,
ConstraintSolver изменяет свои внутренние структуры, возвращая урав-
нения и переменные в первоначально е состояние.
Такая организация позволяет объекту ConstraintSolver «знакомить» дру-
гие объект ы с информацией, котора я ему необходим а для возврат а в предыдуще е
состояние, не раскрыва я в то же время свою структур у и представление.
Применимость
Используйт е паттер н хранитель, когда:
а необходим о сохранит ь мгновенны й снимо к состояни я объект а (или его части),
чтобы впоследстви и объект можно было восстановит ь в том же состоянии;
а прямо е получени е этог о состояни я раскрывае т детал и реализаци и и нару -
шает инкапсуляци ю объекта.
Структура
Участники
a Memento (SolverState) - хранитель:
- сохраняе т внутренне е состояни е объект а Originator. Объем сохраняе -
мой информаци и може т быть различны м и определяетс я потребностям и
хозяина;
- запрещае т досту п всем други м объектам, кроме хозяина. По существу,
у хранителей есть двалнтерфейса. «Посыльный» Caretaker «видит»
лишь «z/зкмм » интерфей с хранител я - он може т тольк о передават ь храни-
теля други м объектам. Напротив, хозяин у доступе н «широкий » интер -
фейс, которы й обеспечивае т досту п ко всем данным, необходимы м для
восстановлени я в прежне м состоянии. Идеальны й вариан т - когда толь-
ко хозяину, создавшему хранитель, открыт доступ к внутреннему состоя-
нию последнего;
a Originator (ConstraintSolver) - хозяин:
- создае т хранитель, содержащег о снимо к текущег о внутреннег о состояния;
- используе т хранител ь для восстановлени я внутреннег о состояния;
Паттер н Mement o
a Caretaker (механизм отката) - посыльный:
- отвечает за сохранение хранителя;
- не производит никаких операций над хранителем и не исследует его внут-
реннее содержимое.
Отношения
а посыльный запрашивает хранитель у хозяина, некоторое время держит его
у себя, а затем возвращает хозяину, как видно на представленной диаграмме
взаимодействий.
Иногда этого не происходит, так как последнему не нужно восстанавливать
прежнее состояние;
а хранители пассивны. Только хозяин, создавший хранитель, имеет доступ
к информации о состоянии.
Результаты
Характерные особенности паттерна хранитель:
а сохранение границ инкапсуляции. Хранитель позволяет избежать раскрытия
информации, которой должен распоряжаться только хозяин, но которую тем
не менее необходимо хранить вне последнего. Этот паттерн экранирует
объекты от потенциально сложного внутреннего устройства хозяина, не из-
меняя границы инкапсуляции;
а упрощение структуры хозяина. При других вариантах дизайна, направлен-
ного на сохранение границ инкапсуляции, хозяин хранит внутри себя вер-
сии внутреннего состояния, которое запрашивали клиенты. Таким образом,
вся ответственность за управление памятью лежит на хозяине. При пере-
кладывании заботы о запрошенном состоянии на клиентов упрощается струк-
тура хозяина, а клиентам дается возможность не информировать хозяина
о том, что они закончили работу;
а значительные издержки при использовании хранителей. С хранителями могут
быть связаны заметные издержки, если хозяин должен копировать большой
Паттерн ы поведени я
объем информации для занесения в память хранителя или если клиенты со-
здают и возвращают хранителей достаточно часто. Если плата за инкапсуля-
цию и восстановление состояния хозяина велика, то этот паттерн не всегда
подходит (см. также обсуждение инкрементности в разделе «Реализация»);
а определение «узкого» и «широкого» интерфейсов. В некоторых языках слож-
но гарантировать, что только хозяин имеет доступ к состоянию хранителя;
а скрытая плата за содержание хранителя. Посыльный отвечает за удаление
хранителя, однако не располагает информацией о том, какой объем инфор-
мации о состоянии скрыт в нем. Поэтому нетребовательный к ресурсам по-
сыльный может расходовать очень много памяти при работе с хранителем.
Реализация
При реализации паттерна хранитель следует иметь в виду:
а языковую поддержку. У хранителей есть два интерфейса: «широкий» для
хозяев и «узкий» для всех остальных объектов. В идеале язык реализации
должен поддерживать два уровня статического контроля доступа. В C++ это
возможно, если объявить хозяина другом хранителя и сделать закрытым
«широкий» интерфейс последнего (с помощью ключевого слова private).
Открытым (public) остается только «узкий» интерфейс. Например:
class State;
class Originator {
public:
Memento* CreateMemento();
void SetMemento(const Memento*);
// ...
private:
State* _state; // внутренние структуры данных
// ...
};
clas s Mement o {
public:
// узки й открыты й интерфей с
virtua l ~Memento();
private:
// закрыты е член ы доступн ы тольк о хозяин у Originato r
frien d clas s Originator;
Memento();
void SetState(State*);
State * GetState( ) ;
// ...
private:
State * state;
// ...
};
Паттер н Mement o
а сохранение инкрементых изменений. Если хранители создаются и возвраща-
ются своему хозяину в предсказуемой последовательности, то хранитель
может сохранить лишь изменения во внутреннем состоянии хозяина.
Например, допускающие отмену команды в списке истории могут пользо-
ваться хранителями для восстановления первоначального состояния (см.
описание паттерна команда). Список истории предназначен только для от-
мены и повтора команд. Это означает, что хранители могут работать лишь
с изменениями, сделанными командой, а не с полным состоянием объекта.
В примере из раздела «Мотивация» объект, отменяющий ограничения, мо-
жет содержать только такие внутренние структуры, которые изменяются
с целью сохранить линию, соединяющую прямоугольники, а не абсолютные
позиции всех объектов.
Пример кода
Приведенный пример кода на языке C++ иллюстрирует рассмотренный выше
пример класса ConstraintSolver для разрешения ограничений. Мы используем
объекты MoveCommand (см. паттерн команда) для выполнения и отмены перено-
са графического объекта из одного места в другое. Графический редактор вызыва-
ет операцию Execute объекта-команды, чтобы переместить объект, и команду
Unexecute, чтобы отменить перемещение. В команде хранятся координаты места на-
значения, величина перемещения и экземпляр класса ConstraintSolverMement o —
хранителя, содержащего состояние объекта ConstraintSolver:
clas s Graphic;
// базовы й клас с графически х объекто в
clas s MoveCoitiman d {
public:
MoveComman d (Graphic * target, cons t Point & delta);
voi d Execut e ( ) ;
voi d Unexecut e ( ) ;
private:
ConstraintSolverMemento * _stat e ;
Poin t _delta;
Graphic * _target;
};
Ограничения связанности устанавливаются классом ConstraintSolver.
Его основная функция-член, называемая Solve, отменяет ограничения, реги-
стрируемые операцией AddConstraint. Для поддержки отмены действий со-
стояние объекта ConstraintSolver можно разместить в экземпляре класса
ConstraintSolverMement o с помощью операции CreateMemento. В преды-
дущее состояние объект ConstraintSolver возвращается посредством операции
SetMemento. ConstraintSolver является примером паттерна одиночка:
clas s ConstraintSolve r {
public:
stati c ConstraintSolver * Instanced;
voi d Solve();
Паттерн ы поведени я
voi d AddConstraint (
Graphic * startConnection, Graphic * endConnectio n
);
voi d RemoveConstraint (
Graphic * startConnection, Graphic * endConnectio n
);
ConstraintSolverMemento * CreateMemento();
voi d SetMemento(ConstraintSolverMemento*);
private:
// нетривиально е состояни е и операци и
// дл я поддержк и семантик и связанност и
};
clas s ConstraintSolverMement o {
public:
virtua l -ConstraintSolverMemento();
private:
frien d clas s ConstraintSolver;
ConstraintSolverMemento();
// закрыто е состояни е Solve r
};
С такими интерфейсами мы можем реализоват ь функции-член ы Execut e
и Unexecut e в классе MoveComman d следующим образом:
voi d MoveCommand::Execut e () {
ConstraintSolver * solve r = ConstraintSolver::Instance();
_stat e = solver->CreateMemento(); // создани е хранител я
_target->Move(_delta);
solver->Solve();
}
voi d MoveCommand::Unexecut e () {
ConstraintSolver * solve r = ConstraintSolver::Instance();
_target->Move(-_delta);
solver->SetMemento(_state); // восстановлени е состояни я хозяин а
solver->Solve(};
}
Execute запрашивает хранитель ConstraintSolverMemento перед началом
перемещения графическог о объекта. Unexecut e возвращае т объект на прежнее
место, восстанавливае т состояние Solver и обращается к последнему с целью от-
менить ограничения.
Известные применения
Предыдущий пример основан на поддержке связанност и в каркасе Unidraw
с помощью класса CSolver [VL90].
В коллекция х языка Dylan [App92] для итерации предусмотре н интерфейс,
напоминающи й паттерн хранитель. Для этих коллекций существуе т понятие
состояния объекта, которое является хранителем, представляющим состояние
Паттерн Mement o
итерации. Представление текущего состояния каждой коллекции может быть лю-
бым, но оно полностью скрыто от клиентов. Решение, используемое в языке Dylan,
можно написать на C++ следующим образом:
templat e <clas s Item >
clas s Collectio n {
public:
Collection();
IterationState * CreatelnitialState();
void Next(IterationState* ) ;
bool IsDone(cons t IterationState* ) const;
Item Currentltemfcons t IterationState* ) const;
IterationState * Copy(cons t IterationState* ) const;
void Appendfcons t Item&);
void Remove(cons t Item&);
// ...
};
Операция Createl ni ti al State возвращает инициализированный объект
IterationState для коллекции. Операция Next переходит к следующему объек-
ту в порядке итерации, по сути дела, она увеличивает на единицу индекс итерации.
Операция IsDone возвращает true, если в результате выполнения Next мы оказа-
лись за последним элементом коллекции. Операция Cur rent It em разыменовыва-
ет объект состояния и возвращает тот элемент коллекции, на который он ссылает-
ся. Сору возвращает копию данного объекта состояния. Это имеет смысл, когда
необходимо оставить закладку в некотором месте, пройденном во время итерации.
Если есть класс ItemType, то обойти коллекцию, составленную из его экземп-
ляров, можно так:1
clas s ItemTyp e {
public:
void Proces s () ;
// ...
};
Collection<ItemType* > aCollection;
IterationState * state;
stat e = aCollection.CreatelnitialStatef);
whil e (laCollection.IsDone(state) ) {
aCollection.Currentltem(state)->Process( )
aCollection.Next(state);
}
delet e state;
Отметим, чт о в наше м пример е объек т состояни я удаляетс я п о завершени и итерации. Н о операто р
delete не буде т вызван, есл и Processlte m возбуди т исключение, поэтом у в памят и остаетс я му -
сор. Эт о проблем а в язык е C++, н о н е в Dylan, гд е ест ь сборщи к мусора. Решени е проблем ы обсужда -
етс я н а стр. 258.
Паттерны поведения
У интерфейс а итерации, основанног о на паттерне хранитель, есть два преиму-
щества:
а с одной коллекцие й может быть связано несколько активных состояний (то
же самое верно и для паттерна итератор);
а не требуется нарушат ь инкапсуляци ю коллекции для поддержки итерации.
Хранител ь интерпретируетс я только самой коллекцией, больше никто к нему
доступа не имеет. При других подходах приходитс я нарушать инкапсуляцию,
объявляя классы итераторов друзьями классов коллекций (см. описание пат-
терна итератор). В случае с хранителе м ситуация противоположная: класс
коллекции Collection является другом класса IteratorState.
В библиотек е QOCA для разрешения ограничени й в хранителя х содержитс я
информация об изменениях. Клиент может получит ь хранитель, характеризую-
щий текущее решение системы ограничений. В хранителе находятс я только те
переменные ограничений, которые были преобразован ы со времени последнег о
решения. Обычно при каждом новом решении изменяетс я лишь небольшое под-
множеств о переменных Solver. Но этого достаточно, чтобы вернуть Solver
к предыдущему решению; для отката к более ранним решениям необходимо иметь
все промежуточны е хранители. Поэтому передават ь хранители в произвольно м по-
рядке нельзя. QOCA используе т механизм ведения истории для возврата к преж-
ним решениям.
Родственные паттерны
Команда: команды помещают информацию о состоянии, необходиму ю для от-
мены выполненных действий, в хранители.
Итератор: хранител и можно использоват ь для итераций, как было показано
выше.
Паттер н Observe r
Название и классификация паттерна
Наблюдател ь - паттерн поведения объектов.
Назначение
Определяе т зависимост ь типа «один ко многим» между объектами таким об-
разом, что при изменении состояния одного объекта все зависящие от него опове-
щаются об этом и автоматическ и обновляются.
Известен также под именем
Dependent s (подчиненные), Publish-Subscrib e (издатель-подписчик).
Мотивация
В результат е разбиения системы на множество совместно работающих классов
появляется необходимост ь поддерживат ь согласованно е состояние взаимосвязанных
Паттерн Observer
объектов. Но не хотелось бы, чтобы за согласованност ь надо было платит ь жесткой
связанность ю классов, так как это в некоторо й степени уменьшае т возможност и по-
вторног о использования.
Например, во многих библиотека х для построени я графически х интерфейсо в
пользовател я презентационны е аспект ы интерфейс а отделены от данных прило-
жения [КР88, LVC89, Р+88, WGM88]. С классами, описывающим и данные и их
представление, можно работат ь автономно. Электронна я таблица и диаграмма не
имеют информаци и друг о друге, поэтому вы вправе использоват ь их по отдельнос -
ти. Но ведут они себя так, как будто «знают » друг о друге. Когда пользовател ь ра-
ботает с таблицей, все изменени я немедленн о отражаютс я на диаграмме, и наобо-
р о т .
При таком поведени и подразумевается, что и электронна я таблица, и диаграм-
ма зависят от данных объект а и поэтому должны уведомлятьс я о любых измене -
ниях в его состоянии. И нет никаких причин, ограничивающи х количеств о зави-
симых объектов; для работы с одними и теми же данными может существоват ь
любое число пользовательски х интерфейсов.
Паттерн наблюдател ь описывает, как устанавливат ь такие отношения. Клю-
чевыми объектами в нем являютс я субъект и наблюдатель. У субъект а может
быть сколько угодно зависимых от него наблюдателей. Все наблюдател и уведом-
ляются об изменения х в состоянии субъекта. Получив уведомление, наблюдател ь
опрашивае т субъекта, чтобы синхронизироват ь с ним свое состояние.
Такого рода взаимодействи е часто называетс я отношение м издатель-подпис -
чик. Субъект издает или публикуе т уведомлени я и рассылае т их, даже не имея ин-
формации о том, какие объекты являютс я подписчиками. На получени е уведомле -
ний может подписатьс я неограниченно е количеств о наблюдателей.
Применимость
Используйт е паттерн наблюдател ь в следующи х ситуациях:
а когда у абстракци и есть два аспекта, один из которых зависит от другого.
Инкапсуляци и этих аспекто в в разные объект ы позволяют изменят ь и по-
вторно использоват ь их независимо;
Паттерны поведения
а когда при модификаци и одного объекта требуется изменить другие и вы не
знаете, сколько именно объектов нужно изменить;
а когда один объект должен оповещат ь других, не делая предположени й об
уведомляемых объектах. Другими словами, вы не хотите, чтобы объекты
были тесно связаны между собой.
Структура
Участники
a Subject - субъект:
- располагае т информацие й о своих наблюдателях. За субъектом может
«следить» любое число наблюдателей;
- предоставляет интерфейс для присоединения и отделения наблюдателей;
a Observer - наблюдатель:
- определяе т интерфейс обновления для объектов, которые должны быть
уведомлены об изменении субъекта;
a ConcreteSubject - конкретный субъект:
- сохраняе т состояние, представляюще е интерес для конкретног о наблюда-
теля ConcreteObserver;
- посылает информацию своим наблюдателям, когда происходит изменение;
a ConcreteObserver - конкретный наблюдатель:
- хранит ссылку на объект класса ConcreteSub j ect;
- сохраняет данные, которые должны быть согласованы с данными субъекта;
- реализует интерфейс обновления, определенный в классе Observer, что-
бы поддерживат ь согласованност ь с субъектом.
Отношения
а объект ConcreteSubjec t уведомляе т своих наблюдателе й о любом изме-
нении, которое могло бы привести к рассогласованност и состояний наблю-
дателя и субъекта;
а после получения от конкретног о субъекта уведомлени я об изменении объ-
ект ConcreteObserver может запросить у субъекта дополнительную
Паттерн Observe r
информацию, котору ю используе т для того, чтобы оказатьс я в состоянии,
согласованно м с состояние м субъекта.
На диаграмм е взаимодействи й показан ы отношени я между субъекто м и дву-
мя наблюдателями.
Отметим, что объект Observer, который инициируе т запрос на изменение,
откладывае т свое обновлени е до получени я уведомлени я от субъекта. Опера-
ция Notify не всегда вызываетс я субъектом. Ее может вызват ь и наблюдатель,
и посторонни й объект. В разделе «Реализация » обсуждаютс я часто встреча -
ющиес я варианты.
Результаты
Паттерн наблюдатель позволяет изменять субъекты и наблюдатели незави-
симо друг от друга. Субъект ы разрешаетс я повторн о использоват ь без участия на-
блюдателей, и наоборот. Это дает возможност ь добавлят ь новых наблюдателе й без
модификаци и субъект а или других наблюдателей.
Рассмотри м некоторы е достоинств а и недостатк и паттерна наблюдатель:
а абстрактная связанность субъекта и наблюдателя. Субъек т имеет инфор-
мацию лишь о том, что у него есть ряд наблюдателей, каждый из которых
подчиняетс я простом у интерфейс у абстрактног о класс а Observer. Субъ-
екту неизвестн ы конкретны е классы наблюдателей. Таким образом, связи
между субъектам и и наблюдателям и носят абстрактны й характе р и сведе-
ны к минимуму.
Поскольк у субъект и наблюдател ь не являютс я тесно связанными, то они
могут находиться на разных уровнях абстракции системы. Субъект более
низког о уровня может уведомлят ь наблюдателей, находящихс я на верхних
уровнях, не наруша я иерархи и системы. Если бы субъек т и наблюдател ь
представлял и собой единое целое, то получающийс я объект либо пересе -
кал бы границ ы уровне й (наруша я принци п их формирования), либо дол-
жен был находитьс я на каком-т о одном уровне (компрометиру я абстрак -
цию уровня);
Паттерны поведени я
а поддержка широковещательных коммуникаций. В отличие от обычного за-
проса для уведомления, посылаемого субъектом, не нужно задавать опреде-
ленного получателя. Уведомление автоматически поступает всем подписав-
шимся на него объектам. Субъекту не нужна информация о количестве
таких объектов, от него требуется всего лишь уведомить своих наблюдате-
лей. Поэтому мы можем в любое время добавлять и удалять наблюдателей.
Наблюдатель сам решает, обработать полученное уведомление или игнори-
ровать его;
а неожиданные обновления. Поскольку наблюдатели не располагают информа-
цией друг о друге, им неизвестно и о том, во что обходится изменение субъек-
та. Безобидная, на первый взгляд, операция над субъектом может вызвать
целый ряд обновлений наблюдателей и зависящих от них объектов. Более
того, нечетко определенные или плохо поддерживаемые критерии зависи-
мости могут стать причиной непредвиденных обновлений, отследить кото-
рые очень сложно.
Эта проблема усугубляется еще и тем, что простой протокол обновления не
содержит никаких сведений о том, что именно изменилось в субъекте. Без
дополнительного протокола, помогающего выяснить характер изменений,
наблюдатели будут вынуждены проделать сложную работу для косвенного
получения такой информации.
Реализация
В этом разделе обсуждаются вопросы, относящиеся к реализации механизма
зависимостей:
а отображение субъектов на наблюдателей. С помощью этого простейшего
способа субъект может отследить всех наблюдателей, которым он должен по-
сылать уведомления, то есть хранить на них явные ссылки. Однако при на-
личии большого числа субъектов и всего нескольких наблюдателей это мо-
жет оказаться накладно. Один из возможных компромиссов в пользу
экономии памяти за счет времени состоит в том, чтобы использовать ассоци-
ативный массив (например, хэш-таблицу) для хранения отображения меж-
ду субъектами и наблюдателями. Тогда субъект, у которого нет наблюдате-
лей, не будет зря расходовать память. С другой стороны, при таком подходе
увеличивается время поиска наблюдателей;
а наблюдение более чем за одним субъектом. Иногда наблюдатель может зави-
сеть более чем от одного субъекта. Например, у электронной таблицы быва-
ет более одного источника данных. В таких случаях необходимо расширить
интерфейс Update, чтобы наблюдатель мог «узнать», какой субъект прислал
уведомление. Субъект может просто передать себя в качестве параметра
операции Update, тем самым сообщая наблюдателю, что именно нужно об-
следовать;
а кто инициирует обновление. Чтобы сохранить согласованность, субъект
и его наблюдатели полагаются на механизм уведомлений. Но какой именно
объект вызывает операцию-Notif y для инициирования обновления? Есть
два варианта:
Паттерн Observer
- операци и класса Sub j ect, изменивши е состояние, вызывают Not i f у для
уведомлени я об этом изменении. Преимуществ о такого подхода в том, что
клиента м не надо помнит ь о необходимост и вызыват ь операци ю Noti f y
субъекта. Недостато к же заключаетс я в следующем: при выполнени и каж-
дой из нескольки х последовательны х операций будут производитьс я обнов-
ления, что может стать причиной неэффективно й работы программы;
- ответственност ь за своевременны й вызов Noti f y возлагаетс я на клиен-
та. Преимущество: клиент может отложит ь инициировани е обновлени я
до завершени я серии изменений, исключи в тем самым ненужные проме-
жуточные обновления. Недостаток: у клиенто в появляетс я дополнитель -
ная обязанность. Это увеличивае т вероятност ь ошибок, поскольк у кли-
ент может забыть вызват ь Notify;
а висячие ссылки на удаленные субъекты. Удалени е субъект а не должно при-
водить к появлени ю висячих ссылок у наблюдателей. Избежат ь этого мож-
но, например, поручив субъект у уведомлят ь все свои наблюдател и о своем
удалении, чтобы они могли уничтожит ь хранимые у себя ссылки. В общем
случае простое удалени е наблюдателе й не годится, так как на них могут
ссылаться другие объекты и под их наблюдение м могут находитьс я другие
субъекты;
а гарантии непротиворечивости состояния субъекта перед отправкой уведом-
ления. Важно быть уверенным, что перед вызовом операции Notif y состо-
яние субъект а непротиворечиво, поскольк у в процесс е обновлени я соб-
ственного состояния наблюдатели будут опрашивать состояние субъекта.
Правило непротиворечивост и очень легко нарушить, если операции одного
из подклассо в класса Subjec t вызывают унаследованны е операции. На-
пример, в следующе м фрагмент е уведомлени е отправляется, когда состоя-
ние субъект а противоречиво:
void MySubject::0peration (int newValue) {
BaseClassSubj ect::Operation(newValue);
// отправит ь уведомлени е
_my!nstVa r += newValue;
// обновит ь состояни е подкласс а (слишко м поздно!)
}
Избежат ь этой ловушк и можно, отправля я уведомлени я из шаблонны х ме-
тодов (см. описани е паттерна шаблонны й метод) абстрактног о класс а
Subject. Определит е примитивну ю операцию, замещаему ю в подклассах,
и обратитесь к Notify, используя последнюю операцию в шаблонном мето-
де. В таком случае существуе т гарантия, что состояни е объект а непротиво -
речиво, если операции Subject замещены в подклассах:
voi d Text::Cu t (TextRang e r) {
ReplaceRange(г); // переопределена в подклассах
Notify();
}
Паттерн ы поведени я
Кстати, всегда желательно фиксировать, какие операции класса Subject
инициируют обновления;
а как избежать зависимости протокола обновления от наблюдателя: модели
вытягивания и проталкивания. В реализациях паттерна наблюдатель субъ-
ект довольно часто транслирует всем подписчикам дополнительную инфор-
мацию о характере изменения. Она передается в виде аргумента операции
Update, и объем ее меняется в широких диапазонах.
На одном полюсе находится так называемая модель проталкивания (push
model), когда субъект посылает наблюдателям детальную информацию об
изменении независимо от того, нужно ли им это. На другом - модель вытя-
гивания (pull model), когда субъект не посылает ничего, кроме минимально-
го уведомления, а наблюдатели запрашивают детали позднее.
В модели вытягивания подчеркивается неинформированность субъекта о сво-
их наблюдателях, а в модели проталкивания предполагается, что субъект вла-
деет определенной информацией о потребностях наблюдателей. В случае
применения модели проталкивания степень повторного их использования
может снизиться, так как классы Sub j ect предполагают о классах Observer,
которые не всегда могут быть верны. С другой стороны, модель вытягивания
может оказаться неэффективной, ибо наблюдателям без помощи субъекта
необходимо выяснять, что изменилось;
а явное специфицирование представляющих интерес модификаций Эффек-
тивность обновления можно повысить, расширив интерфейс регистрации
субъекта, то есть предоставив возможность при регистрации наблюдателя
указать, какие события его интересуют. Когда событие происходит, субъект
информирует лишь тех наблюдателей, которые проявили к нему интерес.
Чтобы получать конкретное событие, наблюдатели присоединяются к сво-
им субъектам следующим образом:
void Subj ect::At t ach(Observer*, Aspects i nterest);
где interest определяет представляющее интерес событие. В момент по-
сылки уведомления субъект передает своим наблюдателям изменившийся
аспект в виде параметра операции Update. Например:
void Observer::Update(Subj ect*, Aspects interest);
а инкапсуляция сложной семантики обновления. Если отношения зависимос-
ти между субъектами и наблюдателями становятся особенно сложными, то
может потребоваться объект, инкапсулирующий эти отношения. Будем на-
зывать его ChangeManager (менеджер изменений). Он служит для мини-
мизации объема работы, необходимой для того чтобы наблюдатели смогли
отразить изменения субъекта. Например, если некоторая операция влечет
за собой изменения в нескольких независимых субъектах, то хотелось бы,
чтобы наблюдатели уведомлялись после того, как будут модифицированы
все субъекты, дабы не ставить в известность одного и того же наблюдателя
несколько раз.
Паттерн Observe r
У класс а ChangeManage r есть три обязанности:
- строит ь отображени е межд у субъекто м и его наблюдателям и и предо-
ставлят ь интерфей с для поддержани я отображени я в актуально м состоя -
нии. Это освобождае т субъекто в от необходимост и хранит ь ссылк и на
своих наблюдателе й и наоборот;
- определят ь конкретну ю стратеги ю обновления;
- обновлят ь всех зависимы х наблюдателе й по запрос у от субъекта.
На следующе й диаграмм е представлен а проста я реализаци я паттерн а на-
блюдатель с использованием менеджера изменений ChangeManager. Име-
ется два специализированны х менеджера. SimplechangeManage r всегд а
обновляе т всех наблюдателе й каждог о субъекта, a DAGChangeManage r обра-
батывае т направленны е ациклически е графы зависимосте й между субъектам и
и их наблюдателями. Когда наблюдател ь долже н «присматривать » за несколь -
кими субъектами, предпочтительне е использоват ь DAGChangeManager. В этом
случае изменени е сразу двух или более субъекто в може т привест и к избыточ -
ным обновлениям. Объект DAGChangeManage r гарантирует, что наблюдател ь
в любом случа е получи т только одно уведомление. Если обновлени е одног о
и того же наблюдател я допускаетс я нескольк о раз подряд, то вполне достаточ -
но объекта SimplechangeManager.
ChangeManage r - это приме р паттерн а посредник. В обще м случа е есть
только один объект ChangeManager, известны й всем участникам. Поэтом у
полезе н будет также и паттер н одиночка;
а комбинирование классов Subject и Observer, В библиотека х классов, которые на-
писаны на языках, не поддерживающи х множественног о наследовани я (на-
пример, на Smalltalk), обычн о не определяютс я отдельны е класс ы Subjec t
и Observer. Их интерфейс ы комбинируютс я в одном классе. Это позво -
ляет определит ь объект, выступающи й в роли одновременн о субъект а
Паттерны поведения
и наблюдателя, без множественного наследования. Так, в Smalltalk интерфей-
сы Sub j ect и Observer определены в корневом классе Obj ect и потому до-
ступны вообще всем классам.
Пример кода
Интерфейс наблюдателя определен в абстрактном классе Observer:
class Subject;
clas s Observe r {
public:
virtua l -Observe r ();
virtua l voi d Updat e (Subject * theChangedSubject ) = 0;
protected:
Observe r ( ) ,-
};
При такой реализации поддерживаетс я несколько субъектов для одного на-
блюдателя. Передача субъекта параметром операции Update позволяет наблюда-
телю определить, какой из наблюдаемых им субъектов изменился.
Таким же образом в абстрактном классе Subject определен интерфейс
субъекта:
class Subject {
public:
virtual -Subject()
virtual void Attach(Observer*);
virtual void Detach(Observer*);
virtual void Notify();
protected:
Subject();
private:
List<0bserver*> *_observers;
};
void Subject::Attach (Observer* o) {
_observers->Append(o);
}
void Subject::Detach (Observer* o) {
_observers->Remove(o);
}
void Subject::Notify () {
Listlterator<0bserver*> i(„observers);
for (i.First (); !i.IsDone() ; i.NextO) {
i.Currentltemf)->Update(this);
}
}
Паттерн Observer
ClockTimer - это конкретный субъект, который следит за временем суток.
Он извещает наблюдателей каждую секунду. Класс ClockTimer предоставляет
интерфей с для получени я отдельны х компоненто в времени: часа, минуты, секун-
ды и т.д.:
clas s ClockTime r : publi c Subjec t {
public:
ClockTimer();
virtual int GetfHour();
virtua l int GetMinute();
virtua l int GetSecond(') ;
voi d Tick();
};
Операция Tick вызывается через одинаковые интервалы внутренним тайме-
ром. Тем самым обеспечивается правильный отсчет времени. При этом обновля-
ется внутреннее состояние объекта ClockTimer и вызывается операция Notify
для извещения наблюдателей об изменении:
voi d ClockTimer::Tic k () {
// обновит ь внутренне е представлени е о времен и
// ...
Notif y () ;
}
Теперь мы можем определить класс DigitalClock, который отображает время.
Свою графическую функциональност ь он наследует от класса Widget, предоставля-
емого библиотекой для построения пользовательских интерфейсов. Интерфейс на-
блюдателя подмешивается к интерфейсу DigitalCloc k путем наследования от клас-
са Observer:
clas s DigitalClock: publi c Widget, publi c Observe r {
public:
DigitalClock(ClockTimer*);
virtua l -DigitalClock();
virtua l voi d Update(Subject*);
// замещае т операци ю класс а Observe r
virtua l voi d Draw();
// замещае т операци ю класс а Widget;
// определяе т спосо б изображени я часо в
private:
ClockTimer * _subject;
};
DigitalClock::DigitalCloc k (ClockTimer * s) {
Паттерны поведения
_subjec t = s;
_subject->Attach(this);
}
DigitalClock::~DigitalCloc k () {
_subject->Detach(this);
}
Прежде чем начнется рисование часов посредством операции Update, будет
проверено, что уведомлени е получен о именно от объекта таймера:
voi d DigitalClock::Updat e (Subject * theChangedSubject ) {
if (theChangedSubjec t == _subject ) {
Draw();
}
}
voi d DigitalClock::Dra w () {
// получит ь новы е значени я от субъект а
int hou r = _subject->GetHour();
int minut e = _subject->GetMinute();
// и т.д.
// нарисоват ь цифровы е час ы
}
Аналогично можно определить класс AnalogClock:
clas s AnalogCloc k : publi c Widget, publi c Observe r {
public:
AnalogClock(ClockTimer*);
virtua l voi d Update(Subject*);
virtua l voi d Draw();
// ...
};
Следующий код создает объекты классов AnalogClock и DigitalClock, ко-
торые всегда показывают одно и то же время:
ClockTimer * time r = new - ClockTimer;
AnalogClock * analogcloc k = ne w AnalogCloc k ( t ime r ) ;
DigitalClock * digitalCloc k = ne w DigitalClock(timer ) ;
При каждом срабатывании таймера timer оба экземпляра часов обновляют-
ся и перерисовывают себя.
Известные применения
Первый и, возможно, самый известный пример паттерна наблюдатель по-
явился в схеме модель/вид/контроллер (МУС) языка Smalltalk, которая пред-
ставляет собой каркас для построения пользовательских интерфейсов в среде
Паттер н Observe r
Smalltalk [KP88]. Класс Model в MVC - это субъект, a View - базовый-класс для
наблюдателей. В языках Smalltalk, ET++ [ WGM88] и библиотеке классов THINK
[Sym93b] предлагается общий механизм зависимостей, в котором интерфейсы
субъекта и наблюдателя помещены в класс, являющийся общим родителем всех
остальных системных классов.
Среди других библиотек для построения интерфейсов пользователя, в кото-
рых используется паттерн наблюдатель, стоит упомянуть Interviews [LVC89],
Andrew Toolkit [P+88] и Unidraw [VL90]. В Interviews явно определены классы
Observer и Observable (для субъектов). В библиотеке Andrew они называют-
ся видом (view) и объектом данных (data object) соответственно. Unidraw делит
объекты графического редактора на части View (для наблюдателей) и Subject.
Родственные паттерны
Посредник: класс ChangeManager действует как посредник между субъек-
тами и наблюдателями, инкапсулируя сложную семантику обновления.
Одиночка: класс ChangeManager может воспользоваться паттерном одиноч-
ка, чтобы гарантировать уникальность и глобальную доступность менеджера из-
менений.
Паттер н Stat e
Название и классификация паттерна
Состояние - паттерн поведения объектов.
Назначение
Позволяет объекту варьировать свое поведение в зависимости от внутреннего
состояния. Извне создается впечатление, что изменился класс объекта.
Мотивация
Рассмотрим класс TCPConnection, с помощью которого представлено сете-
вое соединение. Объект этого класса может находиться в одном из нескольких со-
стояний: Established (установлено), Li steni ng (прослушивание), Closed
(закрыто). Когда объект TCPConnection получает запросы от других объектов,
то в зависимости от текущего состояния он отвечает по-разному. Например, ответ
на запрос Open (открыть) зависит от того, находится ли соединение в состоянии
Closed или Established. Паттерн состояние описывает, каким образом объект
TCPConnect ion может вести себя по-разному, находясь в различных состояниях.
Основная идея этого паттерна заключается в том, чтобы ввести абстрактный
класс TCPState для представления различных состояний соединения. Этот класс
объявляет интерфейс, общий для всех классов, описывающих различные рабочие
состояния. В подклассах TCPState реализовано поведение, специфичное для кон-
кретного состояния. Например, в классах TCPEstabli shed и TCPClosed реали-
зовано поведение, характерное для состояний Established и Closed соответ-
ственно.
Паттерны поведения
Класс TCPConnect ion хранит у себя объект состояния (экземпляр некоторого
подкласса TCPState), представляющий текущее состояние соединения, и деле-
гирует все зависящие от состояния запросы этому объекту. TCPConnection ис-
пользует свой экземпляр подкласса TCPState для выполнения операций, свойствен-
ных только данному состоянию соединения.
При каждом изменении состояния соединения TCPConnection изменяет свой
объект-состояние. Например, когда установленное соединение закрывается,
TCPConnection заменяет экземпляр класса TCPEstablishe d экземпляром
TCPCIosed.
Применимость
Используйте паттерн состояние в следующих случаях:
Q когда поведение объекта зависит от его состояния и должно изменяться во
время выполнения;
О когда в коде операций встречаются состоящие из многих ветвей условные
операторы, в которых выбор ветви зависит от состояния. Обычно в таком
случае состояние представлено перечисляемыми константами. Часто одна
и та же структура условного оператора повторяется в нескольких операци-
ях. Паттерн состояние предлагает поместить каждую ветвь в отдельный
класс. Это позволяет трактовать состояние объекта как самостоятельный
объект, который может изменяться независимо от других.
Структура
Паттерн State
Участники
О Context (TCPConnection) - контекст:
- определяе т интерфейс, представляющи й интере с для клиентов;
- храни т экземпля р подкласс а ConcreteState, которы м определяетс я те-
куще е состояние;
Q State (TCPState) - состояние:
- определяе т интерфей с для инкапсуляци и поведения, ассоциированног о
с конкретны м состояние м контекст а Context;
Q Подклассы ConcreteState (TCPEstablished, TCPListen, TCPClosed) -
конкретно е состояние:
- кажды й подклас с реализуе т поведение, ассоциированно е с некоторы м со-
стояние м контекст а Context.
Отношения
Q клас с Contex t делегируе т зависящи е от состояни я запрос ы текущем у объ-
екту ConcreteState;
Q контекст може т передать себя в качеств е аргумент а объекту State, кото-
рый буде т обрабатыват ь запрос. Это дает возможност ь объекту-состояни ю
при необходимост и получит ь досту п к контексту;
Q Contex t - это основно й интерфей с для клиентов. Клиент ы могу т конфигу -
рироват ь контекс т объектам и состояни я State. Один раз сконфигуриро -
вав контекст, Клиент ы уже не должн ы напряму ю связыватьс я с объектам и
состояния;
О либо Context, либо подкласс ы ConcreteStat e могу т решить, при каки х
условия х и в како м порядк е происходи т смена состояний.
Результаты
Результат ы использовани я паттерн а состояние:
Q локализует зависящее от состояния поведение и делит его на части, соот-
ветствующие состояниям. Паттер н состояни е помещае т все поведение, ассо-
циированно е с конкретны м состоянием, в отдельны й объект. Поскольк у зави-
сящий от состояни я код целико м находитс я в одно м из подклассо в класс а
State, то добавлят ь новые состояни я и переход ы можн о прост о путе м порож-
дения новых подклассов.
Вмест о этог о можн о было бы использоват ь данные-член ы для определени я
внутренни х состояний, тогда операци и объект а Contex t проверял и бы эти
данные. Но в таком случа е похожи е условны е оператор ы или оператор ы ветв-
ления были бы разбросаны по всему коду класса Context. При этом добав-
ление новог о состояни я потребовал о бы изменени я нескольки х операций,
что затруднил о бы сопровождение.
Паттер н состояни е позволяе т решит ь эту проблему, но одновременн о по-
рождае т другую, поскольк у поведени е для различны х состояни й оказыва -
ется распределенны м межд у нескольким и подклассам и State. Это увели -
чивае т числ о классов. Конечно, один клас с компактнее, но если состояни й
Паттерн ы поведени я
много, то такое распределени е эффективнее, так как в противно м случае
пришлос ь бы имет