Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Языки программирования / Книги / ob''ektno-orientirovannyj_analiz_i_proektirovanie

.pdf
Скачиваний:
42
Добавлен:
11.05.2015
Размер:
9.27 Mб
Скачать

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

321

·add

·remove

·setUnion

·intersection

·difference

·extent

·isEmpty

·isMember

·isSubset

·isProperSubset

Добавляет элемент к множеству. Удаляет элемент из множества. Объединяет с другим множеством.

Находит пересечение с другим множеством.

Удаляет элементы, которые содержатся в другом множестве. Возвращает количество элементов в множестве. Возвращает 1, если множество пусто.

Возвращает 1, если данный элемент принадлежит множеству.

Возвращает 1, если множество является подмножеством другого множества.

Возвращает 1, если множество является собственным подмножеством другого множества.

Подобным же образом можно определить протокол класса BinaryTree:

· clear

Уничтожает дерево и всех его потомков.

· insert

Добавляет новый узел в корень дерева.

· append

Добавляет к дереву потомка.

· remove

Удаляет потомка из дерева.

· share

Структурно делит данное дерево.

· swapChild

Переставляет потомка с деревом.

· child

Возвращает данного потомка.

· leftChild

Возвращает левого потомка.

· rightChild

Возвращает правого потомка.

· parent

Возвращает родителя дерева.

· setItem

Устанавливает элемент, ассоциированный с деревом.

· hasChildren

Возвращает 1, если у дерева есть потомки.

· isNull

Возвращает 1, если дерево нулевое.

· isShared

Возвращает 1, если дерево структурно разделено.

· isRoot

Возвращает 1, если дерево имеет корень.

· itemAt

Возвращает элемент, ассоциированный с деревом.

Для схожих операций мы используем схожие имена. При разработке интерфейса мы также проверяем полученное решение на соответствие критериям достаточности, полноты и примитивности (см. главу 3).

Классы поддержки

При реализации класса, ответственного за манипуляции с текстовыми строками, мы столкнулись с тем, что возможностей, предоставляемых классами поддержки Bounded и Unbounded, явно недостаточно. Ограниченная форма, в частности, оказывается неэффективной для работы со строками с точки зрения памяти, так как мы должны инстанцировать эту форму в расчете на максимально возможную строку, и следовательно

322

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

понапрасну расходовать память на более коротких строках. Неограниченная форма, в свою очередь, неэффективна с точки зрения быстродействия: поиск элемента в строке может потребовать последовательного перебора всех элементов связного списка. По этим причинам нам пришлось разработать третий, "динамический" вариант:

· Динамический

Структура хранится в "куче" в виде массива, длина которого может

уменьшаться или увеличиваться.

 

Структура хранится в "куче" в виде массива, длина которого может уменьшаться или увеличиваться.

Соответствующий класс поддержки Dynamic представляет собой промежуточный вариант по отношению к ограниченному и неограниченному классам, обеспечивающий быстродействие ограниченной формы (возможно прямое индексирование элементов) и эффективность хранения данных, присущую неограниченной форме (память выделяется только под реально существующие элементы).

Ввиду того, что протокол данного класса идентичен протоколу классов Bounded и Unbounded, добавление к библиотеке нового механизма не составит большого труда. Мы должны создать по три новых класса для каждого семейства (например, DynamicString,

GuardedDynamicString и SynchronizedDynamicString). Таким образом, мы вводим следующий класс поддержки:

template<class Item, class StorageManager> class Dynamic {

public:

Dynamic(unsigned int chunkSize);

protected: Item* rep;

unsigned int size; unsigned int totalChunks; unsigned int chunkSize; unsigned int start; unsigned int stop;

void resize(unsigned int currentLength, unsigned int newLength, int preserve - 1); unsigned int expandLeft(unsigned int from); unsigned int expandRight(unsigned int from); void shrinkLeft(unsigned int from);

void shrinkRight(unsigned int from); };

Последовательности разбиваются на блоки в соответствии с аргументом конструктора chunkSize. Таким образом, клиент может регулировать размер будущего объекта.

Из интерфейса видно, что класс Dynamic имеет много общего с классами Bounded и Unbounded. Отличия в реализации трех типов классов каждого семейства будут минимальны.

Займемся теперь классом ассоциативных массивов. Его реализация потребует новой переработки ограниченной, динамической и неограниченной форм. В частности, поиск

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

323

элемента в ассоциативном массиве требует слишком много времени, если его приходится вести перебором всех элементов. Но производительность можно значительно увеличить, используя открытые хеш-таблицы.

Абстракция открытой хеш-таблицы проста. Таблица представляет собой массив последовательностей, которые называются клетками. Помещая в таблицу новый элемент, мы сначала генерируем хеш-код по этому элементу, а затем используем код для выбора клетки, куда будет помещен элемент. Таким образом, открытая хеш-таблица делит длинную последовательность на несколько более коротких, что значительно ускоряет поиск.

Соответствующую абстракцию можно определить следующим образом:

