Название и классификация паттерна. Фабричный метод - паттерн, порождающий классы

Фабричный метод - паттерн, порождающий классы.

Назначение

Определяет интерфейс для создания объекта, но оставляет подклассам решение о том, какой класс инстанцировать. Фабричный метод позволяет классу делегировать инстанцирование подклассам.

Порождающие паттерны

Известен также под именем

Virtual Constructor (виртуальный конструктор).

Мотивация

Каркасы пользуются абстрактными классами для определения и поддержания отношений между объектами. Кроме того, каркас часто отвечает за создание самих объектов.

Рассмотрим каркас для приложений, способных представлять пользователю сразу несколько документов. Две основных абстракции в таком каркасе – это классы Application и Document. Оба класса абстрактные, поэтому клиенты должны порождать от них подклассы для создания специфичных для приложения реализаций. Например, чтобы создать приложение для рисования, мы определим классы DrawingApplication и DrawingDocument. Класс Application отвечает за управление документами и создает их по мере необходимости, допустим, когда пользователь выбирает из меню пункт Open(открыть) или New (создать).

Поскольку решение о том, какой подкласс класса Document инстанцировать, зависит от приложения, то Application не может «предсказать», что именно понадобится. Этому классу известно лишь, когда нужно инстанцировать новый документ, а не какой документ создать. Возникает дилемма: каркас должен инстанцировать классы, но «знает» он лишь об абстрактных классах, которые инстанцировать нельзя.

Решение предлагает паттерн фабричный метод. В нем инкапсулируется информация о том, какой подкласс класса Document создать, и это знание выводится за пределы каркаса.

 

 

Подклассы класса Application переопределяют абстрактную операцию

CreateDocument таким образом, чтобы она возвращала подходящий подкласс класса Document. Как только подкласс Application инстанцирован, он может инстанцировать специфические для приложения документы, ничего не зная об их классах. Операцию CreateDocument мы называем фабричным методом, поскольку она отвечает за «изготовление» объекта.

 

Применимость

Используйте паттерн фабричный метод, когда:

- классу заранее неизвестно, объекты каких классов ему нужно создавать;

- класс спроектирован так, чтобы объекты, которые он создает, специфицировались подклассами;

- класс делегирует свои обязанности одному из нескольких вспомогательных подклассов, и вы планируете локализовать знание о том, какой класс принимает эти обязанности на себя.

 

Структура

 

 

 

Участники

- Product(Document) - продукт:

- определяет интерфейс объектов, создаваемых фабричным методом;

- ConcreteProduct(MyDocument) - конкретный продукт:

- реализует интерфейс Product;

- Creator(Application) - создатель:

- объявляет фабричный метод, возвращающий объект типа Product.

Creator может также определять реализацию по умолчанию фабрич-

ного метода, который возвращает объект ConcreteProduct;

- может вызывать фабричный метод для создания объекта Product.

- ConcreteCreator(MyApplication) - конкретный создатель:

- замещает фабричный метод, возвращающий объект ConcreteProduct.

Отношения

Создатель «полагается» на свои подклассы в определении фабричного метода, который будет возвращать экземпляр подходящего конкретного продукта.

Результаты

Фабричные методы избавляют проектировщика от необходимости встраивать в код зависящие от приложения классы. Код имеет дело только с интерфейсом класса Product, поэтому он может работать с любыми определенными пользователями классами конкретных продуктов.

 

Потенциальный недостаток фабричного метода состоит в том, что клиентам, возможно, придется создавать подкласс класса Creator для создания лишь одного объекта ConcreteProduct. Порождение подклассов оправдано, если клиенту так или иначе приходится создавать подклассы Creator, в противном случае клиенту придется иметь дело с дополнительным уровнем подклассов.

 

А вот еще два последствия применения паттерна срабричный метод:

- предоставляет подклассам операции-зацепки (hooks). Создание объектов внутри класса с помощью фабричного метода всегда оказывается более гибким решением, чем непосредственное создание. Фабричный метод создает в подклассах операции-зацепки для предоставления расширенной версии объекта.

В примере с документом класс Document мог бы определить фабричный метод CreateFileDialog, который создает диалоговое окно для выбора файла существующего документа. Подкласс этого класса мог бы определить специализированное для приложения диалоговое окно, заместив этот фабричный метод. В данном случае фабричный метод не является абстрактным, а содержит разумную реализацию по умолчанию;

- соединяет параллельные иерархии. В примерах, которые мы рассматривали до сих пор, фабричные методы вызывались только создателем. Но это совершенно необязательно: клиенты тоже могут применять фабричные методы, особенно при наличии параллельных иерархий классов.

