Гради Буч - Объектно-ориентированный анализ и проектирование с примерами приложений на С++ (1158635), страница 69
Текст из файла (страница 69)
Рис. 9-7 иллюстрирует их модульную архитектуру.Мы выбрали именно такую схему, чтобы изолировать классы, которые, повидимому, будут чаще всего подвергаться изменениям.Рис. 9-7. Модули управления памятьюИсключенияНесмотря на то, что язык C++ можно заставить соблюдать многиестатические предположения (нарушение которых повлечет ошибкукомпиляции), для выявления динамических нарушений (таких, как попыткадобавить элемент к полностью заполненной ограниченной очереди илиудалить элемент из пустого списка) приходится использовать и другиемеханизмы. В данной библиотеке используются средства обработкиисключений, предоставляемые C++ [14].
Наша архитектура включает в себяиерархию классов исключений и, отдельно от нее, ряд механизмов повыявлению таких ситуаций.Начнем с базового класса Exception (исключение), обладающегонесложным протоколом:class Exception {public:Exception(const char* name, const char* who, const char*what);void display() const;const char* name() const;const char* who() const;const char* what() const;protected:…};Каждой особой ситуации можно сопоставить имя ее источника ипричину возникновения.
Кроме того, мы можем обеспечить скрытые отклиентов средства для вывода информации об ошибке в соответствующийпоток.Анализ различных классов нашей библиотеки подсказываетвозможные типы исключений, которые можно оформить в виде подклассовбазового класса Exception:•••••••••••••ContainerErrorDuplicateIllegalPatternIsNullLexicalErrorMathErrorNotFoundNotNullNotRootOverflowRangeErrorStorageErrorUnderflowОбъявление класса overflow (переполнение) может выглядетьследующим образом:class Overflow : public Exception {public:Overflow(const char* who, const char* what): Exception("Overflow", who, what) {}};Обязанность этого класса состоит лишь в знании своего имени,которое он передает конструктору суперкласса.В данном механизме функции-члены классов библиотеки тольковозбуждают исключения; они не в состоянии перехватить исключение,главным образом, потому, что ни одна из них не может осмысленноотреагировать на эту ситуацию.
По соглашению мы возбуждаем исключениепри нарушении условий, предполагавшихся относительно некоторогосостояния. Условие представляет собой обычное булевское выражение,которое должно быть истинным в нормальной ситуации. Чтобы упроститьбиблиотеку, мы ввели следующую функцию, не принадлежащую ни одному изклассов:inline void _assert(int expression,const Exception&exception){if (!expression)throw(exception);}Для эффективности мы определили эту функцию как встроенную.Преимущество подобной схемы состоит в том, что она локализует всеисключения (в C++ throw имеет синтаксис вызова функции).
Так, длятрансляторов, которые до сих пор не поддерживают исключений, можноиспользовать специальную директиву (-D для большинства трансляторов C++)для переопределения вызова throw в вызов другой функции-не-члена,выводящей сообщение на экран и останавливающей выполнение программы:void _catch(const Exception& e){cerr << "EXCEPTION: ";e.display();exit(l);}Рассмотрим реализацию функции insert класса Bounded:template<class Item, unsigned int Size>void Bounded<ltem, Size>::insert(const Item& item){unsigned int count = length();_assert((count < Size), Overflow("Bounded::Insert","structure is full"));if (!count)start = stop = 1;else{start--;if (!start)start = Size;}rep[start - 1] = item;}Предусмотрено, что в процессе выполнения функции проверяется, чторазмер структуры не превосходит максимально допустимого.
Если это не так,возбуждается исключение Overflow.Важнейшим преимуществом этого подхода является гарантия того, чтосостояние объекта, возбудившего исключение, не будет нарушено (не считаяслучая исчерпания оперативной памяти, когда уже в принципе ничего нельзяподелать). Любая функция, прежде чем произвести действия, способныеизменить состояние объекта, проверяет предположение.
В приведенной вышефункции insert, например, прежде, чем добавить элемент в массив, мысначала вызываем селектор (который не может вызвать изменения состоянияобъекта), затем проверяем все предусловия функции и лишь затем изменяемсостояние объекта. Мы скрупулезно придерживались подобного стиля приреализации всех функций и настоятельно советуем не отходить от него приконструировании подклассов, основанных на нашей библиотеке.Рис. 9-8 иллюстрирует схему взаимодействия классов,обеспечивающих реализацию механизма обработки исключений.Рис.
9-8. Классы обработки исключенийИтерацияИтерация - это еще один архитектурный шаблон нашей библиотеки. Вглаве 3 уже отмечалось, что итератор представляет собой операцию,обеспечивающую последовательный доступ ко всем частям объекта.Оказывается, такой механизм нужен не только пользователям, он необходим ипри реализации самой библиотеки, в частности, ее базовых классов.При этом перед нами стоял выбор: можно было определять итерациикак часть протокола объектов или создавать отдельные объекты,ответственные за итеративный опрос других структур. Мы выбрали второйподход по двум причинам:• Наличие выделенного итератора классов позволяет одновременнопроводить несколько просмотров одного и того же объекта.• Наличие итерационного механизма в самом классе нескольконарушает его инкапсуляцию; выделение итератора в качестве отдельногомеханизма поведения способствует достижению большей ясности в описаниикласса.Для каждой структуры определены две формы итераций.
Активныйитератор требует каждый раз от клиента явного обращения к себе дляперехода к следующему элементу. Пассивный итератор применяет функцию,предоставляемую клиентом, и, таким образом, требует меньшего участияклиента.1 Чтобы обеспечить безопасность типов, для каждой структурысоздаются свои итераторы.Рассмотрим в качестве примера активный итератор для класса Queue:template <class Item> class QueueActiveIterator {public:QueueActiveIterator(const Queue<Item>&);~QueueActiveIterator();Пассивный итератор реализует "применяемую" функцию.
Эта идиомаобычно используется в функциональных языках программирования.void reset();int next() ;int isDone() const;const Item* currentltem() const;protected:const Queue<Item>& queue;int index;};Каждому итератору в момент создания ставится в соответствиеопределенный объект. Итерация начинается с "верха" структуры, что бы этони значило для данной абстракции.С помощью функции currentItem клиент может получить доступ ктекущему элементу; значение возвращаемого указателя может быть нулевым вслучае, если итерация завершена или если массив пуст.
Переход кследующему элементу последовательности происходит после вызова функцииnext (которая возвращает О, если дальнейшее движение невозможно, какправило, из-за того, что итерация завершена). Селектор isDone служит дляполучения информации о состоянии процесса: он возвращает 0, если итерациязавершена или структура пуста. Функция reset позволяет осуществлятьнеограниченное количество итерационных проходов по объекту.Например, при наличии следующего объявления:BoundedQueue<NetworkEvent> eventQueue;фрагмент кода, использующий активный итератор для захода вкаждый элемент очереди, будет выглядеть так:QueueActiveIterator<NetworkEvent> iter(eventQueue);while (!iter.isDone()) {iter.currentItem()->dispatch();iter.next();}Итерационная схема, приведенная на рис. 9-9, иллюстрирует данныйсценарий работы и, кроме того, раскрывает некоторые детали реализацииитератора.
Рассмотрим их более подробно.Конструктор класса QueueActiveIterator сначала устанавливаетсвязь между итератором и конкретной очередью. Затем он вызываетзащищенную функцию cardinality, которая определяет количествоэлементов в очереди. Таким образом, конструктор можно описать следующимобразом:template<class Item>QueueActiveIterator<ltem>::QueueActiveIterator(constQueue<Item>& q):queue(q), index(q.cardinality() ? О : -1) {}Класс QueueActiveIterator имеет доступ к защищенной функцииcardinality класса Queue, поскольку числится в дружественных ему.Операция итератора isDone проверяет принадлежность текущегоиндекса допустимому диапазону, который определяется количествомэлементов очереди:template<class Item>Рис.
9-9. Механизм итерацииint{QueueActiveIterator<Item>::isDone() constreturn ((index < 0) II (index >= queue.cardinality()));}Функция currentItem возвращает указатель на элемент, на которомостановился итератор. Реализация итератора в виде индекса объекта в очередидает возможность в процессе итераций без труда добавлять и удалятьэлементы из очереди:template<class Item>const Item* QueueActiveIterator<Item>::currentItem () const{return isDone() ? 0 : &queue.itemAt(index);}При выполнении данной операции итератор снова вызываетзащищенную функцию очереди, на сей раз itemAt.
Кстати, currentItemможно использовать для работы как с ограниченной, так и с неограниченнойочередью. Для ограниченной очереди itemAt просто возвращает элементмассива по соответствующему индексу. Для неограниченной очередиоперация itemAt будет осуществлять проход по связному списку. Правда, какмы помним, класс Unbounded хранит информацию о последнем элементе, ккоторому было обращение, поэтому переход к следующему за ним элементуочереди (что и происходит при продвижении итератора) будет достаточнопростым.Операция next увеличивает значение текущего индекса на единицу,что соответствует переходу к следующему элементу очереди, а затемпроверяет допустимость нового значения индекса:template<class Item>int QueueActiveIterator<Item>::next(){index++;return !isDone();}Итератор, таким образом, в процессе своей работы вызывает двезащищенные функции класса Queue: cardinality и itemAt.