Анимация
JavaScript


Главная  Библионтека 

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 [ 68 ] 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105

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

Объект, идентифицирующий тип, мы будем называть идентификатором типа (type identifier) (Не путайте его с ключевым словом typeid.) Идегтгификатор типа позволяет фабрике объектов создавать соответствующий тип. Как мы увидим впоследствии, иногда идентификатор типа можно создавать, ничего не зная о том, что у йас есть и что мы получим. Это похоже на сказку: вы не знаете точно, что означает заклинание (и пытаться это узнать бывает небезопасно), но произносите его, и волшебник возвращает вам полезную вещь. Детали этого превращения знает только волшебник..., т.е. фабрика.

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

8.3. Реализация фабрики объектов

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

class Shape {

public:

virtual void DrawC) const = 0; virtual void RotateCdouble angle) = 0; virtual void ZoomCdouble zoomFactor) = 0;

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

Сохранить фигуру просто: нужно лишь предусмотреть виртуальную функцию Shape: :SaveCstd: :ostream&). Операция Drawing: iSave может выглядеть следующим образом.

class Drawing

public:

void SaveCstd::ofstream* outFile); void LoadCstd::ifstream& inFile);

void Drawing::SaveCstd::ofstream& outFile)

Эта разработка в духе "Здравствуй, мир!" - хорошая основа для проверки знания языка С++. Многие пробовали в ней разобраться, но лишь некоторые из них поняли, как объект загружается из файла, что, в общем-то, не самое важное.



записываем заголовок рисунка

for (.для каждого элемента рисунка

(текущий элемент)->SaveCoutF-\l;

Описанный выше пример часто встречается в книгах, посвященных языку С++, включая классическую книгу Б. Страуструпа (Stroustrup, 1997). Однако в большинстве книг не рассматривается операция зафузки рисунка из файла, потому что она разрушает целостность этой прекрасной на вид модели. Углубление в детали зафузки рисунка заставило бы авторов написать большое количество скобок, нарушив гармонию. С другой стороны, именно эту операцию мы хотим реализовать, поэтому нам придется засучить рукава. Проще всего потребовать, чтобы в начале каждого объекта класса, производного от класса Shape, хранился целочисленный уникальный идентификатор. Тогда код для считывания рисунка из файла выглядел бы следующим образом.

Уникальный id для каждого типа изображаемого объекта namespace Drawnпдтуре

const int LINE = 1, POLYGON =2, CIRCLE = 3

void Drawing:rLoadCstd::ifstream& inFile) {

Обработка ошибок для простоты пропускается

while (inFile)

Считываем тип объекта int drawingType; inFile » drawingType;

Создаем новый пустой объект Shape* pCurrentObject; switch (drawingType)

using namespace DrawingType; case LINE:

pCurrentObject = new Line;

break; case POLYGON:

pCurrentObject = new Polygon;

break; case circle:

pCurrentObject = new Circle;

break; default:

обработка ошибки - неизвестный тип объекта

Считываем содержимое объекта, вызывая виртуальную функцию fn pCurrentObject->Read(inFile); добавляем объект в контейнер



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

• Выполняется оператор switch, основанный на метке типа со всеми вытекающими отсюда последствиями. Именно это категорически запрещено в объектно-ориентированных профаммах.

• В одном файле конценфируется информация обо всех классах, производных от класса shape, что также нежелательно. Файл реализации функции Drawing:: Save вынужден содержать все заголовки всех возможных фигур. Это делает его уязвимым и зависимым от компилятора.

• Класс трудно расширить. Представьте себе, что нам понадобилось добавить новую фигуру, скажем, класс Ellipse. Кроме создания нового класса, придется добавить новую целочисленную константу в просфанство имен DrawingType, записать ее при сохранении объекта класса Ellipse и добавить новую метку в оператор switch внуфи функции-члена Drawing: :Save. И все это ради одной-единственной функции!

Попробуем создать фабрику объектов, лишенную указаннъгх недостатков. Для этого придется отказаться от использования оператора switch, чтобы мы могли выполнять операции по созданию объектов классов Line, Polygon и Ci rcle единообразно.

Для того чтобы связать между собой фрагменты кода, лучше всего использовать указатели на функции, описанные в главе 5. Фрагмент насфаиваемого кода (каждый из элементов оператора swi tch) можно абсфагировать с помощью следуюшей сигнатуры.

Shape* CreateConcreteShapeC);

Фабрика хранит набор указателей на функции вместе с сигнатурами. Кроме того, нужно установить соответствие между идентификатором типа и указателем на функцию, создающую объект. Таким образом, нам нужен ассоциативный массив (тар), предоставляющий доступ к соответствующей функции по идентификатору типа (именно то, что делал оператор switch). Кроме того, этот массив гарантирует масштабируемость, чего оператор switch с его фиксированной статической структурой обеспечить не может. Во время выполнения программы ассоциативный массив может возрастать - мы можем динамически добавлять в него новые элементы (кортежи, состоящие из идентификаторов типов и указателей на функции), а именно это нам и нужно. Можно начать с пустого массива, а затем каждый объект класса, производного от класса shape, добавит в него новый элемент.

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

Начнем с разработки класса shapeFactory, предназначенного для создания всех объектов классов, производных от класса Shape. В реализации класса ShapeFactory мы будем использовать ассоциативный массив из стандартной библиотеки std: :map.

class ShapeFactory {



0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 [ 68 ] 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105