Параллельные иерархии возникают в случае, когда класс делегирует часть своих обязанностей другому классу, не являющемуся производным от него.

Рассмотрим, например, графические фигуры, которыми можно манипулировать интерактивно: растягивать, двигать или вращать с помощью мыши.

Реализация таких взаимодействий с пользователем - не всегда простое

дело. Часто приходится сохранять и обновлять информацию о текущем состоянии манипуляций. Но это состояние нужно только во время самой манипуляции, поэтому помещать его в объект, представляющий фигуру, не следует. К тому же фигуры ведут себя по-разному, когда пользователь манипулирует ими. Например, растягивание отрезка может сводиться к изменению положения концевой точки, а растягивание текста - к изменению междустрочных интервалов.

При таких ограничениях лучше использовать отдельный объект-манипулятор Manipulator, который реализует взаимодействие и контролирует его текущее состояние. У разных фигур будут разные манипуляторы, являющиеся подклассом Manipulator. Получающаяся иерархия класса Manipulator параллельна (по крайней мере, частично) иерархии класса Figure.

Класс Figure предоставляет фабричный метод CreateManipulator, который позволяет клиентам создавать соответствующий фигуре манипулятор. Подклассы Figure замещают этот метод так, чтобы он возвращал подходящий для них подкласс Manipulator. Вместо этого класс Figure может реализовать CreateManipulator так, что он будет возвращать экземпляр класса Manipulator по умолчанию, а подклассы Figure могут наследовать

 

 

это умолчание. Те классы фигур, которые функционируют по описанному

принципу, не нуждаются в специальном манипуляторе, поэтому иерархии

параллельны только отчасти.

Обратите внимание, как фабричный метод определяет связь между обеими иерархиями классов. В нем локализуется знание о том, какие классы способны работать совместно.

 

Реализация

Рассмотрим следующие вопросы, возникающие при использовании паттерна фабричный метод:

- две основных разновидности паттерна. Во-первых, это случай, когда класс Creator является абстрактным и не содержит реализации объявленного в нем фабричного метода. Вторая возможность: Creator – конкретный класс, в котором по умолчанию есть реализация фабричного метода. Редко, но встречается и абстрактный класс, имеющий реализацию по умолчанию;

В первом случае для определения реализации необходимы подклассы, поскольку никакого разумного умолчания не существует. При этом обходится проблема, связанная с необходимостью инстанцировать заранее неизвестные классы. Во втором случае конкретный класс Creator использует фабричный метод, главным образом ради повышения гибкости. Выполняется правило: «Создавай объекты в отдельной операции, чтобы подклассы могли подменить способ их создания». Соблюдение этого правила гарантирует, что авторы подклассов смогут при необходимости изменить класс объектов, инстанцируемых их родителем;

- параметризованные фабричные методы. Это еще один вариант паттерна, который позволяет фабричному методу создавать разные виды продуктов.

Фабричному методу передается параметр, который идентифицирует вид создаваемого объекта. Все объекты, получающиеся с помощью фабричного метода, разделяют общий интерфейс Product. В примере с документами класс Application может поддерживать разные виды документов. Вы передаете методу CreateDocument лишний параметр, который и определяет, документ какого вида нужно создать.

В каркасе Unidraw для создания графических редакторов [VL90] используется именно этот подход для реконструкции объектов', сохраненных на диске.

Unidraw определяет класс Creator с фабричным методом Create, которому в качестве аргумента передается идентификатор класса, определяющий, какой класс инстанцировать. Когда Unidraw сохраняет объект на диске, он сначала записывает идентификатор класса, а затем его переменные экземпляра. При реконструкции объекта сначала считывается идентификатор класса.

Прочитав идентификатор класса, каркас вызывает операцию Create, передавая ей этот идентификатор как параметр. Create ищет конструктор соответствующего класса и с его помощью производит инстанцирование.

И наконец, Create вызывает операцию Read созданного объекта, которая считывает с диска остальную информацию и инициализирует переменные экземпляра.

Параметризованный фабричный метод в общем случае имеет следующий

вид (здесь My Product и Your Product - подклассы Product):

 

class Creator {

public:

virtual Product* Create(Productld);

};

Product* Creator::Create (Productld id) {

if (id == MINE) return new MyProduct;

if (id == YOURS) return new YourProduct;

// выполнить для всех остальных продуктов...

return 0;

}

 

Замещение параметризованного фабричного метода позволяет легко и избирательно расширить или заменить продукты, которые изготавливает создатель. Можно завести новые идентификаторы для новых видов продуктов или ассоциировать существующие идентификаторы с другими продуктами.