template<class Item, class Value, unsigned int Buckets, class Container> class Table {

public:

Table(unsigned int (*hash)(const Item&));

void setHashFunction(unsigned int (*hash)(const Item&)); void clear();

int bind(const Item&, const Value&); int rebind(const Item&, const Value&); int unbind(const Item&);

Container* bucket(unsigned int bucket); unsigned int extent() const;

int isBound(const Item&) const;

const Value* valueOf(const Item&) const;

const Container *const bucket(unsigned int bucket) const;

protected:

Container rep[Buckets]; };

Использование класса Container в качестве аргумента шаблона позволяет применить абстракцию хеш-таблицы вне зависимости от типа конкретной последовательности. Рассмотрим в качестве примера (сильно упрощенное) объявление неограниченного ассоциативного массива, построенного на базе классов Table и Unbounded:

template<class Item, class Value, unsigned int Buckets, class StorageManager>

class UnboundedMap : public Map<Item, Value> { public:

UnboundedMap();

virtual int bind(const Item&, const Value&); virtual int rebind(const Item&, const Value&); virtual int unbind(const Item&);

protected:

Table<Item, Value, Buckets, Unbounded<Pair<Item, Value>, StorageManager>> rep;

};

В данном случае мы истанцируем класс Table контейнером unbounded. Рис. 9-12 иллюстрирует схему взаимодействия этих классов.

324

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

В качестве свидетельства общей применимости этой абстракции мы можем использовать класс Table при реализации классов множеств и наборов.

Рис. 9-12. Классы поддержки.

Инструменты

Для нашей библиотеки основная роль шаблонов заключается в параметризации структур типами элементов, которые будут в них содержаться; поэтому такие структуры называют классами-контейнерами. Но, как видно из определения класса Table, шаблоны можно использовать также для передачи классу некоторой информации о реализации.

Еще более сложная ситуация возникает при создании инструментов, которые оперируют с другими структурами. Как уже отмечалось, алгоритмы тоже можно представить в виде классов, объекты которых будут выступать в роли агентов, ответственных за выполнение алгоритма. Такой подход соответствует идее Джекобсона об объекте управления, который служит связующим звеном, осуществляющим взаимодействие обычных объектов [16]. Преимущество данного подхода состоит в возможности создания семейств алгоритмов, объединенных наследованием. Это не только упрощает их использование, но также позволяет объединить концептуально схожие алгоритмы.

Рассмотрим в качестве примера алгоритмы поиска образца внутри последовательности. Существует целый ряд подобных алгоритмов:

·Простой

·Кнут-Моррис-Пратт

·Бойер-Мур

Поиск образца последовательной проверкой всей структуры. В худшем случае временной показатель сложности данного алгоритма будет O(pn), где p - длина образца и n - длина последовательности.

Поиск образца с временным показателем O(p+n) (Knuth- Morris-Pratt). Алгоритм не требует создания копий, поэтому годится для поиска в потоках.

Поиск с сублинейным временным показателем (BoyereMoore) O(c(p+n)), где c меньше 1 и обратно пропорционально p.

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

325

· Регулярное выражение

Поиск образца, заданного регулярным выражением.

 

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

Об операции сравнения нужно поговорить особо. Предположим, например, что существует упорядоченный список сотрудников фирмы. Мы хотим произвести в нем поиск по определенному критерию, скажем, найти группы из трех записей с сотрудниками, работающими в одном и том же отделе. Использование оператора operator==, определенного для класса PersonnelRecord, не даст нужного результата, так как этот оператор, скорее всего, производит проверку в соответствии с другим критерием, например, табельным номером сотрудника. Поэтому нам придется специально разработать для этой цели новый оператор сравнения, который запрашивал бы (вызовом соответствующего селектора) название отдела, в котором работает сотрудник. Поскольку каждый агент, выполняющий поиск по образцу, требует своей функции проверки на равенство, мы можем разработать общий протокол введения такой функции в качестве части некоторого абстрактного базового класса. Рассмотрим в качестве примера следующее объявление:

template<class Item, class Sequence> class PatternMatch {

public:

PatternMatch();

PatternMatch(int (*isEqual)(const Item& x, const Item& y)); virtual ~PatternMatch();

virtual void setIsEqualFunction(int (*)(const Item& x, const Item& y));

virtual int match(const Sequence& target, const Sequences; pattern, unsigned int start = 0) = 0;

virtual int match(const Sequence&; target, unsigned int start = 0) = 0;

protected: Sequence rep;

int (*isEqual)(const Item& x, const Item& y); private:

void operator=(coust PattemMatcb&) {} void operator==(const PatternMatch&) {} void operator!=(const PatternMatch&) {} };

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

Теперь опишем конкретный подкласс, определяющий алгоритм Бойера-Мура:

326

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

template<class Item, class Sequence>

