Э. Гамма, Р. Хелм
Скачать 6.37 Mb.
|
Tool Manipulate() GraphicTool Manipulate() RotateTool Manipulate() Graphic Draw(Position) Clone() WholeNote Draw(Position) Clone() HalfNote Draw(Position) Clone() Staff Draw(Position) Clone() MusicalNote p = prototype >Clone() while (пользователь тащит мышь) { p >Draw(new position) } вставить p в рисунок Вернуть копию самого себя prototype Вернуть копию самого себя 148 Глава 3. Порождающие паттерны Использование паттерна прототип позволит еще больше сократить число классов. Для целых и половинных нот у нас есть отдельные классы, но, быть может, это излишне. Вместо этого они могли бы быть экземплярами одного и того же класса, инициализированного разными растровыми изо- бражениями и длительностями звучания. Инструмент для создания целых нот становится просто объектом класса GraphicTool , в котором прототип MusicalNote инициализирован целой нотой. Это может значительно умень- шить число классов в системе. Заодно упрощается добавление нового вида нот в музыкальный редактор. Применимость Используйте паттерн прототип, когда система не должна зависеть от того, как в ней создаются, компонуются и представляются продукты; кроме того: классы для создания экземпляров определяются во время выполнения, например с помощью динамической загрузки; или для того чтобы избежать построения иерархий классов или фабрик, па- раллельных иерархии классов продуктов; или экземпляры класса могут находиться в одном из не очень большого числа различных состояний. Может быть удобнее установить соответ- ствующее число прототипов и клонировать их, а не создавать экземпляр каждый раз вручную в подходящем состоянии. Структура Prototype'>Client Operation() Prototype Clone() ConcretePrototype1 Clone() Вернуть копию самого себя ConcretePrototype2 Clone() Вернуть копию самого себя p = prototype>Clone() prototype Участники Prototype ( Graphic ) — прототип: • объявляет интерфейс для клонирования самого себя; Паттерн Prototype (прототип) 149 ConcretePrototype ( Staff — нотный стан, WholeNote — целая нота, HalfNote — половинная нота) — конкретный прототип: • реализует операцию клонирования себя; Client ( GraphicTool ) — клиент: • создает новый объект, обращаясь к прототипу с запросом клонировать себя. Отношения Клиент обращается к прототипу, чтобы тот создал свою копию. Результаты У прототипа те же самые результаты, что у абстрактной фабрики (113) и строителя (124): он скрывает от клиента конкретные классы продуктов, уменьшая тем самым число известных клиенту имен. Кроме того, все эти паттерны позволяют клиентам работать с классами, специфичными для приложения, без модификаций. Ниже перечислены дополнительные преимущества паттерна прототип: добавление и удаление продуктов во время выполнения. Прототип позво- ляет включать новый конкретный класс продуктов в систему, просто за- регистрировав новый экземпляр-прототип на стороне клиента. Это не- сколько более гибкое решение по сравнению с тем, что удастся сделать с помощью других порождающих паттернов, ибо клиент может устанав- ливать и удалять прототипы во время выполнения; определение новых объектов путем изменения значений. Динамичные системы позволяют определять поведение посредством композиции объектов — например, путем задания значений переменных объекта, — а не посредством определения новых классов. Фактически вы опреде- ляете новые виды объектов, создавая экземпляры уже существующих классов и регистрируя их экземпляры как прототипы клиентских объ- ектов. Клиент может изменить поведение, делегируя свои обязанности прототипу. Такой дизайн позволяет пользователям определять новые «классы» без программирования. Фактически клонирование объекта аналогично созданию экземпляра. Паттерн прототип может резко уменьшить число необходимых системе классов. В нашем музыкальном редакторе с по- мощью одного только класса GraphicTool удастся создать бесконечное разнообразие музыкальных объектов; 150 Глава 3. Порождающие паттерны определение новых объектов путем изменения структуры. Многие при- ложения строят объекты из крупных и мелких составляющих. Напри- мер, редакторы для проектирования печатных плат создают электриче- ские схемы из подсхем 1 . Такие приложения часто позволяют создавать экземпляры сложных, определенных пользователем структур — скажем, для многократного использования некоторой подсхемы. Паттерн прототип поддерживает и такую возможность. Мы просто до- бавляем подсхему как прототип в палитру доступных элементов схемы. При условии что объект, представляющий составную схему, реализует операцию Clone как глубокое копирование, схемы с разными структурами могут выступать в качестве прототипов; уменьшение числа подклассов. Паттерн фабричный метод часто порождает иерархию классов Creator , параллельную иерархии классов продуктов. Прототип позволяет клонировать прототип, а не запрашивать фабричный метод создать новый объект. Поэтому иерархия класса Creator стано- вится вообще ненужной. Это преимущество касается главным образом языков типа C++, где классы не рассматриваются как настоящие объек- ты. В языках же типа Smalltalk и Objective C это не так существенно, по- скольку всегда можно использовать объект-класс в качестве создателя. В таких языках объекты-классы уже выступают как прототипы; динамическая настройка конфигурации приложения классами. Некото- рые среды позволяют динамически загружать классы в приложение во время его выполнения. Паттерн прототип — это ключ к применению та- ких возможностей в языке типа C++. Приложение, которое создает экземпляры динамически загружаемого класса, не может обращаться к его конструктору статически. Вместо этого исполняющая среда автоматически создает экземпляр каждого класса в момент его загрузки и регистрирует экземпляр в диспетчере прототипов (см. раздел «Реализация»). Затем приложение может запросить у дис- петчера прототипов экземпляры вновь загруженных классов, которые изначально не были связаны с программой. Каркас приложений ET++ [WGM88] в своей исполняющей среде использует именно такую схему. Основной недостаток паттерна прототип заключается в том, что каждый подкласс класса Prototype должен реализовывать операцию Clone , а это далеко не всегда просто. Например, сложно добавить операцию Clone , если рассматриваемые классы уже существуют. Проблемы возникают 1 Для таких приложений характерны паттерны компоновщик и декоратор Паттерн Prototype (прототип) 151 и в случае, если во внутреннем представлении объекта присутствуют другие объекты, не поддерживающие копирования, или наличествуют циклические ссылки. Реализация Прототип особенно полезен в статически типизированных языках вроде C++, где классы не являются объектами, а во время выполнения информа- ции о типе недостаточно или нет вовсе. Меньший интерес данный паттерн представляет для таких языков, как Smalltalk или Objective C, в которых и так уже есть нечто эквивалентное прототипу (а именно — объект-класс) для создания экземпляров каждого класса. Этот паттерн уже встроен в языки, основанные на прототипах, например Self [US87], где создание любого объ- екта выполняется путем клонирования прототипа. Рассмотрим основные вопросы, возникающие при реализации прототипов: использование диспетчера прототипов. Если число прототипов в си- стеме не фиксировано (то есть они могут создаваться и уничтожаться динамически), создайте реестр доступных прототипов. Клиенты долж- ны не управлять прототипами самостоятельно, а сохранять и извле- кать их из реестра. Клиент запрашивает прототип из реестра перед его клонированием. Такой реестр мы будем называть диспетчером прото- типов. Диспетчер прототипов — это ассоциативное хранилище, которое возвра- щает прототип, соответствующий заданному ключу. В нем есть операции для регистрации прототипа с указанным ключом и отмены регистрации. Клиенты могут изменять и просматривать реестр во время выполне- ния — а значит, расширять систему и вести контроль над ее состоянием без написания кода; реализация операции Clone. Самая трудная часть паттерна прототип — правильная реализация операции Clone . Особенно сложно это в случае, когда в структуре объекта есть циклические ссылки. В большинстве языков имеется некоторая поддержка для клонирования объектов. Например, Smalltalk предоставляет реализацию копирования, которую все подклассы наследуют от класса Object . В C++ поддержива- ются копирующие конструкторы. Тем не менее, эти средства не решают проблему «глубокого и поверхностного копирования» [GR83]. Суть ее в следующем: должны ли при клонировании объекта клонироваться так- же и его переменные экземпляра или клон просто совместно использует с оригиналом эти переменные? 152 Глава 3. Порождающие паттерны Поверхностное копирование просто реализуется, и часто его бывает до- статочно. В частности, его предоставляет по умолчанию Smalltalk. В C++ копирующий конструктор по умолчанию выполняет копирование на уровне членов класса, то есть указатели совместно используются копией и оригиналом. Но для клонирования прототипов со сложной структурой обычно необходимо глубокое копирование, поскольку клон должен быть независим от оригинала. Поэтому нужно гарантировать, что компоненты клона являются клонами компонентов прототипа. При клонировании вам приходится решать, какие компоненты могут использоваться совместно (и могут ли вообще). Если объекты в системе предоставляют операции Save (сохранить) и Load (загрузить), то разрешается воспользоваться ими для реализации опера- ции Clone по умолчанию, просто сохранив и сразу же загрузив объект. Операция Save сохраняет объект в буфере, находящемся в памяти, а Load создает дубликат, реконструируя объект из буфера; инициализация клонов. Хотя некоторым клиентам вполне достаточно клона как такового, другим нужно полностью или частично инициали- зировать его внутреннее состояние. Обычно передать начальные значе- ния операции Clone невозможно, поскольку их число различно для раз- ных классов прототипов. Одним прототипам нужно много параметров инициализации, другим вообще ничего не требуется. Передача Clone параметров мешает построению единообразного интерфейса клониро- вания. Возможно, в ваших классах прототипов уже определяются операции для установки и сброса некоторых важных элементов состояния. Если так, то этими операциями можно воспользоваться сразу после клонирования. В противном случае, возможно, понадобится ввести операцию Initialize (см. раздел «Пример кода»), которая принимает начальные значения в качестве аргументов и соответственно устанавливает внутреннее со- стояние клона. Будьте осторожны, если операция Clone реализует глубо- кое копирование: возможно, копии придется удалять (явно или внутри Initialize ) перед повторной инициализацией. Пример кода Мы определим подкласс MazePrototypeFactory класса MazeFactory . Этот подкласс будет инициализироваться прототипами объектов, которые ему предстоит создавать, поэтому нам не придется порождать подклассы только ради изменения классов создаваемых стен или комнат. Паттерн Prototype (прототип) 153 MazePrototypeFactory дополняет интерфейс MazeFactory конструктором, в аргументах которого передаются прототипы: class MazePrototypeFactory : public MazeFactory { public: MazePrototypeFactory(Maze*, Wall*, Room*, Door*); virtual Maze* MakeMaze() const; virtual Room* MakeRoom(int) const; virtual Wall* MakeWall() const; virtual Door* MakeDoor(Room*, Room*) const; private: Maze* _prototypeMaze; Room* _prototypeRoom; Wall* _prototypeWall; Door* _prototypeDoor; }; Новый конструктор просто инициализирует свои прототипы: MazePrototypeFactory::MazePrototypeFactory ( Maze* m, Wall* w, Room* r, Door* d ) { _prototypeMaze = m; _prototypeWall = w; _prototypeRoom = r; _prototypeDoor = d; } Функции для создания стен, комнат и дверей похожи друг на друга: каж- дая клонирует, а затем инициализирует прототип. Определения функций MakeWall и MakeDoor выглядят так: Wall* MazePrototypeFactory::MakeWall () const { return _prototypeWall->Clone(); } Door* MazePrototypeFactory::MakeDoor (Room* r1, Room *r2) const { Door* door = _prototypeDoor->Clone(); door->Initialize(r1, r2); return door; } MazePrototypeFactory можно применить для создания лабиринта-прототи- па или лабиринта по умолчанию, просто инициализируя его прототипами базовых компонентов: 154 Глава 3. Порождающие паттерны MazeGame game; MazePrototypeFactory simpleMazeFactory( new Maze, new Wall, new Room, new Door ); Maze* maze = game.CreateMaze(simpleMazeFactory); Для изменения типа лабиринта следует инициализировать MazeProto- typeFactory другим набором прототипов. Следующий вызов создает лаби- ринт с дверью типа BombedDoor и комнатой типа RoomWithABomb : MazePrototypeFactory bombedMazeFactory( new Maze, new BombedWall, new RoomWithABomb, new Door ); Объект, который предполагается использовать в качестве прототипа, напри- мер экземпляр класса Wall , должен поддерживать операцию Clone . Кроме того, у него должен быть копирующий конструктор для клонирования. Также может потребоваться операция для повторной инициализации внутреннего состояния. Мы добавим в класс Door операцию Initialize , чтобы дать кли- ентам возможность инициализировать комнаты клона. Сравните следующее определение Door с приведенным на с. 111: class Door : public MapSite { public: Door(); Door(const Door&); virtual void Initialize(Room*, Room*); virtual Door* Clone() const; virtual void Enter(); Room* OtherSideFrom(Room*); private: Room* _room1; Room* _room2; }; Door::Door (const Door& other) { _room1 = other._room1; _room2 = other._room2; } void Door::Initialize (Room* r1, Room* r2) { Паттерн Prototype (прототип) 155 _room1 = r1; _room2 = r2; } Door* Door::Clone () const { return new Door(*this); } Подкласс BombedWall должен заместить операцию Clone и реализовать со- ответствующий копирующий конструктор: class BombedWall : public Wall { public: BombedWall(); BombedWall(const BombedWall&); virtual Wall* Clone() const; bool HasBomb(); private: bool _bomb; }; BombedWall::BombedWall (const BombedWall& other) : Wall(other) { _bomb = other._bomb; } Wall* BombedWall::Clone () const { return new BombedWall(*this); } Операция BombedWall::Clone возвращает Wall* , а ее реализация — указатель на новый экземпляр подкласса, то есть BombedWall* . Мы определяем Clone в базовом классе именно таким образом, чтобы клиентам, клонирующим прототип, не надо было знать о его конкретных подклассах. Клиентам ни- когда не придется приводить значение, возвращаемое Clone , к нужному типу. В Smalltalk стандартный метод копирования, унаследованный от класса Object , может использоваться для клонирования любого прототипа MapSite Фабрикой MazeFactory можно воспользоваться для изготовления любых необходимых прототипов — например, создать комнату по ее номеру #room В классе MazeFactory есть словарь, связывающий имена с прототипами. Его метод make: выглядит так: make: partName ^ (partCatalog at: partName) copy 156 Глава 3. Порождающие паттерны Имея подходящие методы для инициализации MazeFactory прототипами, можно было бы создать простой лабиринт с помощью следующего кода: CreateMaze on: (MazeFactory new with: Door new named: #door; with: Wall new named: #wall; with: Room new named: #room; yourself) где определение метода класса on: для CreateMaze имеет вид on: aFactory | room1 room2 | room1 := (aFactory make: #room) location: 1@1. room2 := (aFactory make: #room) location: 2@1. door := (aFactory make: #door) from: room1 to: room2. room1 atSide: #north put: (aFactory make: #wall); atSide: #east put: door; atSide: #south put: (aFactory make: #wall); atSide: #west put: (aFactory make: #wall). room2 atSide: #north put: (aFactory make: #wall); atSide: #east put: (aFactory make: #wall); atSide: #south put: (aFactory make: #wall); atSide: #west put: door. ^ Maze new addRoom: room1; addRoom: room2; yourself Известные применения Вероятно, паттерн прототип был впервые использован в системе Sketchpad Айвена Сазерленда (Ivan Sutherland) [Sut63]. Первым широко известным применением этого паттерна в объектно-ориентированном языке была система ThingLab, в которой пользователи могли сформировать составной объект, а затем превратить его в прототип, поместив в библиотеку по- вторно используемых объектов [Bor81]. Адель Голдберг и Давид Робсон упоминают прототипы в качестве паттернов в работе [GR83], но Джеймс Коплиен [Cop92] рассматривает этот вопрос гораздо шире. Он описывает связанные с прототипом идиомы языка C++ и приводит много примеров и вариантов. Паттерн Singleton (одиночка) 157 Etgdb — это оболочка для отладчиков на базе ET++, поддерживающая интерфейс типа point-and-click (укажи и щелкни) для различных команд- ных отладчиков. Для каждого из них есть свой подкласс DebuggerAdaptor Например, GdbAdaptor настраивает etgdb на синтаксис команд GNU gdb, а SunDbxAdaptor — на отладчик dbx компании Sun. Набор подклассов DebuggerAdaptor не «зашит» в etgdb. Вместо этого он получает имя адапте- ра из переменной среды, ищет в глобальной таблице прототип с указанным именем, а затем его клонирует. Чтобы добавить к etgdb новые отладчики, следует связать их с подклассом DebuggerAdaptor , разработанным для этого отладчика. Библиотека приемов взаимодействия в программе Mode Composer хранит прототипы объектов, поддерживающих различные способы интерактивных отношений [Sha90]. Любой созданный с помощью Mode Composer способ взаимодействия можно применить в качестве прототипа, если поместить его в библиотеку. Паттерн прототип позволяет программе поддерживать неограниченное число вариантов отношений. Пример музыкального редактора, обсуждавшийся в начале этого раздела, основан на каркасе графических редакторов Unidraw [VL90]. Родственные паттерны В некоторых отношениях прототип и абстрактная фабрика (113) являют- ся конкурентами, о чем будет рассказано в конце главы. Тем не менее, они могут использоваться совместно. Абстрактная фабрика может хранить набор прототипов, которые клонируются и возвращают изготовленные объекты. В тех проектах, где активно применяются паттерны компоновщик (196) и декоратор (209), тоже можно извлечь пользу из прототипа. ПАТТЕРН SINGLETON (ОДИНОЧКА) Название и классификация паттерна Одиночка — паттерн, порождающий объекты. Назначение Гарантирует, что у класса существует только один экземпляр, и предостав- ляет к нему глобальную точку доступа. 158 Глава 3. Порождающие паттерны Мотивация Для некоторых классов важно, чтобы существовал только один экземпляр. В системе может быть много принтеров, но может существовать лишь один спулер. В операционной системе должна быть только одна файловая система и единственный оконный менеджер. В цифровом фильтре может находить- ся только один аналого-цифровой преобразователь (АЦП). Бухгалтерская система обслуживает только одну компанию. Как гарантировать, что у класса есть единственный экземпляр и что этот экземпляр легко доступен? Глобальная переменная дает доступ к объекту, но не запрещает создать несколько экземпляров класса. Более удачное решение — возложить на сам класс ответственность за то, что у него существует только один экземпляр. Класс может запретить создание дополнительных экземпляров, перехватывая запросы на создание новых объектов, и он же способен предоставить доступ к своему экземпляру. Это и есть назначение паттерна одиночка. Применимость Основные условия для применения паттерна одиночка: должен существовать ровно один экземпляр некоторого класса, к кото- рому может обратиться любой клиент через известную точку доступа; единственный экземпляр должен расширяться путем порождения под- классов, а клиенты должны иметь возможность работать с расширен- ным экземпляром без модификации своего кода. Структура Singleton static Instance() SingletonOperation() GetSingletonData() static uniqueInstance singletonData return uniqueInstance Участники Singleton — одиночка: • определяет операцию Instance , которая позволяет клиентам получить доступ к единственному экземпляру. Instance — это операция класса, Паттерн Singleton (одиночка) 159 то есть метод класса в терминологии Smalltalk и статическая функция класса в C++; • может нести ответственность за создание собственного уникального экземпляра. Отношения Клиенты получают доступ к экземпляру класса Singleton только через его операцию Instance Результаты Паттерн одиночка обладает рядом достоинств: контролируемый доступ к единственному экземпляру. Поскольку класс Singleton инкапсулирует свой единственный экземпляр, он полностью контролирует то, как и когда клиенты получают доступ к нему; сокращение пространства имен. Паттерн одиночка — шаг вперед по срав- нению с глобальными переменными. Он позволяет избежать засорения пространства имен глобальными переменными, в которых хранятся уникальные экземпляры; возможность уточнения операций и представления. От класса Singleton можно порождать подклассы, а приложение легко настраивается экзем- пляром расширенного класса. Приложение можно настроить экземпля- ром нужного класса во время выполнения; возможность использования переменного числа экземпляров. Паттерн по- зволяет легко изменить решение и разрешить появление более одного экземпляра класса Singleton . Более того, тот же подход может исполь- зоваться для управления числом экземпляров, используемых в прило- жении. Изменить нужно будет лишь операцию, дающую доступ к экзем- пляру класса Singleton ; большая гибкость, чем у операций класса. Другой способ реализации функциональности одиночки — использование операций класса, то есть статических функций класса в C++ и методов класса в Smalltalk. Но оба этих приема препятствуют изменению дизайна, если потребуется разре- шить наличие нескольких экземпляров класса. Кроме того, статические функции классов в C++ не могут быть виртуальными, что делает невоз- можной их полиморфную замену в подклассах. Реализация При использовании паттерна одиночка надо рассмотреть следующие вопросы: 160 Глава 3. Порождающие паттерны гарантии существования единственного экземпляра. Паттерн одиночка устроен так, что тот единственный экземпляр, который имеется у клас- са, — самый обычный, но сам класс написан так, что больше одного эк- земпляра создать не удастся. Чаще всего для этого операция, создающая экземпляры, скрывается за операцией класса (то есть за статической функцией или методом класса), которая гарантирует создание не более одного экземпляра. Данная операция имеет доступ к переменной, где хранится уникальный экземпляр, и гарантирует инициализацию пере- менной этим экземпляром перед возвратом ее клиенту. При таком под- ходе можно не сомневаться, что одиночка будет создан и инициализиро- ван перед первым использованием. В C++ операция класса определяется с помощью статической функции Instance класса Singleton . В этот класс также включена статическая пере- менная _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 инициализируется нулем, а ста- тическая функция Instance возвращает ее значение, инициализируя ее уникальным экземпляром, если в текущий момент оно равно 0. Функ- ция Instance использует отложенную инициализацию: возвращаемое ей значение не создается и не сохраняется вплоть до момента первого обращения. Паттерн Singleton (одиночка) 161 Обратите внимание, что конструктор защищенный. Клиент, который попытается создать экземпляр класса Singleton непосредственно, полу- чит ошибку на этапе компиляции. Тем самым гарантируется, что будет создан только один экземпляр. Далее, поскольку _instance — указатель на объект класса Singleton , то функция Instance может присвоить этой переменной указатель на любой подкласс данного класса. Применение возможности мы увидим в разделе «Пример кода». О реализации в C++ стоит сказать особо. Недостаточно определить рас- сматриваемый паттерн как глобальный или статический объект, а затем полагаться на автоматическую инициализацию. Тому есть три причины: • невозможно гарантировать, что в программе будет объявлен только один экземпляр статического объекта; • у нас может не быть достаточно информации для создания экземпляра каждого одиночки во время статической инициализации. Одиночке мо- гут потребоваться данные, вычисляемые позже, во время выполнения программы; • в С++ не определяется порядок вызова конструкторов для глобальных объектов через границы единиц трансляции [ES90]. Это означает, что между одиночками не может существовать никаких зависимостей. Если они есть, то ошибок не избежать. Еще один недостаток глобальных/статических объектов в том, что при- ходится создавать всех одиночек, даже если они не используются. При- менение статической функции класса решает эту проблему. В Smalltalk функция, возвращающая уникальный экземпляр, реализу- ется как метод класса Singleton. Чтобы гарантировать единственность экземпляра, следует заместить операцию new . Получающийся класс мог бы иметь два метода класса (в них SoleInstance — это переменная класса, которая больше нигде не используется): new self error: 'cannot create new object' default SoleInstance isNil ifTrue: [SoleInstance := super new]. ^ SoleInstance Порождение подклассов Singleton. Основной вопрос не столько в том, как определить подкласс, а в том, как оформить его уникальный экзем- 162 Глава 3. Порождающие паттерны пляр, чтобы клиенты могли использовать его. По существу, переменная, ссыла ющаяся на экземпляр одиночки, должна инициализироваться вместе с экземпляром подкласса. Простейший способ добиться этого — определить одиночку, которого нужно применять в операции Instance класса Singleton . В разделе «Пример кода» показывается, как можно реализовать эту технику с помощью переменных среды. Другой способ выбора подкласса Singleton — вынести реализацию операции Instance из родительского класса (например, MazeFactory ) и поместить ее в подкласс. Это позволит программисту на C++ задать класс одиночки на этапе компоновки (например, скомпоновав программу с объектным файлом, содержащим другую реализацию), но от клиента одиночка будет по-прежнему скрыт. Такой подход фиксирует выбор класса одиночки на этапе компоновки, затрудняя тем самым его подмену во время выполнения. Применение условных операторов для выбора подкласса увеличивает гибкость ре- шения, но все равно множество возможных классов Singleton остается жестко «зашитым» в код. В общем случае ни тот, ни другой подход не обеспечивают достаточной гибкости. Ее можно добиться за счет использования реестра одиночек. Вместо того чтобы задавать множество возможных классов Singleton в операции Instance , одиночки могут регистрировать себя по имени в некотором всем известном реестре. Реестр сопоставляет одиночкам строковые имена. Когда операции Instance нужен некоторый одиночка, она запрашивает его у реестра по имени. Начинается поиск указанного одиночки, и, если он существует, реестр возвращает его. Такой подход освобождает Instance от необхо- димости «знать» все возможные классы или экземпляры 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 }; Паттерн Singleton (одиночка) 163 Операция Register регистрирует экземпляр класса Singleton под ука- занным именем. Чтобы не усложнять реестр, мы будем хранить его в виде списка объектов NameSingletonPair . Каждый такой объект устанавливает соответствие между именем и одиночкой. Операция Lookup ищет одиноч- ку по имени. Допустим, имя нужного одиночки передается в переменной среды: Singleton* Singleton::Instance () { if (_instance == 0) { const char* singletonName = getenv("SINGLETON"); // Задается пользователем или средой при запуске _instance = Lookup(singletonName); // Lookup возвращает 0, если такой одиночка не найден. } return _instance; } В какой момент классы Singleton регистрируют себя? Одна из возможно- стей — конструктор. Например, подкласс MySingleton мог бы работать так: MySingleton::MySingleton() { // ... Singleton::Register("MySingleton", this); } Разумеется, конструктор не будет вызван, пока кто-то не создаст экземпляр класса, но ведь это та самая проблема, которую паттерн одиночка и пытается разрешить! В C++ ее можно попытаться обойти, определив статический экземпляр класса MySingleton . Например, можно вставить строку static MySingleton theSingleton; в файл с реализацией MySingleton Теперь класс Singleton не отвечает за создание одиночки. Его основной обязанностью становится обеспечение доступа к объекту-одиночке из лю- бой части системы. Решение со статическим объектом по-прежнему имеет потенциальный недостаток: необходимость создания экземпляров всех возможных подклассов Singleton , без чего они не будут зарегистрированы. Пример кода Предположим, нам надо определить класс MazeFactory для создания ла- биринтов, описанный на с. 111. MazeFactory определяет интерфейс для 164 Глава 3. Порождающие паттерны построения различных частей лабиринта. В подклассах эти операции могут переопределяться, чтобы возвращать экземпляры специализированных классов продуктов, например объекты BombedWall , а не просто Wall Существенно здесь то, что приложению Maze нужен лишь один экземпляр фабрики лабиринтов и он должен быть доступен в коде, строящем любую часть лабиринта. Тут-то паттерн одиночка и приходит на помощь. Сделав фабрику MazeFactory одиночкой, мы сможем обеспечить глобальную до- ступность объекта, представляющего лабиринт, не прибегая к глобальным переменным. Для простоты предположим, что мы никогда не порождаем подклассов от MazeFactory . (Чуть ниже будет рассмотрен альтернативный подход.) В C++ для того, чтобы превратить фабрику в одиночку, мы добавляем в класс MazeFactory статическую операцию Instance и статический член _instance , в котором будет храниться единственный экземпляр. Нужно также сделать конструктор защищенным, чтобы предотвратить случайное создание экзем- пляра, в результате которого будет создан лишний экземпляр: class MazeFactory { public: static MazeFactory* Instance(); // Здесь находится существующий интерфейс protected: MazeFactory(); private: static MazeFactory* _instance; }; Соответствующая реализация выглядит так: MazeFactory* MazeFactory::_instance = 0; MazeFactory* MazeFactory::Instance () { if (_instance == 0) { _instance = new MazeFactory; } return _instance; } Теперь посмотрим, что случится, когда у MazeFactory есть подклассы и опре- деляется, какой из них использовать. Вид лабиринта мы будем выбирать с помощью переменной среды, поэтому добавим код, который создает эк- земпляр нужного подкласса MazeFactory в зависимости от значения данной Паттерн Singleton (одиночка) 165 переменной. Лучше всего поместить код в операцию Instance , поскольку она уже и так создает экземпляр MazeFactory : MazeFactory* MazeFactory::Instance () { if (_instance == 0) { const char* mazeStyle = getenv("MAZESTYLE"); if (strcmp(mazeStyle, "bombed") == 0) { _instance = new BombedMazeFactory; } else if (strcmp(mazeStyle, "enchanted") == 0) { _instance = new EnchantedMazeFactory; // ...другие возможные подклассы } else { // по умолчанию _instance = new MazeFactory; } } return _instance; } Отметим, что операцию Instance придется модифицировать при определе- нии каждого нового подкласса MazeFactory . Возможно, в данном приложении это не создаст проблем, но для абстрактных фабрик, определенных в каркасе, такой подход трудно назвать приемлемым. Одно из решений — воспользоваться принципом реестра, описанным в раз- деле «Реализация». Может помочь и динамическое связывание, тогда при- ложению не нужно будет загружать все неиспользуемые подклассы. Известные применения Примером паттерна одиночка в Smalltalk 80 [Par90] является множество изменений кода, представленное классом ChangeSet . Более тонкий пример — это отношение между классами и их метаклассами. Метаклассом называется класс класса, каждый метакласс существует в единственном экземпляре. У метакласса нет имени (разве что косвенное, определяемое экземпляром), но он контролирует свой уникальный экземпляр, и создать второй обычно не разрешается. В библиотеке InterViews, предназначенной для создания пользовательских интерфейсов [LCI+92], паттерн одиночка применяется для доступа к един- ственным экземплярам классов Session (сессия) и WidgetKit (набор видже- тов). Классом Session определяется главный цикл событий в приложении. 166 Глава 3. Порождающие паттерны Он хранит пользовательские настройки стиля и управляет подключением к одному или нескольким физическим дисплеям. WidgetKit — это абстракт- ная фабрика (113) для определения внешнего облика интерфейсных вид- жетов. Операция WidgetKit::instance() определяет конкретный подкласс WidgetKit для создания экземпляра на основании переменной среды, кото- рую определяет Session . Аналогичная операция в классе Session «выясняет», поддерживаются ли монохромные или цветные дисплеи, и соответственно настраивает конфигурацию одиночного экземпляра Session Родственные паттерны С помощью паттерна одиночка могут быть реализованы многие паттерны. См. описания абстрактной фабрики (113), строителя (124) и прототипа (146). ОБСУЖДЕНИЕ ПОРОЖДАЮЩИХ ПАТТЕРНОВ Существуют два распространенных способа параметризации системы клас- сами создаваемых ей объектов. Первый способ — порождение подклассов от класса, создающего объекты. Он соответствует паттерну фабричный метод (135). Основной недостаток такого решения — необходимость создания нового подкласса лишь для того, чтобы изменить класс продукта. Это мо- жет привести к распространению каскадных изменений. Например, если создатель продукта сам создается фабричным методом, то придется замещать и создателя тоже. Другой способ параметризации системы в большей степени основан на композиции объектов. Вы определяете объект, которому известно о клас- сах объектов-продуктов, и делаете его параметром системы. Это ключевой аспект таких паттернов, как абстрактная фабрика (113), строитель (124) и прототип (146). Для всех трех характерно создание «фабричного объек- та», который изготавливает продукты. В абстрактной фабрике фабричный объект производит объекты разных классов. Фабричный объект строителя постепенно создает сложный продукт, следуя специальному протоколу. Фабричный объект прототипа изготавливает продукт путем копирования объекта-прототипа. В последнем случае фабричный объект и прототип — это одно и то же, поскольку именно прототип отвечает за возвращение продукта. Рассмотрим каркас графических редакторов, описанный при обсужде- нии паттерна прототип. Есть несколько способов параметризовать класс GraphicTool классом продукта: Обсуждение порождающих паттернов 167 применить паттерн фабричный метод. Тогда для каждого подкласса клас- са Graphic в палитре будет создан свой подкласс GraphicTool . В классе GraphicTool будет присутствовать операция NewGraphic , переопределя- емая каждым подклассом; использовать паттерн абстрактная фабрика. Возникнет иерархия клас- сов GraphicsFactories , по одной для каждого подкласса Graphic . В этом случае каждая фабрика создает только один продукт: CircleFactory — окружности Circle , LineFactory — отрезки Line и т. д. GraphicTool па- раметризуется фабрикой для создания подходящих графических объ- ектов; применить паттерн прототип. Тогда в каждом подклассе Graphic будет реализована операция Clone , а GraphicTool параметризуется прототи- пом создаваемого графического объекта. Выбор оптимального паттерна зависит от многих факторов. В нашем при- мере каркаса графических редакторов, на первый взгляд, проще всего вос- пользоваться фабричным методом. Определить новый подкласс GraphicTool легко, а экземпляры GraphicTool создаются только в момент определения па- литры. Основной недостаток такого подхода заключается в комбинаторном росте числа подклассов GraphicTool , причем все они почти ничего не делают. Абстрактная фабрика лишь немногим лучше, поскольку требует создания иерархии классов GraphicsFactory такого же размера. Абстрактной фабрике следует отдавать предпочтение перед фабричным методом лишь тогда, когда уже и так существует иерархия класса GraphicsFactory : либо потому, что ее автоматически строит компилятор (как в Smalltalk или Objective C), либо она необходима для другой части системы. В общем, целям каркаса графических редакторов лучше всего отвечает пат- терн прототип, поскольку для его применения требуется лишь реализовать операцию Clone в каждом классе Graphics . Это сокращает число подклассов, а Clone можно с пользой применить и для решения других задач — напри- мер, для реализации пункта меню Duplicate (дублировать), — а не только для создания экземпляров. С паттерном фабричный метод проект в большей степени поддается настройке при незначительном росте сложности. Другие паттерны нуждаются в соз- дании новых классов, а фабричный метод — только в создании одной новой операции. Часто этот паттерн рассматривается как стандартный способ создания объектов, но вряд ли его стоит рекомендовать в ситуации, когда класс создаваемого экземпляра никогда не изменяется или когда экземпляр 168 Глава 3. Порождающие паттерны создается внутри операции, которую легко можно заместить в подклассах (например, во время инициализации). Проекты, в которых используются паттерны абстрактная фабрика, прототип или строитель, оказываются еще более гибкими, чем те, где применяется фа- бричный метод, но за это приходится платить повышенной сложностью. Часто в начале работы над проектом за основу берется фабричный метод, а позже, когда проектировщик обнаруживает, что решение получается недостаточно гибким, он выбирает другие паттерны. Владение разными паттернами про- ектирования открывает перед вами широкий выбор при оценке различных критериев. |