9.1. Анализ
9.1. Анализ
Определение границ проблемной области
На врезке представлены детально сформулированные требования к библиотеке базовых классов. К сожалению, эти требования навряд ли практически выполнимы: библиотека, содержащая абстракции, необходимые для всех возможных программ, оказалась бы слишком большой. Первой обязанностью аналитика, таким образом, является сокращение проблемной области до разумных размеров и формулировка задачи, поддающейся решению. Проблема в том виде, как она представлена сейчас, может повлечь за собой неудачу анализа в целом, поэтому необходимо сконцентрировать внимание лишь на наиболее общих абстракциях и механизмах, пригодных для широкого использования, а не пытаться сделать все для всех (что, скорее всего, приведет к созданию среды, которая никому ничего не даст). Мы начнем анализ с обзора теории структур данных и алгоритмов, а затем перейдем к абстракциям, присущим стандартному программному обеспечению.
Тем, кого интересует теоретический фундамент, можно посоветовать обратиться к плодотворной работе Кнута [2], а также других исследователей в данной области, прежде всего: Ахо, Хопкрофт и Ульман [3], Керниган и Плаугер [4], Седжуик [5], Стабс и Вебре [6], Тененбаум и Аугенстейн [7] и Вирт [8]. По мере изучения теории мы сможем определить ряд основных абстракций для нашей библиотеки, таких как очереди, стеки и графы, а также алгоритмы быстрой сортировки, сравнение с образцом, заданным регулярным выражением, и направленный поиск по дереву.
Первое открытие в ходе нашего анализа - это четкое отделение структурных абстракций (таких как очереди, стеки и графы) от алгоритмических (сортировка, сравнение с образцом и поиск). Первая категория понятий хорошо соответствует классам. Вторая категория, на первый взгляд, не поддается объектно-ориентированной декомпозиции. Однако при надлежащем подходе оказывается, что она вполне возможна: мы можем ввести классы, экземпляры которых будут агентами, выполняющими данные функции. Как будет видно далее, объективация алгоритмических абстракций обеспечивает преимущества общности, благодаря тому, что алгоритмы можно разместить в иерархии "обобщение/специализация".
Требования к библиотеке базовых классов
Библиотека должна содержать универсальные структуры данных и алгоритмы, способные удовлетворить потребности большинства стандартных приложении C++. Кроме того, библиотека должна быть:
? Полной Библиотека должна содержать семейство классов, объединенных согласованным внешним интерфейсом, но с разными представлениями, так чтобы разработчики могли выбрать то, семантика которого наиболее точно соответствует приложению.
? Адаптируемой Все фрагменты кода, зависящие от платформы, должны быть выделены и изолированы в отдельные классы для обеспечения возможности локальных изменений в них. В частности, разработчик должен иметь контроль над механизмами хранения данных и синхронизации процессов.
? Эффективной Процедура подключения различных фрагментов библиотеки к приложению должна быть простой (эффективность при компиляции). Непроизводительные затраты оперативной памяти и процессорного времени на обслуживание и подключение должны быть сведены к минимуму (эффективность при исполнении). Библиотека должна обеспечивать более надежную работу, чем механизмы, разработанные пользователем вручную (эффективность при разработке).
? Безопасной Каждая абстракция должна быть безопасной с точки зрения типов, так чтобы статические предположения о поведении класса могли быть обеспечены компилятором. Для выявления нарушений динамической семантики классов должен быть использован механизм исключений. Возбуждение исключения не должно испортить состояние объекта, вызвавшего исключение.
? Простой Библиотека должна иметь прозрачную структуру, дающую возможность пользователю легко находить и подключать к приложению ее фрагменты.
? Расширяемой Для пользователя должна быть реализована возможность включения в библиотеку новых классов. При этом архитектурная целостность среды разработки не должна нарушаться.
Библиотека должна быть относительно небольших размеров; надо всегда помнить, что пользователь с большей охотой займется разработкой собственного кода, чем изучением чужого малопонятного класса.
Предполагается наличие трансляторов языка C++, поддерживающих параметризованные классы и обработку исключений. В целях обеспечения переносимости библиотеки она не должна зависеть от служб операционной системы.
Таким образом, первым результатом нашего анализа будет разделение всех абстракций на две категории:
? Структуры Содержит все структурные абстракции.
? Инструменты Содержит все алгоритмические абстракции.
Как мы скоро увидим, между этими двумя категориями существует отношение использования: некоторые инструменты построены на базе более примитивных свойств, обеспечиваемых структурами.
На втором этапе анализа мы постараемся выделить базовые классы, которые могут быть использованы в различных стандартных программах (чем шире будет круг рассмотренных приложений, тем лучше). Если в результате окажется, что некоторые из данных классов имеют много общего с абстракциями, определенными на первой стадии анализа, это будет знаком того, что ключевые абстракции были выявлены правильно. Можно составить длинный список специфических абстракций, присущих конкретным видам человеческой деятельности: валюта, астрономические координаты, единицы измерения массы и длины. Мы не будем включать подобные абстракции в нашу библиотеку, так как они либо слишком плохо поддаются формализации (валюта), либо очень специфичны (астрономические координаты), либо настолько примитивны, что нет смысла организовывать специально для них отдельные классы (единицы измерения массы и длины).
Проведя анализ, мы выделим следующие типы структур:
? Набор Множество различных элементов (в том числе дубликатов).
? Множество Набор неповторяющихся элементов.
? Коллекция Индексируемое множество элементов.
? Список Последовательность элементов, имеющая начало; структурное разделение допускается.
? Стек Последовательность элементов; элементы могут удаляться и добавляться только с одного конца.
? Очередь Последовательность элементов, к которой можно добавлять элементы с одного конца, а удалять - с другого.
? Дека Последовательность элементов, к которой можно добавлять и из которой можно удалять элементы с обоих концов.
? Кольцо Последовательность элементов, к которой можно добавлять и из которой можно удалять элементы, находящиеся на вершине круговой структуры.
? Строка Индексируемая последовательность элементов, в которой возможны операции с подстроками.
? Ассоциативный массив Словарь пар "элемент/значение".
? Дерево Набор (имеющий начало - корень дерева) вершин и ребер, которые не могут образовывать циклы и пересекаться; структурное разделение допускается.
? Граф Множество вершин и ребер (без выделенного начального элемента), которое может содержать циклы и пересечения; структурное разделение допускается.
Как уже говорилось в главе 4, упорядочение представленных выше абстракций есть проблема классификации. Мы выбрали именно такую модель из-за того, что она обеспечивает закрепление определенного поведения за каждой категорией объектов.
Обратите внимание на типы поведения, которые использовались в качестве критериев при разбиении на классы: некоторые структуры ведут себя как коллекции (наборы и множества), а другие - как последовательности (деки и стеки). В некоторых структурах (графы, списки и деревья) возможно структурное разделение, в то время как остальные более монолитны и не допускают структурного разделения своих элементов. Как мы увидим далее, подобная классификация поможет в дальнейшем сформировать достаточно простую архитектуру системы.
Для некоторых классов в процессе анализа выявилась желательность их функциональной изменчивости. В частности, нам могут понадобиться упорядоченные коллекции, деки и очереди (последние часто называют приоритетными очередями). Кроме того, мы можем различать ориентированные и неориентированные графы, односвязные и двусвязные списки, бинарные, множественные и AVL-деревья [AVL-дерево - предложенная Г.М.Адельсон-Вольским и Е.М.Ландисом конструкция подравниваемого бинарного дерева. - Примеч. ред.]. Эти специализированные абстракции могут быть получены уточнением одной из вышеперечисленных; их не следует выделять в отдельные большие категории.
Несмотря на то, что мы уже обнаружили признаки общности поведения, мы пока не будем заниматься проработкой иерархической структуры. На этапе анализа важно разобраться в ролях каждой абстракции.
Мы выделим следующие типы инструментов:
? Дата/Время Операции с датой и временем.
? Фильтры Ввод, обработка и вывод.
? Поиск по образцу Операции поиска последовательностей внутри других последовательностей.
? Поиск Операции поиска элементов внутри структур.
? Сортировка Операции упорядочивания структур.
? Утилиты Составные операции, базирующиеся на базовых структурных операциях.
Несомненно, существует масса различных функциональных вариантов этих абстракций. Можно, например, выделить несколько видов сортировок (быстрая сортировка методом пузырька, сортировка кучи и т.д.) или поиска (последовательный, двоичный, различные способы обхода дерева и т.д.). Как и раньше, мы отложим решения относительно наследования этих абстракций.
Модели взаимодействий
Итак, мы определили основные функциональные элементы нашей библиотеки; однако изолированные абстракции сами по себе - еще не среда разработки. Как отметил Вирфс-Брок: "Среда разработки предоставляет пользователю модель взаимодействий между объектами входящих в нее классов... Чтобы освоить среду разработки, прежде всего следует изучить методы взаимодействия и ответственности ее классов". Это и есть тот критерий, по которому можно отличить среду разработки от простого набора классов: среда - это совокупность классов и механизмов взаимодействия экземпляров этих классов.
Анализ показывает, что существует определенный набор основных механизмов, необходимый для библиотеки базовых классов:
• семантика времени и памяти;
• управление хранением данных;
• обработка исключений;
• идиомы итерации;
• синхронизация при многопоточности.
При проектировании системы базовых классов необходимо сохранять баланс между перечисленными техническими требованиями [Действительно, как отмечает Страуструп, "разработка универсальной библиотеки значительно сложнее, чем разработка отдельной программы" [10]]. Если мы будем пытаться решить каждую задачу по отдельности, то, скорее всего, получим ряд изолированных решений, не связанных между собой ни общими протоколами, ни общей концепцией, ни реализацией. Такой наивный подход приведет к изобилию различных подходов, которое испугает потенциального пользователя получившейся библиотеки.
Встанем на точку зрения пользователя нашей библиотеки. Какие абстракции представляют имеющиеся в ней классы? Как они взаимодействуют между собой? Как их можно приспособить к предметной области? Какие классы играют ключевую роль, а какие можно вообще не использовать? Вот те вопросы, на которые нужно дать ответ перед тем, как предлагать пользователям библиотеку для решения нетривиальных задач. К счастью для пользователя, ему не обязательно во всех деталях представлять себе, как работает библиотека, подобно тому, как не нужно понимать принципы работы микропроцессора для программирования на языке высокого уровня. В обоих случаях реализации нижнего уровня может быть продемонстрирована каждому пользователю, но только при его желании.
Рассмотрим описание абстракций нашей библиотеки с двух точек зрения: пользователя, который только объявляет объекты уже существующих классов, и клиента, который конструирует собственные подклассы на базе библиотечных. При проектировании с расчетом на первого пользователя желательно как можно сильнее ограничить доступ к реализациям абстракций и сконцентрироваться на их ответственностях; проектирование с учетом запросов второго пользователя предполагает открытость некоторых внутренних деталей реализации, однако, не настолько, чтобы стало возможным нарушить фундаментальную семантику абстракции. Таким образом, приходится отметить некоторую противоречивость основных требований к системе.
Одной из главных проблем при работе с большой библиотекой являются трудности в понимании того, какие, собственно, механизмы она включает в себя. Перечисленные выше модели представляют собой как бы душу архитектуры библиотеки: чем больше разработчик знает об этих механизмах, тем легче ему будет использовать существующие в библиотеке компоненты, а не сочинять с нуля собственные. На практике получается так, что пользователь сначала знакомится с содержанием и работой наиболее простых классов, и только затем, проверив надежность их работы, постепенно начинает использовать все более сложные классы. В процессе разработки, по мере того как начинают вырисовываться новые, присущие предметной области пользователя, абстракции, они тоже могут добавляться в библиотеку. Развитие объектно-ориентированной библиотеки - это длительный процесс, проходящий через ряд промежуточных этапов.
Именно так мы будем строить нашу библиотеку: сначала определим тот архитектурный минимум, который реализует все пять выделенных нами механизмов, и затем начнем постепенно наращивать на этом остове все новые и новые функции.