Гради Буч - Объектно-ориентированный анализ и проектирование с примерами приложений на С++ (1158635), страница 67
Текст из файла (страница 67)
Класс Queue содержит толькодекларации о друзьях:friend class QueueActiveIterator<Item>;friend class Queuepassivelterator<ltem>;Как мы увидим в дальнейшем, эти объявления друзей понадобятся дляподдержки идиом итератора.Семантика времени и памятиИз пяти основных принципов строения библиотеки базовых классов,возможно, наиболее важен механизм, обеспечивающий клиентаальтернативной простанственно-временной семантикой внутри каждогосемейства классов.Рассмотрим тот спектр требований, который должен учитываться приразработке библиотеки общего назначения. На рабочей станции, обладающейбольшим виртуальным адресным пространством, пользователь скорее всегобудет расточать память ради более высокого быстродействия.
С другойстороны, в некоторых встроенных системах, таких, как спутник илиавтомобильный мотор, ресурсы памяти часто ограничены, и разработчиквынужден выбирать в качестве рабочих те абстракции, которые используютменьше памяти (например, выделяя место под данные в стеке, а не в "куче").Ранее мы различили эти две возможности как ограниченную инеограниченную формы соответственно.Неограниченные формы применимы в тех случаях, когда размерструктуры не может быть предсказан, а выделение и утилизация памяти изкучи не приводит ни к потере времени, ни к снижению надежности (как этобывает в некоторых приложениях, критичных по времени).23 Ограниченныеформы лучше подходят для работы с небольшими структурами, размеркоторых достаточно хорошо предсказуем.
Учтем также, что динамическоевыделение памяти менее терпимо к ошибкам программиста.Таким образом, все структуры данной библиотеки должныприсутствовать в альтернативных вариантах; поэтому нам придется создатьдва низкоуровневых класса поддержки, Unbounded (неограниченный) иBounded (ограниченный). Задачей класса unbounded является поддержкабыстро работающего связного списка, элементы которого размещаются впамяти, выделенной из "кучи". Это представление эффективно по скорости, ноне по памяти, так как каждый элемент списка должен, кроме своего значения,дополнительно содержать указатель на следующий элемент того же списка.Задача класса Bounded состоит в организации структур на базе массива, чтоэффективно с точки зрения памяти, но добиться большой производительноститрудно, так как, например, при добавлении элемента в середину спискаприходится последовательно копировать все последующие (или предыдущие)элементы массива.Как видно из рис.
9-4, для включения этих классов нижнего уровня виерархию основных абстракций мы используем агрегацию. Более точно,диаграмма показывает, что мы используем физическое включение позначению с защищенным Доступом, которое означает, что это низкоуровневоепредставление доступно только подклассам и друзьям. На раннем этапепроектирования мы хотели воспользоваться примесями и сделать unboundedи Bounded защищенными суперклассами.Рис.
9-4. Ограниченная и неограниченная формыМы в конце концов отказались от такого варианта, так как ондостаточно труден для понимания, и к тому же нарушает лакмусов принципнаследования: BoundedQueue, по крайней мере, с точки зрения типа данных,не является частным случаем класса Bounded.Отметим также, что работа с двумя формами требует присутствиявторого аргумента в их шаблоне. Для ограниченной формы - это беззнаковоецелое число Size, обозначающее статический размер объекта.
Длянеограниченной формы - это класс StorageManager, ответственный заполитику размещения в памяти. Мы рассмотрим его работу в следующемразделе.Протокол обоих классов поддержки должен быть, с одной стороны,достаточным для обеспечения работы конкретных подклассов, а с другойстороны, универсальным, чтобы гарантировать выполнение ответственностивсех других структур в библиотеке. В целях компактности и быстродействиямы не включили в описание классов Unbounded и Bounded ни однойвиртуальной функции.
По этой причине мы не можем объединить их однимсуперклассом, несмотря на то, что они имеют общий протокол; кроме того, мыне можем надлежащим образом построить на их базе иерархию подклассов. Вданном случае гибкость приносится в жертву производительности. По той жепричине мы решаем сделать ряд функций встроенными;хорошими кандидатами на это обычно являются селекторы, особенноте, которые возвращают простые переменные.Рассмотрим, например, описание класса Bounded:template<class Item, unsigned int Size>class Bounded {public:Bounded();Bounded(const Bounded<Item, Size>&);~Bounded();Bounded< Item, Size>& operator=(const Bounded<Item,Size>&);int operator==(const Bounded<Item, Size>&) const;int operator!=(const Bounded<Item, Size>&) const;const Item& operator[](unsigned int index) const;Item& operator[](unsigned int index);void clear();void insert(constItem&);void insert(constItem&, unsigned int before);void append(constItem&);void append(constItem&, unsigned int after);void remove(unsigned int at);void replace(unsigned int at, const Item&);unsigned int available() const;unsigned int length() const;const Item& first() const;const Item& last() const;const Item& itemAt(unsigned int) const;Item& itemAt(unsigned int);int location(const Item&) const;static void* operator new(size_t);static void operator delete(void*, size_t);protected:Item rep[Size] ;unsigned int start;unsigned int stop;unsigned int expandLeft(unsigned int from);unsigned int expandRight(unsigned int from);void shrinkLeft(unsigned int from);void shrinkRight(unsigned int from);};Объявление класса следует схеме, описанной ранее.
Каким образом мыпришли именно к такому решению? Если честно, то на 80% это результатчистого проектирования классов, которое рассматривалось в главе 6. Затеминтерфейс дорабатывался в соответствии с результатами пробногоиспользования класса совместно с рядом основных абстракций системы.Основная трудность при эволюции состояла в идентификации подходящихпримитивных операций, которые должны использоваться при работе снабором различных структур.Сердцем класса является защищенный массив rep постоянногоразмера Size. Рассмотрим следующее объявление:Bounded<char, 100U> charSequence;При создании соответствующего объекта в стеке образуется массивпостоянного размера из 100 элементов. Защищенные члены класса start иstop (индексы в этом массиве) указывают начало и конецпоследовательности.
Тем самым мы использовали кольцевой буфер данных.Добавление нового элемента в начало или в конец последовательности непотребует перемещения данных, а добавление элемента в середину массиваприводит к копированию не более чем половины его элементов.Проектирование ограниченного и неограниченного классов поддержкизатрагивает также некоторые тонкие вопросы, касающиеся использованияссылок (мы упоминали о них в главе 3). Нам придется еще раз коснуться этойтемы, и не только потому, что она имеет прямое отношение к разработкеинтерфейса параметризованных классов, но и потому, что данные вопросысами по себе представляют значительный интерес для проектировщика любойболее или менее нетривиальной библиотеки.В C++ ссылки являются механизмом, позволяющим улучшитьпроизводительность. Однако пользоваться ими следует предельно осторожново избежание нарушения корректного доступа к оперативной памяти.
Вданной библиотеке мы используем ссылки для ускорения работы при передачеаргументов функциям-членам. Это касается, например, класса Bounded, гдеподобным образом передаются ссылки на объекты классов Bounded и Item.Ссылки, как правило, не используются для передачи примитивных объектов(например, целых чисел в описании функции-члена itemAt) - программа отэтого будет работать только медленнее. Кроме того, семантика языка C++порождает некоторые опасности при манипулировании с временнымиобъектами.Все наши структуры, однако, содержат в качестве элементов нессылки, а значения, что исключает возникновение ссылок на временныеобъекты в стеке при работе программы.
По той же причине мы отказались отхранения указателей на элементы структур, так как это вызывает крайненестабильное поведение системы при инстанцировании шаблона встроеннымитипами данных. Подобные вопросы чрезвычайно существенны припроектировании сред разработки, включающих в себя параметризованныеклассы, так как пользователь может инстанцировать шаблон произвольнымтипом данных. При использовании ссылок существуют, вообще говоря, трислучая, и нам придется при создании библиотеки постараться найтиопределенный баланс между ними.Во-первых, встроенные типы данных можно без труда передавать поссылке и копировать. Объявив типы аргумента постоянными ссылками, можноизбежать неприятностей, связанных с появлением временных структур,возникающих при приведении типов [12].Во-вторых, типы данных, определенные пользователем, также можнопередавать по ссылке и копировать, но только в том случае, когда для нихопределены копирующий конструктор и оператор присваивания.
Ссылкиможно использовать в полиморфных операциях (передавая объектпроизводного класса вместо объявленного при инстанцировании), нокопирование не будет полиморфным. В результате объект будет "срезан" доразмеров своего базового класса.В-третьих, при полиморфном использовании библиотеки встретитсяинстан-цирование шаблонов указателями на базовый класс. Хотя передачауказателей по ссылке может и не улучшить производительность, нокопирование указателей в представление сохраняет полиморфизмпроизводных объектов.Например, для класса BoundedQueue мы можем написать следующее:class Event ...
typedef Event* EventPtr;BoundedQueue<int, 10U> intQueue;BoundedQueue<Event, 50U> eventQueuel;BoundedQueue<EventPtr, 100U> eventQueue2;С помощью объекта класса eventQueuel можно спокойно создаватьочереди событий, однако при добавлении в очередь экземпляра любогоподкласса Event произойдет "срезка", и полиморфное поведение такогоэкземпляра будет потеряно. С другой стороны, объект класса eventQueue2содержит указатели на объекты класса Event, поэтому проблема "срезки" невозникает.Наше решение, касающееся хранения внутри структур значений, а нессылок, предъявляет определенные требования к конструкторам идеструкторам элементов.
В частности, классы, используемые дляинстанцирования структуры, должны, по крайней мере, иметь конструктор поумолчанию, копирующий конструктор и оператор присваивания. Кроме того,в некоторых случаях элементы не могут быть уничтожены сразу послеудаления из структуры. В ограниченной форме, например, элементы(хранящиеся в массивах) не уничтожаются до уничтожения всей структуры.Посмотрим, как можно использовать класс Bounded приформировании конкретного класса BoundedQueue. Отметим, что абстракцияBoundedQueue содержит защищенный элемент rep класса Bounded.template<class Item, unsigned int Size>class BoundedQueue : public Queue<Item> {public:BoundedQueue();BoundedQueue(const BoundedQueue< Item, Size>&);virtual ~BoundedQueue() ;virtual Queue<Item>& operator=(const Queue<Item>&);virtual Queue<Item>& operator=(const BoundedQueue<Item,Size>&) ;virtual int operator==(const Queue<Item>&) const;virtual int operator" (const BoundedQueue<Item, Size>&)const;int operator!=(const BoundedQueue< Item, Size>&) const;virtual void clear();virtual void append(const Item&);virtual void pop();virtual void remove(unsigned int at);virtual unsigned int available() const;virtual unsigned int length() const;virtual int isEmpty() const;virtual const Item& front() const;virtual int location(const Item&) const;protected:Bounded< Item, Size> rep;virtual void purge();virtual void add(const Item&);virtual unsigned int cardinality() const;virtual const Item& itemAt(unsigned int) const;static void* operator new(size_t);static void operator delete(void*, size_t);};Основная задача данного класса - завершить протокол, определенныйв базовом классе.