Например, подкласс MyCreator мог бы переставить местами MyProduct и YourProduct для поддержки третьего подкласса Their Product:

 

Product* MyCreator::Create (Productld id) {

if (id == YOURS) return new MyProduct;

if (id == MINE) return new YourProduct;

// N.B.: YOURS и MINE переставлены

if (id == THEIRS) return new TheirProduct;

return Creator::Create(id); // вызывается, если больше ничего

/ / н е осталось

}

 

Обратите внимание, что в самом конце операция вызывает метод Create родительского класса. Так делается постольку, поскольку MyCreator: : Create

 

обрабатывает только продукты YOURS, MINE и THEIRS иначе, чем родительский класс. Поэтому MyCreator расширяет некоторые виды создаваемых продуктов, а создание остальных поручает своему родительскому классу;

- языково-зависимые вариации и проблемы. В разных языках возникают собственные интересные варианты и некоторые нюансы.

Так, в программах на Smalltalk часто используется метод, который возвращает класс подлежащего инстанцированию объекта. Фабричный метод Creator может воспользоваться возвращенным значением для создания продукта, a ConcreteCreator может сохранить или даже вычислить это значение. В результате привязка к типу конкретного инстанцируемого продукта ConcreteProduct происходит еще позже.

В версии примера Document на языке Smalltalk допустимо определить метод documentClass в классе Application. Данный метод возвращает подходящий класс Document для инстанцирования документов. Реализация метода documentClass в классе MyApplication возвращает класс MyDocument. Таким образом, в классе Application мы имеем clientMethod

 

document := self documentClass new.

documentClass

self subclassResponsibility

а в классеMyApplication —

documentClass

^ MyDocument

 

что возвращает класс MyDocument, который должно инстанцировать при-

ложение Application.

Еще более гибкий подход, родственный параметризованным фабричным методам, заключается в том, чтобы сохранить подлежащий созданию класс в качестве переменной класса Application. В таком случае для изменения продукта не нужно будет порождать подкласс Application.

В C++ фабричные методы всегда являются виртуальными функциями, а часто даже исключительно виртуальными. Нужно быть осторожней и не вызывать фабричные методы в конструкторе класса Creator: в этот момент фабричный метод в производном классе ConcreteCreator еще недоступен.

Обойти такую сложность можно, если получать доступ к продуктам только с помощью функций доступа, создающих продукт по запросу. Вместо того чтобы создавать конкретный продукт, конструктор просто инициализирует его нулем. Функция доступа возвращает продукт, но сначала проверяет, что он существует. Если это не так, функция доступа создает продукт. Подобную технику часто называют отложенной инициализацией. В следующем примере показана типичная реализация:

 

class Creator {

public:

Product* GetProduct();

protected:

virtual Product* CreateProduct();

private:

Product* _product;

};

Product* Creator: :GetProduct () {

if (.product == 0) {

_product = CreateProduct ( ) ;

}

return _product;

}

 

- использование шаблонов, чтобы не порождать подклассы. К сожалению, допустима ситуация, когда вам придется порождать подклассы только для того, чтобы создать подходящие объекты-продукты. В C++ этого можно избежать, предоставив шаблонный подкласс класса Creator, параметризованный классом Product:

 

class Creator {

public :

virtual Product* CreateProduct () = 0;

};

template <class TheProduct>

class StandardCreator: public Creator {

public:

virtual Product* CreateProduct();

};

template <class TheProduct>

Product* StandardCreator<TheProduct>::CreateProduct () {

return new TheProduct;

}

 

С помощью данного шаблона клиент передает только класс продукта, порождать подклассы от Creator не требуется:

 

class MyProduct : public Product {

public:

MyProduct();

// ...

};

StandardCreator<MyProduct> myCreator ;

 

- соглашения об именовании. На практике рекомендуется применять такие соглашения об именах, которые дают ясно понять, что вы пользуетесь фабричными методами. Например, каркас МасАрр на платформе Macintosh [App89] всегда объявляет абстрактную операцию, которая определяет фабричный метод, в виде Class* DoMakeClass ( ) , где Class - это класс продукта.

Пример кода

Функция CreateMaze строит и возвращает лабиринт. Одна из связанных с ней проблем состоит в том, что классы лабиринта, комнат, дверей и стен жестко «зашиты» в данной функции.Мы введем фабричные методы, которые позволят выбирать эти компоненты подклассам.

Сначала определим фабричные методы в игре MazeGame для создания объектов лабиринта, комнат, дверей и стен:

 