class BMPatternMatch : public PatternMatch<Item, Sequence> { public:

BMPatternMatch();

BMPattemMatch(int (*isEqual) (const Item& x, const Item& y)); virtual ~BMPattemMatch();

virtual int match(const Sequence& target, const Seque unsigned int start = 0);

virtual int match(const Sequence& target, unsigned in

protected:

unsigned int length; unsigned int* skipTable;

void preprogress(const Sequence& pattern);

unsigned int itemsSkip(const Sequence& pattern, const Item& item); };

Рис. 9-13. Классы поиска.

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

На рис. 9-13 приведена иерархия классов поиска. Иерархия подобного типа применима для большинства инструментов библиотеки. При этом формируются сходные по структуре семейства классов, что позволяет пользователям легко в них ориентироваться и выбирать те, которые наилучшим образом подходят для их приложений.

9.4. Сопровождение

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

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

327

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

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

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

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

Рис. 9-14 иллюстрирует работу такого механизма, продлевающего жизнь объектов за счет работы отдельного агента. Класс Persist является дружественным классу Queue; мы определяем эту связь внутри описания класса Queue следующим образом:

friend class Persist<Item, Queue<Item>>;

В этом случае классы становятся дружественными только в момент инстан-цирования класса Queue. Внедрив подобные описания дружественности в каждый абстрактный базовый класс, мы обеспечиваем возможность использования Persist с любой структурой библиотеки.

Параметризованный класс Persist содержит операции записи и считывания put и get, а также функции для подключения потоков обмена данными. Мы можем определить данную абстракцию следующим образом:

template<class Item, class Structure> class Persist {

public:

Persist();

Persist(iostream& input, iostream& output); virtual ~Persist();

virtual void setInputStream(iostream&); virtual void setOutputStream(iostream&); virtual void put(Structure&);

virtual void get(Structure&);

328

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

protected: iostream* inStreain; iostream* outStream; };

Рис. 9-14. Обеспечение сохраняемости с помощью агента.

Реализация данного класса зависит от того, является ли он дружественным классу Structure, который фигурирует в качестве аргумента шаблона. В частности, Persist зависит от наличия в структуре вспомогательных функций purge, cardinality, itemAt, lock, и unlock. Далее срабатывает однородность нашей библиотеки: поскольку каждый базовый класс Structure имеет подобные функции, то persist можно безо всяких изменений использовать для работы со всеми имеющимися в библиотеке структурами.

Рассмотрим в качестве примера реализацию функции Persist::put:

template<class Item, class Structure>

void Persist<Item, Structure>::put(Structure& s)

{

s.lock();

unsigned int count = s.cardinality(); (*outStream) << count << endl;

for (unsigned int index = 0; index < count; index++)

(*outStream) << s.itemAt(index); s.unlock();

}

Эта операция использует разработанный нами ранее механизм блокировки, поэтому она будет работать и для защищенных, и для синхронизированных форм. Алгоритм работы функции несложен: сначала в поток выводится количество элементов структуры, а затем, последовательно, все ее элементы. Реализация persist::get аналогично выполняет обратное действие:

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

329

template<class Item, class Structure>

void Persist<Item, Structure>::get(Structure& s)

{

s.lock();

unsigned int count; Item item;

if (! inStream->eof()) { (*inStream) >> count; s.purge();

for (unsigned int index = 0; (index < count) && (! inStream- >eof());

index++)

{

(*inStream) >> item; s.add(item);

}

}

s.unlock();

}

Для того, чтобы использовать этот простой механизм сохранения данных, клиенту надо всего лишь инстанцировать один дополнительный класс для каждой структуры.

Задача построения среды разработки является довольно сложной. При конструировании основных иерархий классов необходимо учитывать различные, зачастую противоречивые требования к системе. Старайтесь сделать вашу библиотеку как можно более гибкой:

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

Дополнительная литература

Бигерстафф и Перлис (Biggerstaffand Perlis) [H 1989] провели исчерпывающий анализ повторного использования программного обеспечения. Вирфс-Брок (Wirfs-Brock) [С 1988] предложил хорошее введение в объектно-ориентированные среды разработки. Джонсон (Johnson) [G 1992] изучал вопросы документирования архитектуры сред разработки и выявил ряд общих моментов.

Библиотека МасАрр [G 1989] для Macintosh является хорошим примером правильно сконструированной объектно-ориентированной прикладной среды разработки. Введение в более раннюю версию этой библиотеки классов может быть найдено у Шмукера

(Schmucker) [G 1986]. В недавней работе Голдстейн и Алджер (Goldstein and Alger) [С 1992] обсуждают развитие объектно-ориентированного программного обеспечения для

Macintosh.

Другие примеры сред разработки: гипермедиа (Мейровиц (Meirowitz) [С 1986]), распознавание образов (Йошида (Yoshida) [С 1988]), интерактивная графика (Янг (Young)

330

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

[С 1987]), настольные издательские системы (Феррел (Ferrel) [K 1989]). Среды разработки общего назначения: ЕТ++ (Вейнанд (Weinand) [K 1989]) и управляемые событиями MVCархитектуры (Шэн (Shan) [G 1989]). Коггинс (Coggins) [С 1990] изучил, в частности, развитие библиотек для C++.

Эмпирическое изучение объектно-ориентированных архитектур и их влияния на повторное использование можно найти в работе Льюиса (Lewis) [С 1992].

Соседние файлы в папке Книги