автордың кітабын онлайн тегін оқу Паттерны объектно-ориентированного проектирования
Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес
Паттерны объектно-ориентированного проектирования. — СПб.: Питер, 2022.
ISBN 978-5-4461-1595-2
© ООО Издательство "Питер", 2022
Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав.
Предисловие
Книга не является введением в объектно-ориентированное программирование или проектирование. На эти темы написано много других хороших книг. Предполагается, что вы достаточно хорошо владеете по крайней мере одним объектно-ориентированным языком программирования и имеете какой-то опыт объектно-ориентированного проектирования. Безусловно, у вас не должно возникать необходимости лезть в словарь за разъяснением терминов «тип», «полиморфизм», и вам понятно, чем «наследование интерфейса» отличается от «наследования реализации».
С другой стороны, эта книга и не научный труд, адресованный исключительно узким специалистам. Здесь говорится о паттернах проектирования и описываются простые и элегантные решения типичных задач, возникающих в объектно-ориентированном проектировании. Паттерны проектирования не появились сразу в готовом виде; многие разработчики, искавшие возможности повысить гибкость и степень пригодности к повторному использованию своих программ, приложили много усилий, чтобы поставленная цель была достигнута. В паттернах проектирования найденные решения воплощены в краткой и легко применимой на практике форме.
Для использования паттернов не нужны ни какие-то особенные возможности языка программирования, ни хитроумные приемы, поражающие воображение друзей и начальников. Все можно реализовать на стандартных объектно-ориентированных языках, хотя для этого потребуется приложить несколько больше усилий, чем в случае специализированного решения, применимого только в одной ситуации. Но эти усилия неизменно окупаются за счет большей гибкости и возможности повторного использования.
Когда вы усвоите работу с паттернами проектирования настолько, что после удачного их применения воскликнете «Ага!», а не будете смотреть в сомнении на получившийся результат, ваш взгляд на объектно-ориентированное проектирование изменится раз и навсегда. Вы сможете строить более гибкие, модульные, повторно используемые и понятные конструкции, а разве не для этого вообще существует объектно-ориентированное проектирование?
Несколько слов, чтобы предупредить и одновременно подбодрить вас. Не огорчайтесь, если не все будет понятно после первого прочтения книги. Мы и сами не всё понимали, когда начинали писать ее! Помните, что эта книга не из тех, которые, однажды прочитав, ставят на полку. Надеемся, что вы будете возвращаться к ней снова и снова, черпая идеи и ожидая вдохновения.
Книга созревала довольно долго. Она повидала четыре страны, была свидетелем женитьбы трех ее авторов и рождения двух младенцев. В ее создании так или иначе участвовали многие люди. Особую благодарность мы выражаем Брюсу Андерсону (Bruce Anderson), Кенту Беку (Kent Beck) и Андре Вейнанду (Andre Weinand) за поддержку и ценные советы. Также благодарим всех рецензентов черновых вариантов рукописи: Роджера Билефельда (Roger Bielefeld), Грейди Буча (Grady Booch), Тома Каргилла (Tom Cargill), Маршалла Клайна (Marshall Cline), Ральфа Хайра (Ralph Hyre), Брайана Кернигана (Brian Kernighan), Томаса Лалиберти (Thomas Laliberty), Марка Лоренца (Mark Lorenz), Артура Риля (Arthur Riel), Дуга Шмидта (Doug Schmidt), Кловиса Тондо (Clovis Tondo), Стива Виноски (Steve Vinoski) и Ребекку Вирфс-Брок (Rebecca Wirfs-Brock). Выражаем признательность сотрудникам издательства AddisonWesley за поддержку и терпение: Кейту Хабибу (Kate Habib), Тиффани Мур (Tiffany Moore), Лайзе Раффаэле (Lisa Raffaele), Прадипе Сива (Pradeepa Siva) и Джону Уэйту (John Wait). Особая благодарность Карлу Кесслеру (Carl Kessler), Дэнни Саббаху (Danny Sabbah) и Марку Вегману (Mark Wegman) из исследовательского отдела компании IBM за неослабевающий интерес к этой работе и поддержку.
И наконец, не в последнюю очередь мы благодарны всем тем людям, которые высказывали замечания по поводу этой книги по интернету, ободряли нас и убеждали, что такая работа действительно нужна. Вот далеко не полный перечень наших «незнакомых помощников»: Йон Авотинс (Jon Avotins), Стив Берчук (Steve Berczuk), Джулиан Бердич (Julian Berdych), Матиас Болен (Matthias Bohlen), Джон Брант (John Brant), Алан Кларк (Allan Clarke), Пол Чизхолм (Paul Chisholm), Йенс Колдьюи (Jens Coldewey), Дейв Коллинз (Dave Collins), Джим Коплиен (Jim Coplien), Дон Двиггинс (Don Dwiggins), Габриэль Элиа (Gabriele Elia), Дуг Фельт (Doug Felt), Брайан Фут (Brian Foote), Денис Фортин (Denis Fortin), Уорд Харольд (Ward Harold), Херман Хуэни (Hermann Hueni), Найим Ислам (Nayeem Islam), Бикрамжит Калра (Bikramjit Kalra), Пол Кифер (Paul Keefer), Томас Кофлер (Thomas Kofler), Дуг Леа (Doug Lea), Дэн Лалиберте (Dan LaLiberte), Джеймс Лонг (James Long), Анна Луиза Луу (Ann Louise Luu), Панди Мадхаван (Pundi Madhavan), Брайан Мэрик (Brian Marick), Роберт Мартин (Robert Martin), Дэйв МакКомб (Dave McComb), Карл МакКоннелл (Carl McConnell), Кристин Мингинс (Christine Mingins), Ханспетер Мессенбек (Hanspeter Mossenbock), Эрик Ньютон (Eric Newton), Марианна Озкан (Marianne Ozkan), Роксана Пайетт (Roxsan Payette), Ларри Подмолик (Larry Podmolik), Джордж Радин (George Radin), Сита Рамакришнан (Sita Ramakrishnan), Русс Рамирес (Russ Ramirez), Александр Ран (Alexander Ran), Дирк Риле (Dirk Riehle), Брайан Розенбург (Bryan Rosenburg), Аамод Сейн (Aamod Sane), Дури Шмидт (Duri Schmidt), Роберт Зайдль (Robert Seidl), Цинь Шу (Xin Shu) и Билл Уокер (Bill Walker).
Мы не считаем, что набор отобранных нами паттернов полон и неизменен, он всего лишь отражает наши нынешние представления о проектировании. Мы приветствуем любые замечания, будь то критика приведенных примеров, ссылки на известные способы использования, которые не упомянуты здесь, или предложения по поводу дополнительных паттернов. Вы можете писать нам на адрес издательства Addison-Wesley или на электронный адрес design-patterns@cs.uiuc.edu. Исходные тексты всех примеров можно получить, отправив сообщение «send design pattern source» по адресу design-patterns-source@cs.uiuc.edu. А теперь также есть веб-страница http://st-www.cs.uiuc.edu/users/patterns/DPBook/DPBook.html, на которой размещается последняя информация и обновления к книге.
Эрих Гамма, Маунтин Вью, штат Калифорния
Ричард Хелм, Монреаль, Квебек
Ральф Джонсон, Урбана, штат Иллинойс
Джон Влиссидес, Готорн, штат Нью-Йорк
Август 1994
От издательства
С момента издания классической книги «Приемы объектно-ориентированного проектирования. Паттерны проектирования» (Design Patterns: Elements of Reusable Object-Oriented Software) прошло 26 лет. За это время было продано более полумиллиона экземпляров книги на английском и 13 других языках. На этой книге выросло не одно поколение программистов.
В книге описываются простые и изящные решения типичных задач, возникающих в объектно-ориентированном проектировании.
Паттерны появились потому, что многие разработчики искали пути повышения гибкости и степени повторного использования своих программ. Найденные решения воплощены в краткой и легко применимой на практике форме. «Банда Четырех» объясняет каждый паттерн на простом примере четким и понятным языком. Использование паттернов при разработке программных систем позволяет проектировщику перейти на более высокий уровень разработки проекта. Теперь архитектор и программист могут оперировать образными названиями паттернов и общаться на одном языке.
Таким образом, книга решает две задачи.
Во-первых, знакомит с ролью паттернов в создании архитектуры сложных систем.
Во-вторых, позволяет проектировщикам с легкостью разрабатывать собственные приложения, применяя содержащиеся в справочнике паттерны.
Что изменилось в издании 2020 года?
• Актуализирована терминология (например, для «реорганизации» кода уже вполне прижился термин «рефакторинг», для share — «совместное использование» вместо «разделения», а для mixin — «примесь»);
• обновлен стиль;
• устранены излишне громоздкие слова (например, «специфицирование» или «инстанцирование». Первое можно вполне адекватно заменить «определением», второе — «созданием экземпляра»);
• книга наконец-то называется «Паттерны объектно-ориентированного проектирования».
В квадратных скобках даются ссылки на источники (см. Библиографию), а цифры в круглых скобках обозначают ссылку на страницу, где описывается тот или иной паттерн.
Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.
Глава 1. Введение в паттерны проектирования
Проектирование объектно-ориентированных программ — нелегкое дело, а если они предназначены для повторного использования, то все становится еще сложнее. Необходимо подобрать подходящие объекты, отнести их к различным классам, соблюдая разумную степень детализации, определить интерфейсы классов и иерархию наследования и установить ключевые отношения между классами. Дизайн должен, с одной стороны, соответствовать решаемой задаче, с другой — быть общим, чтобы удалось учесть все требования, которые могут возникнуть в будущем. Хотелось бы также избежать вовсе или, по крайней мере, свести к минимуму необходимость перепроектирования. Поднаторевшие в объектно-ориентированном проектировании разработчики скажут вам, что создать «правильный», то есть в достаточной мере гибкий и пригодный для повторного использования дизайн, с первой попытки очень трудно, если вообще возможно. Прежде чем считать цель достигнутой, они обычно пытаются опробовать найденное решение на нескольких задачах, и каждый раз модифицируют его.
И все же опытным проектировщикам удается создать хороший дизайн системы. В то же время новички испытывают шок от количества возможных вариантов и нередко возвращаются к привычным не объектно-ориентированным методикам. Проходит немало времени перед тем, как новички поймут, что же такое удачный объектно-ориентированный дизайн. Очевидно, опытные проектировщики знают какие-то тонкости, ускользающие от новичков. Так что же это?
Прежде всего, опытный разработчик понимает, что не нужно решать каждую новую задачу с нуля. Вместо этого он старается повторно воспользоваться теми решениями, которые оказались удачными в прошлом. Отыскав хорошее решение один раз, он будет прибегать к нему снова и снова. Именно благодаря накопленному опыту проектировщик и становится экспертом в своей области. Во многих объектно-ориентированных системах встречаются повторяющиеся паттерны, состоящие из классов и взаимодействующих объектов. С их помощью решаются конкретные задачи проектирования, в результате чего объектно-ориентированная архитектура становится более гибкой, элегантной, и может использоваться повторно. Проектировщик, знакомый с паттернами, может сразу же применять их к решению новой задачи, не пытаясь каждый раз изобретать велосипед.
Поясним нашу мысль через аналогию. Писатели редко выдумывают совершенно новые сюжеты. Вместо этого они берут за основу уже отработанные в мировой литературе схемы, жанры и образы. Например, персонаж — «трагический герой» (Макбет, Гамлет и т.д.), жанр — «любовный роман» (бесчисленные любовные романы). Точно так же в объектно-ориентированном проектировании используются такие паттерны, как «представление состояния с помощью объектов» или «декорирование объектов, чтобы было проще добавлять и удалять новую функциональность». Если вы знаете паттерн, многие проектировочные решения далее следуют автоматически.
Все мы знаем о ценности опыта. Сколько раз при проектировании вы испытывали дежавю, чувствуя, что уже когда-то решали такую же задачу, только никак не сообразить, когда и где? Если бы удалось вспомнить детали старой задачи и ее решения, то не пришлось бы придумывать все заново. Увы, у нас нет привычки записывать свой опыт на благо другим людям да и себе тоже.
Цель этой книги состоит как раз в том, чтобы документировать опыт разработки объектно-ориентированных программ в виде паттернов проектирования. Каждому паттерну мы присвоим имя, объясним его назначение и роль в проектировании объектно-ориентированных систем. Мы хотели отразить опыт проектирования в форме, которую другие люди могли бы использовать эффективно. Для этого некоторые из наиболее распространенных паттернов были формализованы и сведены в единый каталог.
Паттерны проектирования упрощают повторное использование удачных проектных и архитектурных решений. Представление прошедших проверку временем методик в виде паттернов проектирования делает их более доступными для разработчиков новых систем. Паттерны проектирования помогают выбрать альтернативные решения, упрощающие повторное использование системы, и избежать тех альтернатив, которые его затрудняют. Паттерны улучшают качество документации и сопровождения существующих систем, поскольку они позволяют явно описать взаимодействия классов и объектов, а также причины, по которым система была построена так, а не иначе. Проще говоря, паттерны проектирования дают разработчику возможность быстрее найти правильный путь.
Ни один из паттернов, представленных в книге, не описывает новые или непроверенные разработки. В книгу были включены только такие паттерны, которые неоднократно применялись в разных системах. По большей части они никогда ранее не документировались и либо хорошо известны только в объектно-ориентированном сообществе, либо были частью какой-то удачной объектно-ориентированной системы — ни один источник нельзя назвать простым для начинающих проектировщиков. Таким образом, хотя эти решения не новы, мы представили их в новом и доступном формате: в виде каталога паттернов в едином формате.
Хотя книга получилась довольно объемной, паттерны проектирования — лишь малая часть того, что необходимо знать специалисту в этой области. В издание не включено описание паттернов, имеющих отношение к параллелизму, распределенному программированию и программированию систем реального времени. Отсутствуют и сведения о паттернах, специфичных для конкретных предметных областей. Из этой книги вы не узнаете, как строить интерфейсы пользователя, как писать драйверы устройств и как работать с объектно-ориентированными базами данных. В каждой из этих областей есть свои собственные паттерны; возможно, в будущем кто-то систематизирует и их.
1.1. Что такое паттерн проектирования
По словам Кристофера Александера (Christopher Alexander), «любой паттерн описывает задачу, которая снова и снова возникает в нашей работе, а также принцип ее решения, причем таким образом, что это решение можно потом использовать миллион раз, и при этом никакие две реализации не будут полностью одинаковыми» [AIS+77]. Хотя Александер имел в виду паттерны, возникающие при проектировании зданий и городов, но его слова верны и в отношении паттернов объектно-ориентированного проектирования. Наши решения выражаются в терминах объектов и интерфейсов, а не стен и дверей, но в обоих случаях смысл паттерна — предложить решение определенной задачи в конкретном контексте.
В общем случае паттерн состоит из четырех основных элементов:
1.Имя. Указывая имя, мы сразу описываем проблему проектирования, ее решения и их последствия — и все это в одном-двух словах. Присваивание паттернам имен расширяет наш «словарный запас» проектирования и позволяет проектировать на более высоком уровне абстракции. Наличие словаря паттернов позволяет обсуждать их с коллегами, в документации и даже с самим собой. Имена позволяют анализировать дизайн системы, обсуждать его достоинства и недостатки с другими. Нахождение хороших имен было одной из самых трудных задач при составлении каталога.
2.Задача. Описание того, когда следует применять паттерн. Описание объясняет задачу и ее контекст. Может описываться конкретная проблема проектирования, например способ представления алгоритмов в виде объектов. В нем могут быть отмечены структуры классов или объектов, типичные для негибкого дизайна. Также может включаться перечень условий, при выполнении которых имеет смысл применять данный паттерн.
3. Решение. Описание элементов дизайна, отношений между ними, их обязанностей и взаимодействий между ними. В решении не описывается конкретный дизайн или реализация, поскольку паттерн — это шаблон, применимый в самых разных ситуациях. Вместо этого дается абстрактное описание задачи проектирования и ее возможного решения с помощью некоего обобщенного сочетания элементов (в нашем случае классов и объектов).
4.Результаты — следствия применения паттерна, его вероятные плюсы и минусы. Хотя при описании проектных решений о последствиях часто не упоминают, знать о них необходимо, чтобы можно было выбрать между различными вариантами и оценить преимущества и недостатки данного паттерна. Нередко к результатам относится баланс затрат времени и памяти, а также речь может идти о выборе языка и подробностях реализации. Поскольку в объектно-ориентированном проектировании повторное использование зачастую является важным фактором, то к результатам следует относить и влияние на степень гибкости, расширяемости и переносимости системы. Перечисление всех последствий поможет вам понять и оценить их роль.
Вопрос о том, что считать паттерном, а что нет, зависит от точки зрения. То, что один воспринимает как паттерн, для другого просто примитивный строительный блок. В этой книге мы рассматриваем паттерны на определенном уровне абстракции. Паттерныпроектирования — это не то же самое, что связанные списки или хештаблицы, которые можно реализовать в виде класса и повторно использовать без каких бы то ни было модификаций. С другой стороны, это и не сложные предметно-ориентированные решения для целого приложения или подсистемы. В этой книге под паттернами проектирования понимается описание взаимодействия объектов и классов, адаптированных для решения общей задачи проектирования в конкретном контексте.
Паттерн проектирования именует, абстрагирует и идентифицирует ключевые аспекты структуры общего решения, которые и позволяют применить его для создания повторно используемого дизайна. Он выделяет участвующие классы и экземпляры, их роли и отношения, а также распределение обязанностей. При описании каждого паттерна внимание акцентируется на конкретной задаче объектно-ориентированного проектирования. В формулировке паттерна анализируется, когда следует применять паттерн, можно ли его использовать с учетом других проектных ограничений, каковы будут последствия применения метода. Поскольку любой проект в конечном итоге предстоит реализовывать, в состав паттерна включается пример кода на языке C++ (иногда на Smalltalk), иллюстрирующего реализацию.
Хотя в паттернах описываются объектно-ориентированные архитектуры, они основаны на практических решениях, реализованных на основных языках объектно-ориентированного программирования типа Smalltalk и C++, а не на процедурных (Pascal, C, Ada и т.п.) или более динамических объектно-ориентированных языках (CLOS, Dylan, Self). Мы выбрали Smalltalk и C++ из прагматических соображений, поскольку наш опыт повседневного программирования связан именно с этими языками, и они завоевывают все большую популярность.
Выбор языка программирования безусловно важен. В наших паттернах подразумевается использование возможностей Smalltalk и C++, и от этого зависит, что реализовать легко, а что — трудно. Если бы мы ориентировались на процедурные языки, то включили бы паттерны наследование, инкапсуляция и полиморфизм. Некоторые из наших паттернов напрямую поддерживаются менее распространенными языками. Так, в языке CLOS есть мультиметоды, которые делают ненужным паттерн посетитель (с. 342). Собственно, даже между Smalltalk и C++ есть много различий, из-за чего некоторые паттерны проще выражаются на одном языке, чем на другом (см., например, паттерн итератор — с. 269).
1.2. Паттерны проектирования в схеме MVC в языке Smalltalk
В Smalltalk80 для построения интерфейсов пользователя применяется тройка классов модель/представление/контроллер (Model/View/Controller — MVC) [KP88]. Знакомство с паттернами проектирования, встречающимися в схеме MVC, поможет вам разобраться в том, что мы понимаем под словом «паттерн».
MVC состоит из объектов трех видов. Модель — это объект приложения, а представление — его внешний вид на экране. Контроллер описывает, как интерфейс реагирует на управляющие воздействия пользователя. До появления схемы MVC эти объекты в пользовательских интерфейсах смешивались. MVC отделяет их друг от друга, за счет чего повышается гибкость и улучшаются возможности повторного использования.
MVC отделяет представление от модели, устанавливая между ними протокол взаимодействия «подписка/уведомление». Представление должно гарантировать, что внешнее представление отражает состояние модели. При каждом изменении внутренних данных модель уведомляет все зависящие от нее представления, в результате чего представление обновляет себя. Такой подход позволяет присоединить к одной модели несколько представлений, обеспечив тем самым различные представления. Можно создать новое представление, не переписывая модель.
На следующей схеме показана одна модель и три представления. (Для простоты мы опустили контроллеры.) Модель содержит некоторые данные, которые могут быть представлены в форме электронной таблицы, гистограммы и круговой диаграммы. Модель сообщает своим представлениям обо всех изменениях значений данных, а представления взаимодействуют с моделью для получения новых значений.
На первый взгляд, в этом примере продемонстрирован просто дизайн, отделяющий представление от модели. Но тот же принцип применим и к более общей задаче: разделение объектов таким образом, что изменение одного отражается сразу на нескольких других, причем изменившийся объект не имеет информации о подробностях реализации других объектов. Этот более общий подход описывается паттерном проектирования наблюдатель.
Еще одна особенность MVC заключается в том, что представления могут быть вложенными. Например, панель управления, состоящую из кнопок, допустимо представить как составное представление, содержащее вложенные — по одной кнопке на каждое. Пользовательский интерфейс инспектора объектов может состоять из вложенных представлений, используемых также и в отладчике. MVC поддерживает вложенные представления с помощью класса CompositeView, являющегося подклассом View. Объекты класса CompositeView ведут себя так же, как объекты класса View, поэтому могут использоваться всюду, где и представления. Но еще они могут содержать вложенные представления и управлять ими.
Здесь можно было бы считать, что этот дизайн позволяет обращаться с составным представлением, как с любым из его компонентов. Но тот же дизайн применим и в ситуации, когда мы хотим иметь возможность группировать объекты и рассматривать группу как отдельный объект. Такой подход описывается паттерном компоновщик. Он позволяет создавать иерархию классов, в которой некоторые подклассы определяют примитивные объекты (например, Button — кнопка), а другие — составные объекты (CompositeView), группирующие примитивы в более сложные структуры.
MVC позволяет также изменять реакцию представления на действия пользователя. При этом визуальное воплощение остается прежним. Например, можно изменить реакцию на нажатие клавиши или использовать открывающиеся меню вместо командных клавиш. MVC инкапсулирует механизм определения реакции в объекте Controller. Существует иерархия классов контроллеров, и это позволяет без труда создать новый контроллер как вариант уже существующего.
Представление пользуется экземпляром класса, производного от Controller, для реализации конкретной стратегии реагирования. Чтобы реализовать иную стратегию, нужно просто подставить другой контроллер. Можно даже заменить контроллер представления во время выполнения программы, изменив тем самым реакцию на действия пользователя. Например, представление можно деактивировать, так что он вообще не будет ни на что реагировать, если передать ему контроллер, игнорирующий события ввода.
Отношение представление/контроллер — это пример паттерна проектирования стратегия (с. 327). Стратегия — это объект, представляющий алгоритм. Он будет полезен, когда вы хотите статически или динамически подменить один алгоритм другим, если существует много разновидностей одного алгоритма или когда с алгоритмом связаны сложные структуры данных, которые хотелось бы инкапсулировать.
В MVC используются и другие паттерны проектирования, например фабричный метод (c. 117), позволяющий задать для представления класс контроллера по умолчанию, и декоратор (с. 185) для добавления к представлению возможности прокрутки. Тем не менее, основные отношения в схеме MVC описываются паттернами наблюдатель, компоновщик и стратегия.
1.3. Описание паттернов проектирования
Как мы будем описывать паттерны проектирования? Графические обозначения важны, но их одних недостаточно. Они просто символизируют конечный продукт процесса проектирования в виде отношений между классами и объектами. Чтобы повторно воспользоваться дизайном, нам необходимо документировать решения, альтернативные варианты и компромиссы, которые привели к нему. Важны также конкретные примеры, поскольку они демонстрируют практическое применение паттерна.
При описании паттернов проектирования мы будем придерживаться единого формата. Описание каждого паттерна разбито на разделы, перечисленные ниже. Такой подход позволяет единообразно представить информацию, облегчает изучение, сравнение и применение паттернов.
Название и классификация паттерна
Название паттерна должно быть компактным и четко отражающим его назначение. Выбор названия чрезвычайно важен, потому что оно станет частью вашего словаря проектирования. Классификация паттернов проводится в соответствии со схемой, которая изложена в разделе 1.5.
Назначение
Краткие ответы на следующие вопросы: что делает паттерн? Почему и для чего он был создан? Какую конкретную задачу проектирования можно решить с его помощью?
Другие названия
Другие распространенные названия паттерна, если таковые имеются.
Мотивация
Сценарий, иллюстрирующий задачу проектирования и то, как она решается данной структурой класса или объекта. Благодаря мотивации можно лучше понять последующее, более абстрактное описание паттерна.
Применимость
Описание ситуаций, в которых можно применять данный паттерн. Примеры неудачного проектирования, которые можно улучшить с его помощью. Распознавание таких ситуаций.
Структура
Графическое представление классов в паттерне с использованием нотации, основанной на методике Object Modeling Technique (OMT) [RBP+91]. Мы пользуемся также диаграммами взаимодействий [JCJO92, Boo94] для иллюстрации последовательностей запросов и отношений между объектами. В приложении Б эта нотация описывается подробно.
Участники
Классы или объекты, задействованные в данном паттерне проектирования, и их обязанности.
Отношения
Взаимодействие участников для выполнения своих обязанностей.
Результаты
Насколько паттерн удовлетворяет поставленным требованиям? К каким результатам приводит паттерн, на какие компромиссы приходится идти? Какие аспекты структуры системы можно независимо изменять при использовании данного паттерна?
Реализация
О каких сложностях и подводных камнях следует помнить при реализации паттерна? Существуют ли какие-либо советы и рекомендуемые приемы? Есть ли у данного паттерна зависимость от языка программирования?
Пример кода
Фрагменты кода, демонстрирующие возможную реализацию на языках C++ или Smalltalk.
Известные применения
Возможности применения паттерна в реальных системах. Приводятся по меньшей мере два примера из различных областей.
Родственные паттерны
Какие паттерны проектирования тесно связаны с данным? Какие важные различия существуют между ними? С какими другими паттернами хорошо сочетается данный паттерн?
В приложениях приводится общая информация, которая поможет вам лучше понять паттерны и связанные с ними вопросы. Приложение A содержит глоссарий употребляемых нами терминов. В уже упомянутом приложении Б дано описание разнообразных нотаций. Некоторые аспекты применяемой нотации мы поясняем по мере ее появления в тексте книги. Наконец, в приложении В приведен исходный код фундаментальных классов, встречающихся в примерах.
1.4. Каталог паттернов проектирования
Каталог, начинающийся на с. 92, содержит 23 паттерна. Ниже для удобства перечислены их имена и назначение. В скобках после названия каждого паттерна указан номер страницы, откуда начинается его подробное описание.
Abstract Factory (абстрактная фабрика) (97)
Предоставляет интерфейс для создания семейств связанных между собой или зависимых объектов без указания их конкретных классов.
Adapter (адаптер) (150)
Преобразует интерфейс класса в другой интерфейс, ожидаемый клиентами. Обеспечивает совместную работу классов, которая была бы невозможна без данного паттерна из-за несовместимости интерфейсов.
Bridge (мост) (162)
Отделяет абстракцию от реализации, чтобы их можно было изменять независимо друг от друга.
Builder (строитель) (107)
Отделяет конструирование сложного объекта от его представления, чтобы один процесс конструирования мог использоваться для создания различных представлений.
Chain of Responsibility (цепочка обязанностей) (234)
Можно избежать формирования жесткой связи между отправителем запроса и его получателем, для чего возможность обработки запроса предоставляется нескольким объектам. Объекты-получатели объединяются в цепочку, и запрос передается по цепочке, пока не будет обработан каким-либо объектом.
Command (команда) (245)
Инкапсулирует запрос в виде объекта, позволяя тем самым параметризовывать клиентов по типу запроса, ставить запросы в очередь, протоколировать их и поддерживать отмену выполнения операций.
Composite (компоновщик) (173)
Группирует объекты в древовидные структуры для представления иерархий типа «часть — целое». Позволяет клиентам работать с единичными объектами так же, как с группами объектов.
Decorator (декоратор) (185)
Динамически наделяет объект новыми обязанностями. Декораторы применяются для расширения существующей функциональности и являются гибкой альтернативой порождению подклассов.
Facade (фасад) (196)
Предоставляет унифицированный интерфейс к набору интерфейсов в некоторой подсистеме. Определяет интерфейс более высокого уровня, облегчающий работу с подсистемой.
Factory Method (фабричный метод) (117)
Определяет интерфейс для создания объектов, позволяя подклассам решить, экземпляр какого класса следует создать. Позволяет классу передать ответственность за создание экземпляра в подклассы.
Flyweight (приспособленец) (205)
Применяет механизм совместного использования для эффективной поддержки большого числа мелких объектов.
Interpreter (интерпретатор) (256)
Для заданного языка определяет представление его грамматики вместе с интерпретатором, который использует представление для интерпретации предложений языка.
Iterator (итератор) (269)
Дает возможность последовательно обойти все элементы составного объекта, не раскрывая его внутреннего представления.
Mediator (посредник) (285)
Определяет объект, в котором инкапсулирована информация о взаимодействии объектов из некоторого множества. Способствует ослаблению связей между объектами, позволяя им работать без явных ссылок друг на друга. Это, в свою очередь, дает возможность независимо изменять схему взаимодействия.
Memento (хранитель) (295)
Позволяет без нарушения инкапсуляции получать и сохранять во внешней памяти внутреннее состояние объекта, чтобы позже объект можно было восстановить в точно таком же состоянии.
Observer (наблюдатель) (305)
Определяет между объектами зависимость типа «один-ко-многим», так что при изменении состояния одного объекта все зависящие от него получают уведомление и автоматически обновляются.
Prototype (прототип) (127)
Описывает виды создаваемых объектов с помощью прототипа и создает новые объекты путем его копирования.
Proxy (заместитель) (218)
Подменяет другой объект для контроля доступа к нему.
Singleton (одиночка) (138)
Гарантирует, что некоторый класс может существовать только в одном экземпляре, и предоставляет глобальную точку доступа к нему.
State (состояние) (317)
Позволяет объекту изменять свое поведение при модификации внутреннего состояния. При этом все выглядит так, словно поменялся класс объекта.
Strategy (стратегия) (327)
Определяет семейство алгоритмов, инкапсулируя их все и позволяя подставлять один вместо другого. Позволяет менять алгоритм независимо от клиента, который им пользуется.
Template Method (шаблонный метод) (337)
Определяет скелет алгоритма, перекладывая ответственность за некоторые его шаги на подклассы. Позволяет подклассам переопределять отдельные шаги алгоритма, не меняя его общей структуры.
Visitor (посетитель) (342)
Представляет операцию, которую надо выполнить над элементами объектной структуры. Позволяет определить новую операцию без изменения классов элементов, к которым он применяется.
1.5. Организация каталога
Паттерны проектирования различаются степенью детализации и уровнем абстракции. Паттернов проектирования довольно много, поэтому их нужно как-то организовать. В данном разделе описывается классификация, позволяющая ссылаться на семейства взаимосвязанных паттернов. Она поможет быстрее освоить паттерны, описанные в каталоге, а также укажет направление поиска новых.
Мы будем классифицировать паттерны по двум критериям (табл. 1.1). Первый — цель — отражает назначение паттерна. Паттерны делятся на порождающие, структурные и паттерны поведения. Первые связаны с процессом создания объектов. Вторые имеют отношение к композиции объектов и классов. Паттерны поведения характеризуют то, как классы или объекты взаимодействуют.
Таблица 1.1. Пространство паттернов проектирования
| Цель Уровень |
Порождающие паттерны |
Структурные паттерны |
Паттерны поведения |
| Класс |
Фабричный метод (117) |
Адаптер (150) |
Интерпретатор (256) |
| Шаблонный метод (337) |
|||
| Объект |
Абстрактная фабрика (97) |
Адаптер (150) |
Итератор (269) |
| Одиночка (138) |
Декоратор (185) |
Команда (245) |
|
| Прототип (127) |
Заместитель (218) |
Наблюдатель (305) |
|
| Строитель (107) |
Компоновщик (173) |
Посетитель (342) |
|
| Мост (162) |
Посредник (285) |
||
| Приспособленец (205) |
Состояние (317) |
||
| Фасад (196) |
Стратегия (327) |
||
| Хранитель (295) |
|||
| Цепочка обязанностей (234) |
Второй критерий — уровень — сообщает, к чему обычно применяется паттерн: к объектам или классам. Паттерны уровня классов описывают отношения между классами и их подклассами. Такие отношения выражаются с помощью наследования, поэтому они статичны, то есть зафиксированы на этапе компиляции. Паттерны уровня объектов описывают отношения между объектами, которые могут изменяться во время выполнения и потому более динамичны. Почти все паттерны в какой-то мере используют наследование. Поэтому к категории «паттерны классов» отнесены только те, что концентрируются лишь на отношениях между классами. Обратите внимание: большинство паттернов действует на уровне объектов.
Порождающие паттерны классов частично делегируют ответственность за создание объектов своим подклассам, тогда как порождающие паттерны объектов передают ответственность другому объекту. Структурные паттерны классов используют наследование для составления классов, в то время как структурные паттерны объектов описывают способы сборки объектов из частей. Поведенческие паттерны классов используют наследование для описания алгоритмов и потока управления, а поведенческие паттерны объектов описывают, как объекты, принадлежащие некоторой группе, совместными усилиями выполняют задачу, которая ни одному отдельному объекту не под силу.
Существуют и другие способы классификации паттернов. Некоторые паттерны часто используются вместе. Например, компоновщик применяется с итератором или посетителем. Некоторыми паттернами предлагаются альтернативные решения. Так, прототип нередко можно использовать вместо абстрактной фабрики. Применение части паттернов приводит к схожему дизайну, хотя изначально их назначение различно. Например, структурные диаграммы компоновщика и декоратора похожи.
Классифицировать паттерны можно и по их ссылкам (см. разделы «Родственные паттерны»). На рис. 1.1 такие отношения изображены графически.
Ясно, что организовать паттерны проектирования допустимо многими способами. Оценивая паттерны с разных точек зрения, вы глубже поймете, как они функционируют, как их сравнивать и когда применять тот или другой паттерн.
1.6. Как решать задачи проектирования с помощью паттернов
Паттерны проектирования позволяют решать многие повседневные задачи, с которыми сталкиваются проектировщики объектно-ориентированных приложений. Поясним эту мысль примерами.
Поиск подходящих объектов
Объектно-ориентированные программы состоят из объектов. Объект сочетает данные и процедуры для их обработки. Такие процедуры обычно называют методами или операциями. Объект выполняет операцию, когда получает запрос (или сообщение) от клиента.
Отправка запроса — это единственный способ заставить объект выполнить операцию. А выполнение операции — единственный способ изменить внутреннее состояние объекта. Из-за этих двух ограничений говорят, что внутреннее состояние объекта инкапсулировано: к нему нельзя обратиться напрямую, а его представление невидимо за пределами объекта.
Рис. 1.1. Отношения между паттернами проектирования
Самая трудная задача в объектно-ориентированном проектировании — разложить систему на объекты. При решении приходится учитывать множество факторов: инкапсуляцию, глубину детализации, наличие зависимостей, гибкость, производительность, возможную эволюцию, повторное использование и т.д. и т.п. Все это влияет на декомпозицию, причем часто противоречивым образом.
Методологии объектно-ориентированного проектирования отражают разные подходы. Можно сформулировать задачу письменно, выделить из получившейся фразы существительные и глаголы, после чего создать соответствующие классы и операции. Другой путь — сосредоточиться на отношениях и разделении обязанностей в системе. Можно построить модель реального мира или перенести выявленные при анализе объекты в свой дизайн. Разработчики никогда не придут к единому мнению относительно того, какой подход самый лучший.
Многие объекты возникают в проекте из построенной в ходе анализа модели. Однако нередко появляются и классы, не имеющие аналогов в реальном мире. Это могут быть классы как низкого уровня, например массивы, так и высокого. Паттерн компоновщик (173) вводит такую абстракцию для единообразной трактовки объектов, у которой нет физического аналога. Если придерживаться строгого моделирования и ориентироваться только на реальный мир, то получится система, отражающая сегодняшние потребности, но, возможно, не учитывающая будущего развития. Абстракции, возникающие в ходе проектирования, — ключ к гибкому дизайну.
Паттерны проектирования помогают выявить не вполне очевидные абстракции и объекты, которые могут их использовать. Например, объектов, представляющих процесс или алгоритм, в действительности нет, но они являются неотъемлемыми составляющими гибкого дизайна. Паттерн стратегия (327) описывает способ реализации взаимозаменяемых семейств алгоритмов. Паттерн состояние (317) представляет состояние некоторой сущности в виде объекта. Эти объекты редко возникают во время анализа и даже на ранних стадиях проектирования. Они появляются позднее, при попытках сделать дизайн более гибким и пригодным для повторного использования.
Определение степени детализации объекта
Размеры и число объектов могут изменяться в широком диапазоне. С помощью объектов можно представить все, от физических устройств до программ. Как же решить, что должен представлять объект?
Паттерны проектирования помогут решить и эту проблему. Паттерн фасад (196) показывает, как представить в виде объекта целые подсистемы, а паттерн приспособленец (205) — как поддерживать большое число объектов при высокой степени детализации. Другие паттерны указывают путь к разложению объекта на меньшие подобъекты. Абстрактная фабрика (97) и строитель (107) описывают объекты, единственной целью которых является создание других объектов, а посетитель (342) и команда (245) — объекты, отвечающие за реализацию запроса к другому объекту или группе.
Определение интерфейсов объекта
Для любой операции, объявляемой объектом, должны быть заданы: имя операции, объекты, передаваемые в качестве параметров, и значение, возвращаемое операцией. Эту триаду называют сигнатурой операции. Множество сигнатур всех определенных для объекта операций называется интерфейсом этого объекта. Интерфейс описывает все множество запросов, которые можно отправить объекту. Любой запрос, сигнатура которого входит в интерфейс объекта, может быть ему отправлен.
Тип представляет собой имя, используемое для обозначения конкретного интерфейса. Говорят, что объект имеет тип Window, если он готов принимать запросы на выполнение любых операций, определенных в интерфейсе с именем Window. У одного объекта может быть много типов. Напротив, сильно отличающиеся объекты могут разделять общий тип. Одна часть интерфейса объекта может характеризоваться одним типом, а другие части — другими типами. Два объекта одного типа могут разделять только часть своих интерфейсов. Интерфейсы могут содержать другие интерфейсы в качестве подмножеств. Мы говорим, что один тип является подтипом другого, если интерфейс первого содержит интерфейс второго. В этом случае второй тип называется супертипом для первого. Часто говорят также, что подтип наследует интерфейс своего супертипа.
В объектно-ориентированных системах интерфейсы играют фундаментальную роль. Все взаимодействие с объектами осуществляется через их интерфейсы. Никакого способа получить информацию об объекте или заставить его что-то сделать в обход интерфейса не существует. Интерфейс объекта ничего не говорит о его реализации; разные объекты вправе реализовывать сходные запросы совершенно по-разному. Это означает, что два объекта с различными реализациями могут иметь одинаковые интерфейсы.
Когда объекту посылается запрос, то операция, которую он выполнит, зависит как от запроса, так и от объекта-адресата. Разные объекты, поддерживающие одинаковые интерфейсы, могут выполнять в ответ на такие запросы разные операции. Ассоциирование запроса с объектом и одной из его операций во время выполнения называется динамическим связыванием.
Динамическое связывание означает, что отправка некоторого запроса не определяет никакой конкретной реализации до момента выполнения. Следовательно, возможно написать программу, рассчитанную на объект с конкретным интерфейсом, точно зная, что любой объект с подходящим интерфейсом сможет принять этот запрос. Более того, динамическое связывание позволяет во время выполнения подставить вместо одного объекта другой, если он имеет идентичный интерфейс. Такая взаимозаменяемость называется полиморфизмом и является важнейшей особенностью объектно-ориентированных систем. Она позволяет клиенту ограничиваться минимальными предположениями об объектах — а именно поддержкой этими объектами определенного интерфейса. Полиморфизм упрощает определение клиентов, позволяет отделить объекты друг от друга и дает объектам возможность изменять отношения между ними во время выполнения.
Паттерны проектирования позволяют определять интерфейсы посредством задания их основных элементов и того, какие данные можно передавать через интерфейс. Паттерн может также сообщить, что не должно включаться в интерфейс. Хорошим примером в этом отношении является хранитель (295). Он описывает, как инкапсулировать и сохранить внутреннее состояние объекта таким образом, чтобы в будущем его можно было восстановить точно в таком же состоянии. Объекты, удовлетворяющие требованиям паттерна хранитель, должны определить два интерфейса: один ограниченный, который позволяет клиентам держать у себя и копировать хранителей, а другой привилегированный, которым может пользоваться только исходный объект для сохранения и извлечения информации о своем состоянии в хранителе.
Паттерны проектирования также определяют отношения между интерфейсами. В частности, нередко они требуют, чтобы некоторые классы имели схожие интерфейсы, а иногда налагают ограничения на интерфейсы классов. Так, декоратор (185) и заместитель (218) требуют, чтобы интерфейсы объектов этих паттернов были идентичны интерфейсам декорируемых и замещаемых объектов соответственно. Интерфейс объекта, использующего паттерн посетитель (342), должен отражать все классы объектов, с которыми он будет работать.
Определение реализации объектов
До сих пор мы почти ничего не сказали о том, как же в действительности определяется объект. Реализация объекта определяется его классом. Класс определяет внутренние данные объекта и его представление, а также операции, которые объект может выполнять.
В нашей нотации, основанной на OMT (см. приложение Б), класс изображается прямоугольником, внутри которого жирным шрифтом написано имя класса. Ниже обычным шрифтом перечислены операции. Все данные, определенные для класса, следуют после операций. Имя класса, операции и данные разделяются горизонтальными линиями:
Типы возвращаемого значения и переменных экземпляра необязательны, поскольку мы не ограничиваем себя языками программирования с сильной типизацией.
Объекты создаются посредством создания экземпляров класса. Говорят, что объект является экземпляром класса. В процессе создания экземпляров выделяется память для внутренних данных объекта (переменных экземпляра), а с этими данными связываются операции. Создавая экземпляры одного класса, можно создать много сходных объектов.
Пунктирная линия со стрелкой обозначает класс, который создает объекты другого класса. Стрелка направлена в сторону класса создаваемых объектов.
Новые классы могут определяться в контексте существующих с использованием наследованияклассов. Если подкласс наследует родительскомуклассу, то он включает определения всех данных и операций, определенных в родительском классе. Объекты, являющиеся экземплярами подкласса, будут содержать все данные, определенные как в самом подклассе, так и во всех его родительских классах. Такой объект сможет выполнять все операции, определенные в этом подклассе и его предках. Отношение «является подклассом» обозначается вертикальной линией с треугольником.
Класс называется абстрактным, если его единственное назначение — определить общий интерфейс для всех своих подклассов. Абстрактный класс делегирует реализацию всех или части своих операций подклассам, поэтому у него не может быть экземпляров. Операции, объявленные, но не реализованные в абстрактном классе, называются абстрактными. Класс, не являющийся абстрактным, называется конкретным.
Подклассы могут уточнять или переопределять поведение своих предков. Точнее, класс может заместить операцию, определенную в родительском классе. Замещение дает подклассам возможность обрабатывать запросы, адресованные родительским классам. Наследование позволяет определять новые классы, просто расширяя возможности старых. Таким образом можно без труда определять семейства объектов, обладающих сходной функциональностью.
Имена абстрактных классов записываются курсивом, чтобы отличать их от конкретных. Курсив используется также для обозначения абстрактных операций. На диаграмме может изображаться псевдокод, описывающий реализацию операции; в таком случае код представлен в прямоугольнике с загнутым уголком, соединенном пунктирной линией с операцией, которую он реализует.
Примесью (mixin class) называется класс, назначение которого — предоставить дополнительный интерфейс или функциональность другим классам. Он отчасти похож на абстрактные классы в том смысле, что не предполагает непосредственного создания экземпляров. Для работы с примесями необходимо множественное наследование:
Наследование класса и наследование интерфейса
Важно понимать различие между классом объекта и его типом.
Класс объекта определяет реализацию объекта, то есть внутреннее состояние и реализацию операций объекта. Напротив, тип относится только к интерфейсу объекта — множеству запросов, на которые объект способен ответить. У объекта может быть много типов, и объекты разных классов могут иметь один и тот же тип.
Разумеется, между классом и типом существует тесная связь. Поскольку класс определяет операции, которые может выполнять объект, то он также определяет и его тип. Когда мы говорим «объект является экземпляром класса», то подразумеваем, что он поддерживает интерфейс, определяемый этим классом.
В языках вроде C++ и Eiffel классы определяют как тип объекта, так и его реализацию. В программах на языке Smalltalk типы переменных не объявляются, поэтому компилятор не проверяет, что тип объекта, присваиваемого переменной, является подтипом типа переменной. При отправке сообщения необходимо проверять, что класс получателя реализует реакцию на сообщение, но проверка того, что получатель является экземпляром определенного класса, не нужна.
Важно также понимать различие между наследованием класса и наследованием интерфейса (или порождением подтипов). В случае наследования класса реализация объекта определяется в терминах реализации другого объекта. Проще говоря, это механизм разделения кода и представления. Напротив, наследование интерфейса (порождение подтипов) описывает, когда один объект можно использовать вместо другого.
Эти две концепции легко спутать, поскольку во многих языках явное различие отсутствует. В таких языках, как C++ и Eiffel, под наследованием понимается одновременно наследование интерфейса и реализации. Стандартный способ реализации наследования интерфейса в C++ — это открытое наследование классу, в котором есть (чисто) виртуальные функции. Чистое наследование интерфейса можно смоделировать в C++ посредством открытого наследования чисто абстрактному классу. Чистая реализация или наследование классов может моделироваться посредством закрытого наследования. В Smalltalk под наследованием понимается только наследование реализации. Переменной можно присвоить экземпляры любого класса при условии, что они поддерживают операции, выполняемые над значением этой переменной.
Хотя в большинстве языков программирования различие между наследованием интерфейса и реализации не поддерживается, на практике оно существует. Программисты на Smalltalk обычно считают, что подклассы — это подтипы (хотя имеются и хорошо известные исключения [Coo92]). Программисты на C++ манипулируют объектами через типы, определяемые абстрактными классами.
Многие паттерны проектирования зависят от этого различия. Например, объекты, построенные в соответствии с паттерном цепочка обязанностей (234), должны иметь общий тип, но обычно они не используют общую реализацию. В паттерне компоновщик (173) отдельный объект (компонент) определяет общий интерфейс, но реализацию часто определяет составной объект (композиция). Паттерны команда (245), наблюдатель (305), состояние (317) и стратегия (327) часто реализуются абстрактными классами, которые представляют чистые интерфейсы.
Программирование в соответствии с интерфейсом, а не с реализацией
Наследование классов — это не что иное, как механизм расширения функциональности приложения путем повторного использования функциональности родительских классов. Оно позволяет быстро определить новый вид объектов через уже имеющийся. Новую реализацию можно получить почти без всякого труда посредством наследования большей части необходимого кода из существующих классов.
Впрочем, повторное использование реализации — лишь полдела. Не менее важно, что наследование позволяет определять семейства объектов с идентичными интерфейсами (обычно за счет наследования от абстрактных классов). Почему? Потому что от этого зависит полиморфизм.
Если пользоваться наследованием осторожно (некоторые сказали бы правильно), то все классы, производные от некоторого абстрактного класса, будут обладать его интерфейсом. Отсюда следует, что подкласс добавляет новые или замещает старые операции и не скрывает операции, определенные в родительском классе. Все подклассы могут отвечать на запросы, соответствующие интерфейсу абстрактного класса, поэтому они являются подтипами этого абстрактного класса.
У манипулирования объектами строго через интерфейс абстрактного класса есть два преимущества:
• клиенту не нужно располагать информацией о конкретных типах объектов, которыми он пользуется, при условии что все они имеют ожидаемый клиентом интерфейс;
• клиенту необязательно «знать» о классах, с помощью которых реализованы объекты. Клиент