class MazeGame {

public:

Maze* CreateMaze();

// фабричные методы:

virtual Maze* MakeMazeO const

{ return new Maze; }

virtual Room* MakeRoom(int n) const

{ return new Room(n); }

virtual Wall* MakeWalK) const

{ return new Wall; }

virtual Door* MakeDoor(Room* rl, Room* r2) const

{ return new Door(rl, r2); }

};

 

Каждый фабричный метод возвращает один из компонентов лабиринта. Класс MazeGame предоставляет реализации по умолчанию, которые возвращают простейшие варианты лабиринта, комнаты, двери и стены.

Теперь мы можем переписать функцию CreateMaze с использованием этих

фабричных методов:

 

Maze* MazeGame::CreateMaze () {

Maze* aMaze = MakeMaze();

Room* rl = MakeRoom(l);

Room* r2 = MakeRoom(2);

Door* theDoor = MakeDoor(rl, r2);

aMaze->AddRoom(rl);

aMaze->AddRoom(r2);

rl->SetSide(North, MakeWall());

rl->SetSide(East, theDoor);

rl->SetSide(South, MakeWall());

rl->SetSide(West, MakeWall());

r2->SetSide(North, MakeWall());

r2->SetSide(East, MakeWall());

r2->SetSide(South, MakeWall());

r2->SetSide(West, theDoor);

return aMaze;

}

 

В играх могут порождаться различные подклассы MazeGame для специализации частей лабиринта. В этих подклассах допустимо переопределение некоторых или всех методов, от которых зависят разновидности продуктов. Например, в игре BombedMazeGame продукты Room и Wall могут быть переопределены так, чтобы возвращать комнату и стену с заложенной бомбой:

 

class BombedMazeGame : public MazeGame {

public:

BombedMazeGame();

virtual Wall* MakeWall() const

{ return new BombedWall; }

virtual Room* MakeRoom(int n) const

{ return new RoomWithABomb(n); }

};

 

- в игре Enchant edMazeGame допустимо определить такие варианты:

 

class EnchantedMazeGame : public MazeGame {

public:

EnchantedMazeGame();

virtual Room* MakeRoomdnt n) const

{ return new EnchantedRoom(n, CastSpell()); }

virtual Door* MakeDoor(Room* rl, Room* r2) const

{ return new DoorNeedingSpell(rl, r2); }

protected:

Spell* CastSpell() const;

};

Известные применения

Фабричные методы в изобилии встречаются в инструментальных библиотеках и каркасах. Рассмотренный выше пример с документами - это типичное применение в каркасе МасАрр и библиотеке ЕТ++ [WGM88]. Пример с манипулятором заимствован из каркаса Unidraw.

Класс View в схеме модель/вид/контроллер из языка Smalltalk-80 имеет метод defaultController, который создает контроллер, и этот метод выглядит как фабричный [РагЭО]. Но подклассы View специфицируют класс своего контроллера по умолчанию, определяя метод def aultControllerClass, возвращающий класс, экземпляры которого создает defaultController. Таким образом, реальным фабричным методом является def aultControllerClass, то есть метод, который должен переопределяться в подклассах.

Более необычным является пример фабричного метода parserClass, тоже

взятый из Smalltalk-80, который определяется поведением Behavior (суперкласс всех объектов, представляющих классы). Он позволяет классу использовать специализированный анализатор своего исходного кода. Например, клиент может определить класс SQLParser для анализа исходного кода класса, содержащего встроенные предложения на языке SQL. Класс Behavior реализует par serClass так, что тот возвращает стандартный для Smalltalk класс анализатора Parser. Класс же, включающий предложения SQL, замещает этот метод (как метод класса) и возвращает класс SQLParser.

Система Orbix ORB от компании IONA Technologies [ION94] использует фабричный метод для генерирования подходящих заместителей (см. паттерн заместитель) в случае, когда объект запрашивает ссылку на удаленный объект. Фабричный метод позволяет без труда заменить подразумеваемого заместителя, например таким, который применяет кэширование на стороне клиента.

Родственные паттерны

Абстрактная фабрика часто реализуется с помощью фабричных методов.

Пример в разделе «Мотивация» из описания абстрактной фабрики иллюстри. ет также и паттерн фабричные методы.

Паттерн фабричные методы часто вызывается внутри шаблонных методов.

В примере с документами NewDocument - это шаблонный метод.

Прототипы не нуждаются в порождении подклассов от класса Creator. Однако им часто бывает необходима операция Initialize в классе Product.

Treator использует Initialize для инициализации объекта. Фабричному

