А. Александреску - Современное проектирование на C++ (1119444), страница 54
Текст из файла (страница 54)
Классы не имеют сштуса; их нельзя копировать, хранить в переменно1( или возврашать из функции. Сушествуют языки, в которых классы являются обьекглами. В этих языках некоторые объекты с определенными свойствами по умолчанию рассматриваются как классы. Следовательно, в этих языках во время выполнения программы моэсно создавать новые классы, копировать их, хранить в переменных и т.д. Если бы язык С++ был одним из таких языков, можно было бы написать примерно такой код.
// предупреждение — зто ня сч-+ // Будем считать, что с1азэ — это класс и объект одновременно с1аэз яеао(сопят спаг* тз1енаве); оосцвепт* посцвепсмапайег:".ореппосцвепс(сопле сваг* Р11енаве) ( с1аэз спес1аээ = яеао(г(1енаве) посовепс* рпос = пев гпес1азэ; Таким образом, переменную типа с1аээ можно было бы передавать оператору пев. В рамках этой парадигмы передача известного имени класса оператору пев ничем не отличалась бы от явного указания константы, В таких динамических языках за счет потери гибкости достигнут определенный компромисс между типовой безопасностью и эффективностью, поскольку статическая типизация — это важный компонент оптимизации кода, В языке С++ принят прямо противоположный подход — опора на статическую систему типов, позволяюшая достигать максимальной гибкости.
Итак, создание фабрик объектов в языке С++ представляет собой сложную задачу. В языке С++ между типами и значениями лежит пропасть: значение имеет тип, но 220 Часть й. Компоненты тип не может существовать сам по себе. Для создания объекта чисто динамическим способом нужно иметь средства для выражения "чистого" типа, передачи его во внешнюю среду и создания значения на основе этой информации. Поскольку это невозможно„нужно как-то выразить типы с помощью объектов — целых чисел, строк и т.п. Затем следует придумать способы идентификации типов и создания на их основе соответствующих объектов. Формула объект-тип-объект лежит в основе создания фабрик объектов в языках со статической типизацией.
Объект, идентифицирующий тип, мы будем называть аденто4икатором тола (суре (денс(бег) (Не пугайте его с ключевым словом сурезд.) Идентификатор типа позволяет фабрике объектов создавать соответствующий тип. Как мы увидим впоследствии, иногда идентификатор типа можно создавать, ничего не зная о том, что у пас есть и что мы получим. Это похоже на сказку: вы не знаете точно, что означает заклинание (и пытаться это узнать бывает небезопасно), но произносите его, и волшебник возвращает вам полезную вещь.
Детали этого превращения знает только волшебник..., т.е. фабрика. Мы построим простую фабрику, решающую конкретную задачу, рассмотрим ее разные реализации, а затем выделим ее обобщенную часть в шаблонный класс. 8.3. Реализация фабрики объектов Допустим, что мы создаем приложение для рисования, позволяющее редактировать простые векторные рисунки, состоящие из линий, окружностей, многоугольников и т.п.' В рамках классической объектно-ориентированной парадигмы мы определяем абстрактный кпасс 5(заре, из которого будут выведены все остальные фигуры.
с1азз 5(саре ( рц(з)з с: чз гсца1 чо(д пгавО сопят = О; чз гтца1 чо(д яосасе(даосе апд1е) = О; чз гсца1 чо(д асов(доцЫе аоовяассог) = О; Затем можно определить класс Огавзпд, содержащий сложный рисунок. По сушеству, этот класс хранит набор указателей на объекты класса 5(заре, т.е. список, вектор или иерархическую структуру, и обеспечивает выполнение операций рисования в целом. Разумеется, нам понадобится сохранять рисунки в файл и загружать их оттуда. Сохранить фигуру просто: нужно лишь предусмотреть виртуальную функцию 5(саре::5аче(згд::овсгеавй).
Операция Огавзпд::5аче может выглядеть следующим образом. с1азз огав1пд ( рцЫзс: чо1д 5аче(зсд::оЯзсгеавй оися11е); чо(д ьоад(зсд::1Рзсгеавй зпяз1е); чозд пгавзпд::5аче(зсд::оРзсгеавй оцср(1е) ' Эта разработка в духе "Злравствуй, мир!" — хорошая основа лля проверки знания языка С++. Многие пробовали в ней разобраться, но лишь некоторые из них поняли, как объект загружается из файла, что, в общем-то, не самое важное, 221 Глава 8. Фабрики объектов записываем заголовок рисунка Еог (для каждого элемента рисунка) (текущий элелсент)->5аче(оитг(1е); ) Описанный выше пример часто встречается в книгах, посвяшенных языку С++„ включая классическую книгу Б.
Страуструпа (Ягоивхгар, 1997). Однако в большинстве книг нс рассматривается операция загрузки рисунка из файла, потому что она разру- шает целостность этой прекрасной на вид модели. Углубление в детали загрузки ри- сунка заставило бы авторов написать большое количество скобок, нарушив гармонию. С другой стороны, именно эту операцию мы хотим реализовать, поэтому нам придет- ся засучить рукава. Проше всего потребовать, чтобы в начале каждого объекта класса, производного от класса 5иаре, хранился целочисленный уникальный идентификатор.
Тогда код лля считывания рисунка из файла выглядел бы следуюшим образом. // уникальный хо для каждого типа изображаемого объекта паюеврасе оган(пцтуре сопвх (пт ЕХМЕ = 1, Ростбон = 2, СХЯСЕЕ = 3 чозд Огаедпц: гьоад(втд::зрвтгеаюй (пр(1е) ( // обработка ошибок для простоты пропускается иЬ(1е (з'пР(1е) ( // Считываем тип объекта (пт дгам(пцтуре; (пез1е » дгаедпцтуре; // Создаем новый пустой объект 5Ьаре" рСиггептОЬ)ест; вкдтсЬ (дгаедпцтуре) ( ив(пц паюеврасе огаедпцтуре; саве ЕХме: рСиггептОЬ)ест = пои езпе; Ьгеак; саве Роьчеом; рСиггептОЬ)ест = пеи Ро1уцоп; Ьгеай; саве схясье: рСиггептОЬ)ест = пеи Сзгс1е; Ьгеак; дераи1т: обработка ошибки - - неизвестныи тип обьекта // считываем содержимое объекта, // вызывая виртуальную функцию Рп рсиггептОЬ)ест->яеад(зпР(1е); добавллел~ обьект в контейнер Часть й.
Компоненты Это настоящая фабрика объектов. Она считывает из файла идентификатор типа, основываясь на этом идентификаторе, создает объект соответствуюшего типа и вызывает виртуальную функцию, загружающую этот обьект из файла. Одно плохо: все это нарушает важные правила объектно-ориентированного программирования. ° Выполняется оператор зи1тсй, основанный на метке типа со всеми вытекаю- шими отсюда последствиями.
Именно это категорически запрешено в объектно-ориентированных программах. ° В одном файле концентрируется информация обо всех классах, производных от класса 5паре, что также нежелательно. Файл реализации функции пгаи- 5 пд:: 5аче вынужден содержать все заголовки всех возможных фигур. Это делает его уязвимым и зависимым от компилятора.
° Класс трудно расширить. Представьте себе, что нам понадобилось добавить новую фигуру, скажем, класс д11)рзе. Кроме создания нового класса, прилется добавить новую целочисленную константу в пространство имен пгаизпдтуре, записать ее при сохранении объекта класса д111рзе и добавить новую метку в оператор зи)тсб внутри функции-члена пгааб пд:: 5аче. И все это ради одной- единственной функции! Попробуем создать фабрику объектов, лишенную указанных недостатков. Для этого придется отказаться от использования оператора заб тсп, чтобы мы могли выполнять операции по созданию объектов классов ьб пе, яо1 удоп и с) гс1е единообразно.
Дяя того чтобы связать между собой фрагменты кода, лучше всего использовать указатели на функции, описанные в главе 5. Фрагмент настраиваемого кода (каждый из элементов оператора зи1 тсб) можно абстрагировать с помошью следуюшей сигнатуры. 56аре* СгеатеСопсгете56ареО; Фабрика хранит набор указателей на функции вместе с сигнатурами. Кроме того, нужно установить соответствие между идентификатором типа и указателем на функцию, создаюшую объект. Таким образом, нам нужен ассоциативный массив (шар), предоставляюший доступ к соответствующей функции по идентификатору типа (именно то, что делал оператор затей). Кроме того, этот массив гарантирует масштабируемость, чего оператор зи1ссб с его фиксированной статической структурой обеспечить не может. Во время выполнения программы ассоциативный массив может возрастать — мы можем динамически добавлять в него новые элементы (кортежи, состояшие из идентификаторов типов и указателей на функции), а именно зто нам и нужно.