Гради Буч - Объектно-ориентированный анализ и проектирование с примерами приложений на С++ (1158635), страница 71
Текст из файла (страница 71)
Агент чтения сохраняет состояние объекта; онвызывает только функции-селекторы. Как видно, множественная формасинхронизации обеспечивает наивысшую степень параллелизма процессов.Мы можем реализовать обе политики в виде подклассов абстрактногобазового класса Monitor. Обе формы можно построить на основе классаSemaphore.В отличие от защищенных форм, синхронизованные классы несодержат дополнительных функций-членов по сравнению со своимсуперклассом: они просто переопределяют все виртуальные функциисуперкласса. Семантика, вносимая синхронизированным классом, заставляеттрактовать каждую такую функцию как атомарную транзакцию. В то время,как клиенты защищенного объекта должны для получения эксклюзивногодоступа каждый раз явно захватывать и освобождать доступ,синхронизированные формы обеспечивают эксклюзивность доступа, не требуяспециальных действий со стороны своих клиентов.Это достигается с помощью механизма блокировки, схема работыкоторого приведена на рис.
9-11. Взаимодействие мониторов с экземплярамипредопределенных классов ReadLock и WriteLock обеспечиваетэксклюзивность вызова каждой функции-члена. В этом механизме блокировкаиспользует либо семафор, либо монитор в качестве агента, ответственного запроцесс синхронизации, а сама блокировка отвечает за захват этого агента присоздании и освобождение при удалении. В качестве примера рассмотримопределение класса ReadLock:class ReadLock {public:ReadLock (const Monitor& m): monitor(m) {monitor.seizeForReadingt);} ~ReadLock ()(monitor.releaseFromReading();}private:Monitor& monitor;};Рис. 9-11.
Механизм блокировкиОпределив блокировку и ее монитор как две отдельные абстракции,мы дали клиенту возможность использовать различные политики блокировки.Описание класса WriteLock аналогично, разница лишь в том, что ониспользует протокол монитора для записи.Описания всех функций-членов синхронизированного классаиспользуют блокировки для "оборачивания" операций, унаследованных изсуперкласса.
Рассмотрим в качестве примера реализацию функции length длясинхронизированной неограниченной очереди:template<class Item, class StorageManager, class Monitor>unsigned int SynchronizedUnboundedQueue<Item, StorageManager,Monitor>::length() const{ReadLock lock(monitor);return UnboundedQueue<Item, StorageManager>::length();}Данный фрагмент кода иллюстрирует механизм, приведенный на рис.9-11.
Как правило, объекты класса ReadLock используются для всехсинхронизированных селекторов, а экземпляры WriteLock - длясинхронизированных модификаторов. Простота и элегантность подобнойархитектуры проявляется в том, что каждая функция представляет собойзаконченную операцию, в любом случае гарантирующую сохранностьсостояния объекта, причем без каких-либо явных действий со стороны агентовчтения/записи.Действительно, клиенты, работающие с синхронизированнымиобъектами, не должны придерживаться специальной последовательностидействий, так как механизм синхронизации процессов поддерживается здесь внеявном виде.
Это исключает появление ошибок типа неверной блокировки.Разработчику следует, однако, предпочитать защищенную формусинхронизированной, когда вызов нескольких функций нужно оформить какатомарную транзакцию; синхронизированная форма может гарантироватьатомарность только отдельных функций-членов.Наша архитектура обеспечивает синхронизированным формамотсутствие ситуаций типа "смертельное объятие".
Например, операцииприсваивания объекта самому себе или сравнения его с самим собойпотенциально опасны, так как требуют блокировки и левого и правогоэлементов выражения, которые в данном случае являются одним и тем жеобъектом. Будучи создан, объект не может изменить свою идентичность,поэтому тесты на самоидентичность выполнятся до блокировки какого-либообъекта. Именно поэтому описанный ранее оператор присваиванияoperator= включал такую проверку, как показывает следующаясокращенная запись:template<class Item>Queue<Item>& Queue<Item>::operator=(const Queue<Item>& q){if (this == &q)return *this;}Любые функции-члены, среди аргументов которых есть экземплярыкласса, к которому они принадлежат, должны проектироваться так, чтобыобеспечивалась корректная схема блокировки этих аргументов.
Наше решениебазируется на полиморфизме двух служебных функций, lock и unlock,определенных в каждом абстрактном базовом классе. Каждый абстрактныйбазовый класс по умолчанию содержит заглушку для этих двух функций;синхронизированные формы обеспечивают захват и освобождение аргумента.Вот почему описанный ранее оператор присваивания operator= включалвызовы этих двух функций, как показывает следующая сокращенная запись:q)template<class Item>Queue<Item>& Queue<Item>::operator=(const Queue<Item>&{((Queue<Item>&)q).lock();((Queue<Item>&)q),unlock();return *this;}Явное приведение типа используется в данном случае для того, чтобыосвободиться от ограничения const на аргумент.9.3. ЭволюцияПроектирование интерфейса классовПосле того, как выработаны основные принципы построенияархитектуры системы, остающаяся работа проста, но зачастую довольноскучна и утомительна.
Следующий этап будет состоять в реализации трех иличетырех семейств классов (таких, как очередь, множество и дерево) всоответствии с выбранной архитектурой, и в последующем их тестировании внескольких приложениях27.Наиболее тяжелой частью данного этапа является созданиеподходящего интерфейса для каждого базового класса. И здесь, в процессеизолированной разработки отдельных классов (см.
главу 6), нельзя забывать озадаче обеспечения глобального соответствия всех частей системы друг другу.В частности, для класса Set можно определить следующий протокол:• setHashFunctionУстанавливает функцию хешированиядля элементов множества.• clearОчищает множество.• addДобавляет элемент к множеству.• removeУдаляет элемент из множества.• setUnionОбъединяет с другим множеством.• intersectionНаходит пересечение с другим множеством.• differenceУдаляет элементы, которые содержатся в другоммножестве.• extentВозвращает количество элементов в множестве.• isEmptyВозвращает 1, если множество пусто.• isMemberВозвращает 1, если данный элементпринадлежит множеству.• isSubsetВозвращает 1, если множество являетсяподмножеством другого множества.Возвращает 1, если множество является• isProperSubsetсобственным подмножеством другого множества.Подобным же образом можно определить протокол класса BinaryTree:Уничтожает дерево и всех его потомков.• clearДобавляет новый узел в корень дерева.• insert• appendДобавляет к дереву потомка.• removeУдаляет потомка из дерева.• shareСтруктурно делит данное дерево.Переставляет потомка с деревом.• swapChild• childВозвращает данного потомка.• leftChildВозвращает левого потомка.• rightChildВозвращает правого потомка.Возвращает родителя дерева.• parent• setItemУстанавливает элемент, ассоциированный сдеревом.Возвращает 1, если у дерева есть потомки.• hasChildrenВозвращает 1, если дерево нулевое.• isNull• isSharedВозвращает 1, если дерево структурноразделено.• isRootВозвращает 1, если дерево имеет корень.• itemAtВозвращает элемент, ассоциированный сдеревом.Для схожих операций мы используем схожие имена.
При разработкеинтерфейса мы также проверяем полученное решение на соответствиекритериям достаточности, полноты и примитивности (см. главу 3).Классы поддержкиПри реализации класса, ответственного за манипуляции с текстовымистроками, мы столкнулись с тем, что возможностей, предоставляемыхклассами поддержки Bounded и Unbounded, явно недостаточно.Ограниченная форма, в частности, оказывается неэффективной для работы состроками с точки зрения памяти, так как мы должны инстанцировать этуформу в расчете на максимально возможную строку, и следовательнопонапрасну расходовать память на более коротких строках.
Неограниченнаяформа, в свою очередь, неэффективна с точки зрения быстродействия: поискэлемента в строке может потребовать последовательного перебора всехэлементов связного списка. По этим причинам нам пришлось разработатьтретий, "динамический" вариант:• Динамический Структура хранится в "куче" в виде массива, длинакото-рого может уменьшаться или увеличиваться.Соответствующий класс поддержки Dynamic представляет собойпромежуточный вариант по отношению к ограниченному и неограниченномуклассам, обеспечивающий быстродействие ограниченной формы (возможнопрямое индексирование элементов) и эффективность хранения данных,присущую неограниченной форме (память выделяется только под реальносуществующие элементы).Ввиду того, что протокол данного класса идентичен протоколу классовBounded и Unbounded, добавление к библиотеке нового механизма несоставит большого труда.
Мы должны создать по три новых класса длякаждого семейства (например, DynamicString, GuardedDynamicString, ИSynchronizedDynamicString). Таким образом, мы вводим следующий классподдержки:template<class Item, class StorageManager> class Dynamic{ Public:Dynamic(unsigned int chunkSize);Protected:Item* rep;unsigned int size;unsignedint totalChunks;unsignedint chunkSize;unsignedint start;unsignedint stop;void resize(unsigned int currentLength,unsigned int newLength, int preserve- 1);unsignedint expandLeft(unsigned int from);unsignedint expandRight(unsigned int from);void shrinkLeft(unsigned int from);void shrinkRight(unsigned int from);};Последовательности разбиваются на блоки в соответствии саргументом конструктора chunkSize.
Таким образом, клиент можетрегулировать размер будущего объекта.Из интерфейса видно, что класс Dynamic имеет много общего склассами Bounded и Unbounded. Отличия в реализации трех типов классовкаждого семейства будут минимальны.Займемся теперь классом ассоциативных массивов. Его реализацияпотребует новой переработки ограниченной, динамической и неограниченнойформ. В частности, поиск элемента в ассоциативном массиве требует слишкоммного времени, если его приходится вести перебором всех элементов.