методу такая операция не требуется.

 

 

Паттерн Prototype

Паттерн Prototype

Название и классификация паттерна

Прототип - паттерн, порождающий объекты.

Назначение

Задает виды создаваемых объектов с помощью экземпляра-прототипа и создает новые объекты путем копирования этого прототипа.

Мотивация

Построить музыкальный редактор удалось бы путем адаптации общего каркаса графических редакторов и добавления новых объектов, представляющих ноты, паузы и нотный стан. В каркасе редактора может присутствовать палитра инструментов для добавления в партитуру этих музыкальных объектов. Палитра может также содержать инструменты для выбора, перемещения и иных манипуляций с объектами. Так, пользователь, щелкнув, например, по значку четверти поместил бы ее тем самым в партитуру. Или, применив инструмент перемещения, : двигал бы ноту на стане вверх или вниз, чтобы изменить ее высоту.

Предположим, что каркас предоставляет абстрактный класс Graphic для графических компонентов вроде нот и нотных станов, а также абстрактный класс Tool для определения инструментов в палитре. Кроме того, в каркасе имеется предопределенный подкласс GraphicTool для инструментов, которые создают графические объекты и добавляют их в документ.

Однако класс GraphicTool создает некую проблему для проектировщика

каркаса. Классы нот и нотных станов специфичны для нашего приложения, а класс GraphicTool принадлежит каркасу. Этому классу ничего неизвестно о том, как создавать экземпляры наших музыкальных классов и добавлять их в партитуру. Можно было бы породить от GraphicTool подклассы для каждого вида музыкальных объектов, но тогда оказалось бы слишком много классов, отличающихся только тем, какой музыкальный объект они инстанцируют. Мы знаем, что гибкой альтернативой порождению подклассов является композиция. Вопрос в том, как каркас мог бы воспользоваться ею для параметризации экземпляров GraphicTool классом того объекта Graphic, который предполагается создать.

Решение - заставить GraphicTool создавать новый графический объект, копируя или ≪клонируя≫ экземпляр подкласса класса Graphic. Этот экземпляр мы будем называть прототипом. GraphicTool параметризуется прототипом, который он должен клонировать и добавить в документ. Если все подклассы Graphic поддерживают операцию Clone, то GraphicTool может клонировать любой вид графических объектов.

Итак, в нашем музыкальном редакторе каждый инструмент для созданияму зыкального объекта - это экземпляр класса GraphicTool, инициализированный тем или иным прототипом. Любой экземпляр GraphicTool будет создавать музыкальный объект, клонируя его прототип и добавляя клон в партитуру.

Можно воспользоваться паттерном прототип, чтобы еще больше сократить число классов. Для целых и половинных нот у нас есть отдельные классы, но, быть может, это излишне. Вместо этого они могли бы быть экземплярами одного и того же класса, инициализированного разными растровыми изображениями и длительностями звучания. Инструмент для создания целых нот становится просто объектом класса GraphicTool, в котором прототип MusicalNote инициализирован целой нотой. Это может значительно уменьшить число классов в системе.

Заодно упрощается добавление нового вида нот в музыкальный редактор.

Применимость

Используйте паттерн прототип, когда система не должна зависеть от того, как в ней создаются, компонуются и представляются продукты:

- инстанцируемые классы определяются во время выполнения, например с помощью динамической загрузки;

- для того чтобы избежать построения иерархий классов или фабрик, параллельных иерархии классов продуктов;

- экземпляры класса могут находиться в одном из не очень большого числа различных состояний. Может оказаться удобнее установить соответствую щее число прототипов и клонировать их, а не инстанцировать каждый раз класс вручную в подходящем состоянии.

Структура

Участники

- Prototype(Graphic) - прототип:

- объявляет интерфейс для клонирования самого себя;

- ConcretePrototype( S t a f f - нотный стан, WholeNote - целая нота, Half Note - половинная нота) - конкретный прототип:

- реализует операцию клонирования себя;

- Client(GraphicTool) - клиент:

- создает новый объект, обращаясь к прототипу с запросом клонировать себя.

Отношения

Клиент обращается к прототипу, чтобы тот создал свою копию.

Результаты

У прототипа те же самые результаты, что у абстрактной фабрики и строителя: он скрывает от клиента конкретные классы продуктов, уменьшая тем самым число известных клиенту имен. Кроме того, все эти паттерны позволяют клиентам работать со специфичными для приложения классами без модификаций.

Ниже перечислены дополнительные преимущества паттерна прототип:

