Э. Гамма, Р. Хелм
Скачать 6.37 Mb.
|
ГЛАВА 3 ПОРОЖДАЮЩИЕ ПАТТЕРНЫ Порождающие паттерны проектирования абстрагируют процесс создания экземпляров. Они позволяют сделать систему независимой от способа созда- ния, композиции и представления объектов. Паттерн, порождающий классы, использует наследование, чтобы варьировать класс создаваемого экземпляра, а паттерн, порождающий объекты, делегирует создание экземпляров другому объекту. Эти паттерны начинают играть более важную роль, когда система эволюционирует и начинает в большей степени зависеть от композиции объ- ектов, чем от наследования классов. При этом основной акцент смещается с жесткого кодирования фиксированного набора поведений на определение небольшого набора фундаментальных поведений, посредством композиции которых можно получить любое число более сложных. Таким образом, для создания объектов с конкретным поведением требуется нечто большее, чем простое создание экземпляра класса. Для порождающих паттернов характерны два аспекта. Во-первых, эти пат- терны инкапсулируют знания о конкретных классах, которые применяются в системе. Во-вторых, они скрывают подробности создания и компоновки экземпляров этих классов. Единственная информация об объектах, извест- ная системе, — это их интерфейсы, определенные с помощью абстрактных классов. Следовательно, порождающие паттерны обеспечивают большую гибкость в отношении того, что создается, кто это создает, как и когда. Это позволяет настроить систему «готовыми» объектами с самой различной структурой и функциональностью статически (на этапе компиляции) или динамически (во время выполнения). В некоторых ситуациях возможен выбор между тем или иным порождаю- щим паттерном. Например, есть случаи, когда с пользой для дела можно Порождающие паттерны 109 использовать как прототип (146), так и абстрактную фабрику (113). В других ситуациях порождающие паттерны дополняют друг друга. Так, применяя паттерн строитель (124), можно использовать другие паттерны для решения вопроса о том, какие компоненты нужно строить, а прототип (146) может использовать одиночку (157) в своей реализации. Поскольку порождающие паттерны тесно связаны друг с другом, мы изучим сразу все пять, чтобы лучше были видны их сходства и различия. Изучение будет вестись на общем примере — построении лабиринта для компьютерной игры. Правда, и лабиринт, и игра будут слегка варьироваться для разных пат- тернов. Иногда целью игры станет просто отыскание выхода из лабиринта; тогда игроку будет доступно только локальное представление лабиринта. В других случаях в лабиринтах могут встречаться задачи, которые игрок должен решить, и опасности, которые ему предстоит преодолеть. В подоб- ных играх может отображаться карта того участка лабиринта, который уже был исследован. Мы опустим многие детали того, что может встречаться в лабиринте, и пред- назначен ли лабиринт для одного или нескольких игроков, а сосредоточимся лишь на принципах создания лабиринта. Лабиринт будет определяться как множество комнат. Любая комната «знает» о своих соседях, в качестве ко- торых могут выступать другая комната, стена или дверь в другую комнату. Классы Room (комната), Door (дверь) и Wall (стена) определяют компоненты лабиринта и используются во всех наших примерах. Мы определим только те части этих классов, которые важны для создания лабиринта. Не будем рассматривать игроков, операции отображения и блуждания в лабиринте и другие важные функции, не имеющие отношения к построению лабиринта. На схеме ниже показаны отношения между классами Room , Door и Wall MapSite Enter() Room roomNumber Enter() SetSide() GetSide() Maze AddRoom() RoomNo() Wall Door Enter() Enter() isOpen Стороны Комнаты 110 Глава 3. Порождающие паттерны У каждой комнаты есть четыре стороны. Для задания северной, южной, восточной и западной сторон будем использовать перечисление Direction в реализации на языке C++: enum Direction {North, South, East, West}; В программах на языке Smalltalk для представления направлений будут использоваться соответствующие символические имена. Класс MapSite — общий абстрактный класс для всех компонентов лабиринта. Для упрощения примера в нем определяется только одна операция Enter , смысл которой зависит от того, куда именно вы входите. Когда вы входите в комнату, ваше местоположение изменяется. При попытке затем войти в дверь может произойти одно из двух. Если дверь открыта, то вы попадаете в следующую комнату, а если закрыта, то вы разбиваете себе нос: class MapSite { public: virtual void Enter() = 0; }; Операция Enter составляет основу для более сложных игровых операций. Например, если вы находитесь в комнате и говорите «Иду на восток», то игрой определяется, какой объект класса MapSite находится к востоку от вас, и для него вызывается операция Enter . Определенные в подклассах операции Enter «выяснят», изменили вы свое местоположение или рас- шибли нос. В реальной игре Enter мог бы передаваться аргумент с объектом, представляющим блуждающего игрока. Room — это конкретный подкласс класса MapSite , который определяет клю- чевые отношения между компонентами лабиринта. Он содержит ссылки на другие объекты MapSite , а также хранит номер комнаты. Все комнаты в лабиринте идентифицируются номерами: class Room : public MapSite { public: Room(int roomNo); MapSite* GetSide(Direction) const; void SetSide(Direction, MapSite*); virtual void Enter(); private: MapSite* _sides[4]; int _roomNumber; }; Порождающие паттерны 111 Следующие классы представляют стены и двери, находящиеся с каждой стороны комнаты: class Wall : public MapSite { public: Wall(); virtual void Enter(); }; class Door : public MapSite { public: Door(Room* = 0, Room* = 0); virtual void Enter(); Room* OtherSideFrom(Room*); private: Room* _room1; Room* _room2; bool _isOpen; }; Тем не менее, информации об отдельных частях лабиринта недостаточно. Определим еще класс Maze для представления набора комнат. В этот класс включена операция RoomNo для нахождения комнаты по ее номеру: class Maze { public: Maze(); void AddRoom(Room*); Room* RoomNo(int) const; private: // ... }; RoomNo могла бы выполнять поиск с помощью линейного списка, хеш-таблицы или даже простого массива. Но пока нас не интересуют такие подробности. Займемся тем, как описать компоненты объекта, представляющего лабиринт. Определим также класс MazeGame , который создает лабиринт. Самый про- стой способ сделать это — строить лабиринт последовательностью операций, добавляющих к нему компоненты, которые потом соединяются. Например, следующая функция создаст лабиринт из двух комнат с одной дверью между ними: 112 Глава 3. Порождающие паттерны Maze* MazeGame::CreateMaze () { Maze* aMaze = new Maze; Room* r1 = new Room(1); Room* r2 = new Room(2); Door* theDoor = new Door(r1, r2); aMaze->AddRoom(r1); aMaze->AddRoom(r2); r1->SetSide(North, new Wall); r1->SetSide(East, theDoor); r1->SetSide(South, new Wall); r1->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; } Функция получилась довольно сложной, если учесть, что она всего лишь строит лабиринт из двух комнат. Есть очевидные способы упростить ее. Например, конструктор класса Room мог бы инициализировать стороны без дверей заранее; но это означает лишь перемещение кода в другое место. Суть проблемы не в размере этой функции, а в ее негибкости. Структура лаби- ринта жестко «зашита» в функции. Чтобы изменить структуру, придется изменить саму функцию, либо заместив ее (то есть полностью переписав заново), либо непосредственно модифицируя ее фрагменты. Оба пути чре- ваты ошибками и не способствуют повторному использованию. Порождающие паттерны показывают, как сделать дизайн более гибким, хотя и необязательно меньшим по размеру. В частности, их применение позволит легко менять классы, определяющие компоненты лабиринта. Предположим, вы хотите использовать уже существующую структуру в новой игре с волшебными лабиринтами. В такой игре появляются не существовавшие ранее компоненты, например DoorNeedingSpell — запер- тая дверь, для открывания которой нужно произнести заклинание, или EnchantedRoom — комната, где есть необычные предметы, скажем, волшебные ключи или магические слова. Как легко изменить операцию CreateMaze , чтобы она создавала лабиринты с новыми классами объектов? В данном случае самое серьезное препятствие лежит в жестко зашитой информации о классах, экземпляры которых создаются в коде. С помощью Паттерн Abstract Factory (абстрактная фабрика) 113 порождающих паттернов можно различными способами избавиться от яв- ных упоминаний конкретных классов из кода, создающего их экземпляры: если CreateMaze вызывает виртуальные функции вместо конструкто- ров для создания комнат, стен и дверей, то классы, экземпляры которых создаются, можно подменить, создав подкласс MazeGame и переопреде- лив в нем виртуальные функции. Такой подход применяется в паттерне фабричный метод (135); когда функции CreateMaze в параметре передается объект, используемый для создания комнат, стен и дверей, то их классы можно изменить, пере- дав другой параметр. Это пример паттерна абстрактная фабрика (113); если функции CreateMaze передается объект, способный целиком соз- дать новый лабиринт с помощью своих операций для добавления комнат, дверей и стен, можно воспользоваться наследованием для из- менения частей лабиринта или способа его построения. Такой подход применяется в паттерне строитель (124); если CreateMaze параметризована прототипами комнаты, двери и стены, которые она затем копирует и добавляет к лабиринту, то состав лаби- ринта можно варьировать, заменяя одни объекты-прототипы другими. Это паттерн прототип (146). Последний из порождающих паттернов, одиночка (157), может гарантиро- вать существование единственного лабиринта в игре и свободный доступ к нему со стороны всех игровых объектов, не прибегая к глобальным пере- менным или функциям. Паттерн одиночка также позволяет легко расширить или заменить лабиринт, не трогая существующий код. ПАТТЕРН ABSTRACT FACTORY (АБСТРАКТНАЯ ФАБРИКА) Название и классификация паттерна Абстрактная фабрика — паттерн, порождающий объекты. Назначение Предоставляет интерфейс для создания семейств взаимосвязанных или взаимозависимых объектов, не специфицируя их конкретных классов. Другие названия Kit (инструментарий). 114 Глава 3. Порождающие паттерны Мотивация Client MotifWindow PMWindow Window MotifScrollBar PMScrollBar ScrollBar WidgetFactory CreateScrollBar() CreateWindow() MotifWidgetFactory CreateScrollBar() CreateWindow() PMWidgetFactory CreateScrollBar() CreateWindow() Рассмотрим инструментальную программу для создания пользовательского интерфейса, поддерживающего разные стандарты оформления, например Motif и Presentation Manager. Оформление определяет визуальное пред- ставление и поведение элементов пользовательского интерфейса («видже- тов») — полос прокрутки, окон и кнопок. Чтобы приложение можно было перенести на другой стандарт, в нем не должен быть жестко закодировано оформление виджетов. Если создание экземпляров классов для конкретного оформления разбросано по всему приложению, то изменить оформление впоследствии будет нелегко. Для решения этой проблемы можно определить абстрактный класс WidgetFactory , в котором объявлен интерфейс для создания всех основных видов виджетов. Есть также абстрактные классы для каждого отдельного вида и конкретные подклассы, реализующие виджеты с определенным оформле- нием. В интерфейсе WidgetFactory имеется операция, возвращающая новый объект-виджет для каждого абстрактного класса виджетов. Клиенты вызывают эти операции для получения экземпляров виджетов, но при этом ничего не знают о том, какие именно классы используются. Таким образом, клиенты остаются независимыми от выбранного стандарта оформления. Для каждого стандарта оформления существует определенный подкласс WidgetFactory . Каждый такой подкласс реализует операции, необходимые для создания соответствующего стандарту виджета. Например, операция CreateScrollBar в классе MotifWidgetFactory создает экземпляр и возвра- щает полосу прокрутки в стандарте Motif, тогда как соответствующая опе- Паттерн Abstract Factory (абстрактная фабрика) 115 рация в классе PMWidgetFactory возвращает полосу прокрутки в стандарте Presentation Manager. Клиенты создают виджеты, пользуясь исключительно интерфейсом WidgetFactory , и им ничего не известно о классах, реализу- ющих виджеты для конкретного стандарта. Другими словами, клиенты должны лишь придерживаться интерфейса, определенного абстрактным, а не конкретным классом. Класс WidgetFactory также устанавливает зависимости между конкретными классами виджетов. Полоса прокрутки для Motif должна использоваться с кнопкой и текстовым полем Motif, и это ограничение поддерживается автоматически как следствие использования класса MotifWidgetFactory Применимость Основные условия для применения паттерна абстрактная фабрика: система не должна зависеть от того, как создаются, компонуются и пред- ставляются входящие в нее объекты; система должна настраиваться одним из семейств объектов; входящие в семейство взаимосвязанные объекты спроектированы для со- вместной работы, и вы должны обеспечить выполнение этого ограничения; вы хотите предоставить библиотеку объектов, раскрывая только их ин- терфейсы, но не реализацию. Структура AbstractProductA'>Client ProductA1 ProductA2 AbstractProductA ProductB1 ProductB2 AbstractProductB AbstractFactory CreateProductA() CreateProductB() ConcreteFactory1 CreateProductA() CreateProductB() ConcreteFactory2 CreateProductA() CreateProductB() 116 Глава 3. Порождающие паттерны Участники AbstractFactory ( WidgetFactory ) — абстрактная фабрика: • объявляет интерфейс для операций, создающих абстрактные объ- екты-продукты; ConcreteFactory ( MotifWidgetFactory , PMWidgetFactory ) — конкретная фабрика: • реализует операции, создающие конкретные объекты-продукты; AbstractProduct ( Window , ScrollBar ) — абстрактный продукт: • объявляет интерфейс для типа объекта-продукта; ConcreteProduct ( MotifWindow , MotifScrollBar ) — конкретный продукт: • определяет объект-продукт, создаваемый соответствующей конкрет- ной фабрикой; • реализует интерфейс AbstractProduct ; Client — клиент: • пользуется исключительно интерфейсами, которые объявлены в клас- сах AbstractFactory и AbstractProduct Отношения Обычно во время выполнения создается единственный экземпляр клас- са ConcreteFactory . Эта конкретная фабрика создает объекты-продук- ты, имеющие вполне определенную реализацию. Для создания других видов объектов клиент должен воспользоваться другой конкретной фа- брикой; AbstractFactory передоверяет создание объектов-продуктов своему подклассу ConcreteFactory Результаты Паттерн абстрактная фабрика: изолирует конкретные классы. Паттерн помогает контролировать клас- сы объектов, создаваемых приложением. Поскольку фабрика инкапсу- лирует ответственность за создание классов и сам процесс их создания, то она изолирует клиента от подробностей реализации классов. Кли- енты манипулируют экземплярами через их абстрактные интерфейсы. Имена изготавливаемых классов известны только конкретной фабрике, в коде клиента они не упоминаются; Паттерн Abstract Factory (абстрактная фабрика) 117 упрощает замену семейств продуктов. Класс конкретной фабрики по- является в приложении только один раз: при создании экземпляра. Это облегчает замену используемой приложением конкретной фабрики. Приложение может изменить конфигурацию продуктов, просто подста- вив новую конкретную фабрику. Поскольку абстрактная фабрика создает все семейство продуктов, то и заменяется сразу все семейство. В нашем примере для переключения пользовательского интерфейса с виджетов Motif на виджеты Presentation Manager достаточно переключиться на продукты соответствующей фабрики и заново создать интерфейс; гарантирует сочетаемость продуктов. Если продукты некоторого се- мейства спроектированы для совместного использования, то важно, чтобы приложение в каждый момент времени работало только с про- дуктами единственного семейства. Класс AbstractFactory позволяет легко соблюсти это ограничение; не упрощает задачу поддержки нового вида продуктов. Расширение абстрактной фабрики для изготовления новых видов продуктов — не- простая задача. Дело в том, что интерфейс AbstractFactory фиксирует набор продуктов, которые можно создать. Для поддержки новых про- дуктов необходимо расширить интерфейс фабрики, то есть изменить класс AbstractFactory и все его подклассы. Одно из возможных реше- ний этой проблемы рассматривается в разделе «Реализация». Реализация Некоторые полезные приемы реализации паттерна абстрактная фабрика: фабрики как объекты, существующие в единственном экземпля- ре. Как правило, приложению нужен только один экземпляр класса ConcreteFactory на каждое семейство продуктов. Поэтому для реализа- ции лучше всего применить паттерн одиночка (157); создание продуктов. Класс AbstractFactory объявляет только интер- фейс для создания продуктов. Фактическое их создание — дело подклас- сов ConcreteProduct . Чаще всего для этой цели определяется фабрич- ный метод для каждого продукта (см. паттерн фабричный метод (135)). Конкретная фабрика определяет свои продукты путем замещения фа- бричного метода для каждого из них. Хотя такая реализация проста, она требует создавать новый подкласс конкретной фабрики для каждого се- мейства продуктов, даже если они почти ничем не отличаются. Если семейств продуктов может быть много, то конкретную фабрику удастся реализовать с помощью паттерна прототип (146). В этом случае |