- добавление и удаление продуктов во время выполнения. Прототип позволяет включать новый конкретный класс продуктов в систему, просто сообщив клиенту о новом экземпляре-прототипе. Это несколько более гибкое решение по сравнению с тем, что удастся сделать с помощью других порождающих паттернов, ибо клиент может устанавливать и удалять прототипы во время выполнения;

- спецификация новых объектов путем изменения значений. Динамичные системы позволяют определять поведение за счет композиции объектов - например, путем задания значений переменных объекта, - а не с помощью определения новых классов. По сути дела, вы определяете новые виды объектов, инстанцируя уже существующие классы и регистрируя их-экземпляры как прототипы клиентских объектов. Клиент может изменить поведение, делегируя свои обязанности прототипу.

Такой дизайн позволяет пользователям определять новые классы без программирования. Фактически клонирование объекта аналогично инстанцированию класса. Паттерн прототип может резко уменьшить число необходимых системе классов. В нашем музыкальном редакторе с помощью одного только класса GraphicTool удастся создать бесконечное разнообразие музыкальных объектов;

- специфицирование новых объектов путем изменения структуры. Многие

приложения строят объекты из крупных и мелких составляющих. Например, редакторы для проектирования печатных плат создают электрические схемы из подсхем.1 Такие приложения часто позволяют инстанцировать сложные, определенные пользователем структуры, скажем, для многократного использования некоторой подсхемы.

Паттерн прототип поддерживает и такую возможность. Мы просто добавляем подсхему как прототип в палитру доступных элементов схемы. При условии, что объект, представляющий составную схему, реализует операцию Clone как глубокое копирование, схемы с разными структурами могут выступать в качестве прототипов;

- уменьшение числа подклассов. Паттерн фабричный метод часто порождает иерархию классов Creator, параллельную иерархии классов продуктов.

Прототип позволяет клонировать прототип, а не запрашивать фабричный

метод создать новый объект. Поэтому иерархия класса Creator становится вообще ненужной. Это преимущество касается главным образом языков типа C++, где классы не рассматриваются как настоящие объекты. В языках же типа Smalltalk и Objective С это не так существенно, поскольку всегда можно использоватьобъект-класс в качестве создателя. В таких языках объекты-классы уже выступают как прототипы;

- динамическое конфигурирование приложения классами. Некоторые среды позволяют динамически загружать классы в приложение во время его выполнения. Паттерн прототип - это ключ к применению таких возможностей в языке типа C++.

Для таких приложений характерны паттерны компоновщик и декоратор.

Приложение, которое создает экземпляры динамически загружаемого класса, не может обращаться к его конструктору статически. Вместо этого исполняющая среда автоматически создает экземпляр каждого класса в момент его загрузки и регистрирует экземпляр в диспетчере прототипов (см. раздел ≪Реализация≫). Затем приложение может запросить у диспетчера прототипов экземпляры вновь загруженных классов, которые изначально не были связаны с программой. Каркас приложений ЕТ++ [WGM88] в своей исполняющей среде использует именно такую схему.

Основной недостаток паттерна прототип заключается в том, что каждый подкласс класса Prototype должен реализовывать операцию Clone, а это далеко не всегда просто. Например, сложно добавить операцию Clone, когда рассматриватмые классы уже существуют. Проблемы возникают и в случае, если во внутреннем представлении объекта есть другие объекты или наличествуют круговые ссылки.

Реализация

Прототип особенно полезен в статически типизированных языках вроде C++, где классы не являются объектами, а во время выполнения информации о типе достаточно или нет вовсе. Меньший интерес данный паттерн представляет для "аких языков, как Smalltalk или Objective С, в которых и так уже есть нечто эквивалентное прототипу (именно - объект-класс) для создания экземпляров каждо::≫ класса. В языки, основанные на прототипах, например Self [US87], где создание любого объекта выполняется путем клонирования прототипа, этот паттерн просто встроен.

 

Рассмотрим основные вопросы, возникающие при реализации прототипов:

- использование диспетчера прототипов. Если число прототипов в системе не фиксировано (то есть они могут создаваться и уничтожаться динамически), ведите реестр доступных прототипов. Клиенты должны не управлять прототипами самостоятельно, а сохранять и извлекать их из реестра. Клиент запрашивает прототип из реестра перед его клонированием. Такой реестр мы будем называть диспетчером прототипов.

Диспетчер прототипов - это ассоциативное хранилище, которое возвращает прототип, соответствующий заданному ключу. В нем есть операции для регистрации прототипа с указанным ключом и отмены регистрации. Клиенты могут изменять и даже ≪просматривать≫ реестр во время выполнения, а значит, расширять систему и вести контроль над ее состоянием без написания кода;

- реализация операции Clone. Самая трудная часть паттерна прототип - правильная реализация операции Clone. Особенно сложно это в случае, когда в структуре объекта есть круговые ссылки.

В большинстве языков имеется некоторая поддержка для клонирования объектов. Например, Smalltalk предоставляет реализацию копирования, которую все подклассы наследуют от класса Object. В C++ есть копирующий конструктор. Но эти средства не решают проблему глубокого и поверхностного копирования [GR83]. Суть ее в следующем: должны ли при клонировании объекта клонироваться также и его переменные экземпляра или клон просто разделяет с оригиналом эти переменные?

Поверхностное копирование просто, и часто его бывает достаточно. Именно такую возможность и предоставляет по умолчанию Smalltalk. В C++ копирующий конструктор по умолчанию выполняет почленное копирование, то есть указатели разделяются копией и оригиналом. Но для клонирования прототипов со сложной структурой обычно необходимо глубокое копирование, поскольку клон должен быть независим от оригинала. Поэтому нужно гарантировать, что компоненты клона являются клонами компонентов прототипа. При клонировании вам приходится решать, что именно может разделяться и может ли вообще.

Если объекты в системе предоставляют операции Save (сохранить) и Load (загрузить), то разрешается воспользоваться ими для реализации операции Clone по умолчанию, просто сохранив и сразу же загрузив объект. Операция Save сохраняет объект в буфере памяти, a Load создает дубликат, реконструируя объект из буфера;

а инициализация клонов. Хотя некоторым клиентам вполне достаточно клона как такового, другим нужно инициализировать его внутреннее состояние полностью или частично. Обычно передать начальные значения операции Clone невозможно, поскольку их число различно для разных классов прототипов. Для некоторых прототипов нужно много параметров инициализации, другиевообще ничего не требуют. Передача Clone параметров мешает построению единообразного интерфейса клонирования.

Может оказаться, что в ваших классах прототипов уже определяются операции для установки и очистки некоторых важных элементов состояния.

Если так, то этими операциями можно воспользоваться сразу после клонирования. В противном случае, возможно, понадобится ввести операцию Initialize (см. раздел ≪Пример кода≫), которая принимает начальные значения в качестве аргументов и соответственно устанавливает внутреннее состояние клона. Будьте осторожны, если операция Clone реализует глубокое копирование: копии может понадобиться удалять (явно или внутри Initialize) перед повторной инициализацией.

Пример кода

Мы определим подкласс MazePrototypeFactory класса MazeFactory.

Этот подкласс будет инициализироваться прототипами объектов, которые ему предстоит создавать, поэтому нам не придется порождать подклассы только ради изменения классов создаваемых стен или комнат.

 

MazePrototypeFactory дополняет интерфейс MazeFactory конструктором, принимающим в качестве аргументов прототипы:

 

class MazePrototypeFactory : public MazeFactory {

public:

MazePrototypeFactory(Maze*, Wall*, Room*, Door*);

virtual Maze* MakeMaze() const;

virtual Room* MakeRoom(int) const;

virtual Wall* MakeWalK) 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;

}

 

Функции-члены для создания стен, комнат и дверей похожи друг на друга:

каждая клонирует, а затем инициализирует прототип. Вот определения функций ..'akeWall и MakeDoor:

 

Wall* MazePrototypeFactory::MakeWall () const {

return _prototypeWall->Clone();

}

Door* MazePrototypeFactory::MakeDoor (Room* rl, Room *r2) const {

Door* door = _prototypeDoor->Clone();

door->Initialize(rl, r2);

return door;

}

 

Мы можем применить MazePrototypeFactory для создания прототипичного или принимаемого по умолчанию лабиринта, просто инициализируя его прототипами базовых компонентов:

 

MazeGame game;

MazePrototypeFactory simpleMazeFactory(

new Maze, new Wall, new Room, new Door

);

Maze* maze = game.CreateMaze(simpleMazeFactory);

 

Для изменения типа лабиринта инициализируем MazePrototypeFactory

другим набором прототипов. Следующий вызов создает лабиринт с дверью типа BombedDoor и комнатой типа RoomWithABomb:

 

 

MazePrototypeFactory bombedMazeFactory(

new Maze, new BombedWall,

new RoomWithABomb, new Door

);

Порождающие паттерны

Объект, который предполагается использовать в качестве прототипа, напри-

мер экземпляр класса Wall, должен поддерживать операцию Clone. Кроме того.

у него должен быть копирующий конструктор для клонирования. Также может

потребоваться операция для повторной инициализации внутреннего состояния.

Мы добавим в класс Door операцию Initialize, чтобы дать клиентам возмож-

ность инициализировать комнаты клона.

Сравните следующее определение Door с приведенным на стр. 91:

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* _rooml;

Room* _room2;

};

Door::Door (const Door& other) {

_rooml = other._rooml;

_room2 = other._room2;

}

void Door::Initialize (Room* rl, Room* r2) {

_rooml = rl;

_room2 = r2;

}

Door* Door::Clone () const {

return new Door(*this);

}

Подкласс BombedWall должен заместить операцию Clone и реализовать ее

ответствующий копирующий конструктор:

class BombedWall : public Wall {

public:

BombedWall();

BombedWall(const BombedWallk);

virtual Wall* Clone() const;

bool HasBomb();

private:

bool _bomb;

};

Паттерн Prototype

BombedWall::BombedWall (const BombedWallk other) : Wall(other) {

_bomb = other._bomb;

}

Wall* BombedWall::Clone () const {

return new BombedWall(*this);

}

Операция BombedWall: : Clone возвращает Wall*, а еереализация - указа-

тель на новый экземпляр подкласса, то есть BombedWal 1 *. Мы определяем Clone

в базовом классе именно таким образом, чтобы клиентам, клонирующим прото-

тип, не надо было знать о его конкретных подклассах. Клиентам никогда не при-

дется приводить значение, возвращаемое Clone, к нужному типу.

В Smalltalk разрешается использовать стандартный метод копирования, уна-

следованный от класса Object, для клонирования любого прототипа MapSite.

Можно воспользоваться фабрикой MazeFactory для изготовления любых необ-

ходимых прототипов. Например, допустимо создать комнату по ее номеру #room.

В классе MazeFactory есть словарь, сопоставляющий именам прототипы. Его

метод make: выглядит так:

k

make: partName

^ (partCatalog at: partName) copy

Имея подходящие методы для инициализации 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

| rooml room2 |

rooml := (aFactory make: #room). location: 1@1.

room2 := (aFactory make: #room) location: 2@1.

door := (aFactory make: #door) from: rooml to: room2.

rooml

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: rooml;

addRoom: room2;

yourself

Известные применения

Быть может, впервые паттерн прототип был использован в системе Sketchpad

Ивана Сазерленда (Ivan Sutherland) [Sut63]. Первым широко известным приме-

нением этого паттерна в объектно-ориентированном языке была система Thing-

Lab, в которой пользователи могли сформировать составной объект, а затем пре-

вратить его в прототип, поместив в библиотеку повторно используемых объектов

[Вог81]. Ад ель Голдберг и Давид Робсон упоминают прототипы в качестве пат-

тернов в работе [GR83], но Джеймс Коплиен [Сор92] рассматривает этот вопрос

гораздо шире. Он описывает связанные с прототипом идиомы языка C++ и при-

водит много примеров и вариантов.

Etgdb - это оболочка отладчиков на базе ЕТ++, где имеется интерфейс вида

point-and-click (укажи и щелкни) для различных командных отладчиков. Для

каждого из них есть свой подкласс DebuggerAdaptor. Например, GdbAdaptor

настраивает etgdb на синтаксис команд GNU gdb, a SunDbxAdaptor - на отлад-

чик dbx компании Sun. Набор подклассов DebuggerAdaptor не ≪зашит≫ в etgdb.

Вместо этого он получает имя адаптера из переменной среды, ищет в глобальной

таблице прототип с указанным именем, а затем его клонирует. Добавить к etgdb

новые отладчики можно, связав ядро с подклассом DebuggerAdaptor, разрабо-

танным для этого отладчика.

Библиотека приемов взаимодействия в программе Mode Composer хранит

прототипы объектов, поддерживающих различные способы интерактивных отно-

шений [Sha90]. Любой созданный с помощью Mode Composer способ взаимодей-

ствия можно применить в качестве прототипа, если поместить его в библиотеку.

Паттерн прототип позволяет программе поддерживатьнеограниченное число ва-

риантов отношений.

Пример музыкального редактора, обсуждавшийся в начале этого раздела, ос-

нован на каркасе графических редакторов Unidraw [VL90].

Родственные паттерны

В некоторых отношениях прототип и абстрактная фабрика являются кон-

курентами. Но их используют и совместно. Абстрактная фабрика может хранить

набор прототипов, которые клонируются и возвращают изготовленные объекты.

В тех проектах, где активно применяются паттерны компоновщик и декора-

тор, тоже можно извлечь пользу из прототипа.


Паттерн Singleton