Всё для программиста

Процесс разработки
Индекс материала
Процесс разработки
Рабочие потоки процесса
Технические артефакты
Идентификация риска
Анализ риска
Планирование управления риском
Этап НАЧАЛО (Inception)
Этап РАЗВИТИЕ (Elaboration)
Этап КОНСТРУИРОВАНИЕ (Construction)
Этап ПЕРЕХОД (Transition)
Этап НАЧАЛО
Этап РАЗВИТИЕ
Этап КОНСТРУИРОВАНИЕ
ХР-реализация
ХР-итерация
Элемент ХР-разработки
Коллективное владение кодом
Взаимодействие с заказчиком
Объектно-ориентированное тестирование
Особенности тестирования объектно-ориентированных «модулей»
Объектно-ориентированное тестирование правильности
Тестирование, основанное на ошибках
Тестирование, основанное на сценариях
Тестирование поверхностной и глубинной структуры
Тестирование разбиений на уровне классов
Стохастическое тестирование
Тестирование разбиений
Листинг 16.1.
Листинг 16.5.
Листинг 16.10
Листинг 16.15.
Листинг 16.20.
Автоматизация конструирования визуальной модели программной системы
Создание диаграммы последовательности
Создание диаграммы классов
Создание компонентной диаграммы
Заключение
Все страницы

ГЛАВА 15. Унифицированный процесс разработки объектно-ориентированных ПС

 

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

Эволюционно-инкрементная организация жизненного цикла разработки

 

Рассматриваемый подход является развитием спиральной модели Боэма [8], [40], [44], [57]. В этом случае процесс разработки программной системы организуется в виде эволюционно-инкрементного жизненного цикла. Эволюционная составляющая цикла основывается на доопределении требований в ходе работы, инкрементная составляющая — на планомерном приращении реализации требований.

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

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

Такая методика построения системы нацелена на выявление и уменьшение риска в самом начале жизненного цикла. В итоге минимизируются затраты на уменьшение риска.

 

Рис. 15.1. Типовая итерация эволюционно-инкрементного жизненного цикла

 

 

Рис. 15.2. Два измерения унифицированного процесса разработки

 

Как показано на рис. 15.2, в структуре унифицированного процесса разработки выделяют два измерения:

q       горизонтальная ось представляет время и демонстрирует характеристики жизненного цикла процесса;

q       вертикальная ось представляет рабочие потоки процесса, которые являются логическими группировками действий.

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

Этапы и итерации

 

По времени в жизненном цикле процесса выделяют четыре этапа:

q       начало (Inception) — спецификация представления продукта;

q       развитие (Elaboration) — планирование необходимых действий и требуемых ресурсов;

q       конструирование (Construction) — построение программного продукта в виде серии инкрементных итераций;

q       переход (Transition) — внедрение программного продукта в среду пользователя (промышленное производство, доставка и применение).

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


Рабочие потоки процесса

 

Рабочие потоки процесса имеют следующее содержание:

q       Сбор требований — описание того, что система должна делать;

q       Анализ — преобразование требований к системе в классы и объекты, выявляемые в предметной области;

q       Проектирование — создание статического и динамического представления системы, выполняющего выявленные требования и являющегося эскизом реализации;

q       Реализация — производство программного кода, который превращается в исполняемую систему;

q       Тестирование — проверка всей системы в целом.

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

Между артефактами потоков существуют зависимости. Например, модель Use Case, генерируемая в ходе сбора требований, уточняется моделью анализа из процесса анализа, обеспечивается проектной моделью из процесса проектирования, реализуется моделью реализации из процесса реализации и проверяется тестовой моделью из процесса тестирования.

Модели

 

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

q       бизнес-модель. Определяет абстракцию организации, для которой создается система;

q       модель области определения. Фиксирует контекстное окружение системы;

q       модель Use Case. Определяет функциональные требования к системе;

q       модель анализа. Интерпретирует требования к системе в терминах проектной модели;

q       проектная модель. Определяет словарь проблемы и ее решение;

q       модель размещения. Определяет аппаратную топологию, в которой исполняется система;

q       модель реализации. Определяет части, которые используются для сборки и реализации физической системы;

q       тестовая модель. Определяет тестовые варианты для проверки системы;

q       модель процессов. Определяет параллелизм в системе и механизмы синхронизации.


Технические артефакты

 

Технические артефакты подразделяются на четыре основных набора:

q       набор требований. Описывает, что должна делать система;

q       набор проектирования. Описывает, как должна быть сконструирована система;

q       набор реализации. Описывает сборку разработанных программных компонентов;

q       набор размещения. Обеспечивает всю информацию о поставляемой конфигурации.

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

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

Он может включать проектную модель, тестовую модель и другие формы выражения сущности системы (например, макеты).

Набор реализации группирует все данные о программных элементах, образующих систему (программный код, файлы конфигурации, файлы данных, программные компоненты, информацию о сборке системы).

Набор размещения группирует всю информацию об упаковке, отправке, установке и запуске системы.

Управление риском

 

Словарь русского языка С. И. Ожегова и Н. Ю. Шведовой определяет риск как «возможность опасности, неудачи». Влияние риска вычисляют по выражению

RE = P(UO) x L(UO),

где:

q       RE — показатель риска (Risk Exposure — подверженность риску);

q       P(UO) — вероятность неудовлетворительного результата (Unsatisfactory Outcome);

q       L(UO) — потеря при неудовлетворительном результате.

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

1.      Идентификация риска — выявление элементов риска в проекте.

2.      Анализ риска — оценка вероятности и величины потери по каждому элементу риска.

3.      Ранжирование риска — упорядочение элементов риска по степени их влияния.

4.      Планирование управления риском — подготовка к работе с каждым элементом риска.

5.      Разрешение риска — устранение или разрешение элементов риска.

6.      Наблюдение риска — отслеживание динамики элементов риска, выполнение корректирующих действий.

Первые три действия относят к этапу оценивания риска, последние три действия — к этапу контроля риска [20].


Идентификация риска

 

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

Выделяют три категории источников риска: проектный риск, технический риск, коммерческий риск.

Источниками проектного риска являются:

q       выбор бюджета, плана, человеческих ресурсов программного проекта;

q       формирование требований к программному продукту;

q       сложность, размер и структура программного проекта;

q       методика взаимодействия с заказчиком.

К источникам технического риска относят:

q       трудности проектирования, реализации, формирования интерфейса, тестирования и сопровождения;

q       неточность спецификаций;

q       техническая неопределенность или отсталость принятого решения.

Главная причина технического риска — реальная сложность проблем выше предполагаемой сложности.

Источники коммерческого риска включают:

q       создание продукта, не требующегося на рынке;

q       создание продукта, опережающего требования рынка (отстающего от них);

q       потерю финансирования.

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

1. Дефицит персонала.

2. Нереальные расписание и бюджет.

3. Разработка неправильных функций и характеристик.

4. Разработка неправильного пользовательского интерфейса.

5. Слишком дорогое обрамление.

6. Интенсивный поток изменения требований.

7. Дефицит поставляемых компонентов.

8. Недостатки в задачах, разрабатываемых смежниками.

9. Дефицит производительности при работе в реальном времени.

10. Деформирование научных возможностей.

На практике каждый элемент списка снабжается комментарием — набором методик для предотвращения источника риска.

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


Анализ риска

 

В ходе анализа оценивается вероятность возникновения Рi и величина потери Li для каждого выявленного i-го элемента риска. В результате вычисляется влияние REi i-го элемента риска на проект.

Вероятности определяются с помощью экспертных оценок или на основе статистики, накопленной за предыдущие разработки. Итоги анализа, как показано в табл. 15.1, сводятся в таблицу.

Таблица 15.1. Оценка влияния элементов риска

Элемент риска

Вероятность, %

Потери

Влияние риска

1. Критическая программная ошибка

3-5

10

30-50

2. Ошибка потери ключевых данных

3-5

8

24-40

3. Отказоустойчивость недопустимо снижает производительность

4-8

7

28-56

4. Отслеживание опасного условия как безопасного

5

9

45

5. Отслеживание безопасного условия как опасного

5

3

15

6. Аппаратные задержки срывают планирование

6

4

24

7. Ошибки преобразования данных приводят к избыточным вычислениям

8

1

8

8. Слабый интерфейс пользователя снижает эффективность работы

6

5

30

9. Дефицит процессорной памяти

1

7

7

10. СУБД теряет данные

2

2

4

Ранжирование риска

 

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

Для больших проектов количество элементов риска может быть очень велико (30-40 элементов). В этом случае управление риском затруднено. Поэтому к элементам риска применяют принцип Парето 80/20. Опыт показывает, что 80% всего проектного риска приходятся на долю 20% от общего количества элементов риска. В ходе ранжирования определяют эти 20% элементов риска (их называют существенными элементами). В дальнейшем учитывается влияние только существенных элементов риска.


Планирование управления риском

 

Цель планирования — сформировать набор функций управления каждым элементом риска. Введем необходимые определения.

В планировании используют понятие эталонного уровня риска. Обычно выбирают три эталонных уровня риска: превышение стоимости, срыв планирования, упадок производительности. Они могут быть причиной прекращения проекта. Если комбинация проблем, создающих риск, станет причиной превышения любого из этих уровней, работа будет остановлена. В фазовом пространстве риска эталонному уровню риска соответствует эталонная точка. В эталонной точке решения « продолжать проект» и «прекратить проект» имеют одинаковую силу. На рис. 15.3 показана кривая останова, составленная из эталонных точек.

Рис. 15.3. Кривая останова проекта

 

Ниже кривой располагается рабочая область проекта, выше кривой — запретная область (при попадании в эту область проект должен быть прекращен).

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

Теперь рассмотрим последовательность шагов планирования.

1.         Исходными данными для планирования является набор четверок [Ri Pi, Li, REi], где Ri — 2-й элемент риска, Pi — вероятность i-го элемента риска, Li — потеря по i-му элементу риска, REi — влияние i-го элемента риска.

2.         Определяются эталонные уровни риска в проекте.

3.         Разрабатываются зависимости между каждой четверкой [Ri Pi, Li, REi] и каждым эталонным уровнем.

4.         Формируется набор эталонных точек, образующих сферу останова. В сфере останова предсказываются области неопределенности.

5.         Для каждого элемента риска разрабатывается план управления. Предложения плана составляются в виде ответов на вопросы «зачем, что, когда, кто, где, как и сколько».

6.         План управления каждым элементом риска интегрируется в общий план программного проекта.

Разрешение и наблюдение риска

 

Основанием для разрешения и наблюдения является план управления риском. Работы по разрешению и наблюдению производятся с начала и до конца процесса разработки.

Разрешение риска состоит в плановом применении действий по уменьшению риска.

Наблюдение риска гарантирует:

q       цикличность процесса слежения за риском;

q       вызов необходимых корректирующих воздействий.

Для управления риском используется эффективная методика «Отслеживание 10 верхних элементов риска». Эта методика концентрирует внимание на факторах повышенного риска, экономит много времени, минимизирует «сюрпризы» разработки.

Рассмотрим шаги методики «Отслеживания 10 верхних элементов риска».

1.      Выполняется выделение и ранжирование наиболее существенных элементов риска в проекте.

2.      Производится планирование регулярных просмотров (проверок) процесса разработки. В больших проектах (в группе больше 20 человек) просмотр должен проводиться ежемесячно, в остальных проектах — чаще.

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

4.      Внимание участников просмотра концентрируется на любых проблемах в разрешении элементов риска.

Этапы унифицированного процесса разработки

 

Обсудим назначение, цели, содержание и основные итоги каждого этапа унифицированного процесса разработки.


Этап НАЧАЛО (Inception)

 

Главное назначение этапа — запустить проект.

Цели этапа НАЧАЛО:

q       определить область применения проектируемой системы (ее предназначение, границы, интерфейсы с внешней средой, критерий признания — приемки);

q       определить элементы Use Case, критические для системы (основные сценарии поведения, задающие ее функциональность и покрывающие главные проектные решения);

q       определить общие черты архитектуры, обеспечивающей основные сценарии, создать демонстрационный макет;

q       определить общую стоимость и план всего проекта и обеспечить детализированные оценки для этапа развития;

q       идентифицировать основные элементы риска. Основные действия этапа НАЧАЛО:

q       формулировка области применения проекта — выявление требований и ограничений, рассматриваемых как критерий признания конечного продукта;

q       планирование и подготовка бизнес-варианта и альтернатив развития для управления риском, определение персонала, проектного плана, а также выявление зависимостей между стоимостью, планированием и полезностью;

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

В итоге этапа НАЧАЛО создаются следующие артефакты:

q       спецификация представления основных проектных требований, ключевых характеристик и главных ограничений;

q       начальная модель Use Case (20% от полного представления); а начальный словарь проекта;

q       начальный бизнес-вариант (содержание бизнеса, критерий успеха — прогноз дохода, прогноз рынка, финансовый прогноз);

q       начальное оценивание риска;

q       проектный план, в котором показаны этапы и итерации.


Этап РАЗВИТИЕ (Elaboration)

 

Главное назначение этапа — создать архитектурный базис системы.

Цели этапа РАЗВИТИЕ:

q       определить оставшиеся требования, функциональные требования формулировать как элементы Use Case;

q       определить архитектурную платформу системы;

q       отслеживать риск, устранить источники наибольшего риска;

q       разработать план итераций этапа КОНСТРУИРОВАНИЕ.

Основные действия этапа РАЗВИТИЕ:

q       развитие спецификации представления, полное формирование критических элементов Use Case, задающих дальнейшие решения;

q       развитие архитектуры, выделение ее компонентов.

В итоге этапа РАЗВИТИЕ создаются следующие артефакты:

q       модель Use Case (80% от полного представления);

q       дополнительные требования (нефункциональные требования, а также другие требования, которые не связаны с конкретным элементом Use Case);

q       описание программной архитектуры;

q       выполняемый архитектурный макет;

q       пересмотренный список элементов риска и пересмотренный бизнес-вариант;

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

Обсудим более подробно главную цель этапа РАЗВИТИЕ — создание архитектурного базиса.

Архитектура объектно-ориентированной системы многомерна — она описывается множеством параллельных представлений. Как показано на рис. 15.4, обычно используется «4+1»-представление [44].

 

Рис. 15.4. «4+1»-представление архитектуры

 

Представление Use Case описывает систему как множество взаимодействий с точки зрения внешних актеров. Это представление создается на этапе НАЧАЛО жизненного цикла и управляет оставшейся частью процесса разработки.

Логическое представление содержит набор пакетов, классов и отношений. Изначально создается на этапе развития и усовершенствуется на этапе конструирования.

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

Представление реализации содержит модули и подсистемы. Представление изначально создается на этапе развития и усовершенствуется на этапе конструирования.

Представление размещения содержит физические узлы системы и соединения между узлами. Создается на этапе развития.

В качестве примера рассмотрим порядок создания логического представления архитектуры. Для решения этой задачи исследуются элементы Use Case, разработанные на этапе НАЧАЛО. Рассматриваются экземпляры элементов Use Case — сценарии. Каждый сценарий преобразуется в диаграмму последовательности. Далее в диаграммах последовательности выделяются объекты. Объекты группируются в классы. Классы могут группироваться в пакеты.

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

В качестве другого примера рассмотрим разработку плана итераций для этапа КОНСТРУИРОВАНИЕ. Такой план должен задавать управляемую серию архитектурных реализаций, каждая из которых увеличивает свои функциональные возможности, а конечная — покрывает все требования к полной системе. Главным источником информации являются элементы Use Case и диаграммы последовательности. Будем называть их обобщенно — сценариями. Сценарии группируются так, чтобы обеспечивать реализацию определенной функциональности системы. Кроме того, группировки должны устранять наибольший (в данный момент) риск в проекте.

План итераций включает в себя следующие шаги:

1.      Определяются все элементы риска в проекте. Устанавливаются их приоритеты.

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

3.      В результате анализа сценариев формируются классы и отношения, которые их реализуют.

4.      Программируются сформированные классы и отношения.

5.      Разрабатываются тестовые варианты.

6.      Тестируются классы и отношения. Цель — проверить выполнение функционального назначения сценария.

7.      Результаты объединяются с результатами предыдущих итераций, проводится тестирование интеграции.

8.      Оценивается итерация. Выделяется необходимая повторная работа. Она назначается на будущую итерацию.


Этап КОНСТРУИРОВАНИЕ (Construction)

 

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

Цели этапа КОНСТРУИРОВАНИЕ:

q       минимизировать стоимость разработки путем оптимизации ресурсов и устранения необходимости доработок;

q       добиться быстрого получения приемлемого качества;

q       добиться быстрого получения контрольных версий (альфа, бета и т. д.).

Основные действия этапа КОНСТРУИРОВАНИЕ:

q       управление ресурсами, контроль ресурсов, оптимизация процессов;

q       полная разработка компонентов и их тестирование (по сформулированному критерию эволюции);

q       оценивание реализаций продукта (по критерию признания из спецификации представления).

В итоге этапа КОНСТРУИРОВАНИЕ создаются следующие артефакты:

q       программный продукт, готовый для передачи в руки конечных пользователей;

q       описание текущей реализации;

q       руководство пользователя.

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

1.      Идентификация реализуемых классов и отношений.

2.      Определение в классах типов данных (для свойств) и сигнатур (для операций). Добавление сервисных операций, например операций доступа и управления. Добавление сервисных классов (классов-контейнеров, классов-контроллеров). Реализация отношений ассоциации, агрегации и наследования.

3.      Создание текста на языке программирования.

4.      Создание(обновление) документации.

5.      Тестирование функций реализации продукта.

6.      Объединение текущей и предыдущей реализаций. Тестирование итерации.


Этап ПЕРЕХОД (Transition)

 

Главное назначение этапа — применить программный продукт в среде пользователей и завершить реализацию продукта.

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

Оценка качества проектирования

 

Качество проектирования оценивают с помощью объектно-ориентированных метрик, введенных в главе 14.

Этап РАЗВИТИЕ

 

Качество логического представления архитектуры оценивают по метрикам:

q       WMC — взвешенные методы на класс;

q       NOC — количество детей;

q       DIT — высота дерева наследования;

q       NOM — суммарное количество методов, определенных во всех классах системы;

q       NC — общее количество классов в системе.

Метрики WMC, NOC вычисляются для каждого класса, кроме того, формируются их средние значения в системе. Метрики DIT, NOM, NC вычисляются для всей системы.

Этап КОНСТРУИРОВАНИЕ

 

На каждой итерации конструирования продукта вычисляются метрики:

q       WMC — взвешенные методы на класс;

q       NOC — количество детей;

q       СВО — сцепление между классами объектов;

q       RFC — отклик для класса;

q       LCOM — недостаток связности в методах;

q       CS — размер класса;

q       NOO — количество операций, переопределяемых подклассом;

q       NOA — количество операций, добавленных подклассом;

q       SI — индекс специализации;

q       OSavg — средний размер операции;

q       NPavg — среднее количество параметров на операцию;

q       NC — общее количество классов в системе;

q       LOC — суммарная LOC-оценка всех методов системы;

q       DIT — высота дерева наследования;

q       NOM — суммарное количество методов в системе.

Метрики WMC, NOC, СВО, RFC, LCOM, CS, NOO, NOA, SI, OSAVG, NPAVG вычисляются для каждого класса, кроме того, формируются их средние значения в системе. Метрики DIT, NOM, NC, LOCS вычисляются для всей системы.

На последней итерации дополнительно вычисляется набор метрик MOOD, предложенный Абреу:

q       МНF — фактор закрытости метода;

q       AHF — фактор закрытости свойства;

q       MIF — фактор наследования метода;

q       AIF — фактор наследования свойства;

q       POF — фактор полиморфизма;

q       СОF — фактор сцепления.

Пример объектно-ориентированной разработки

 

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


Этап НАЧАЛО

 

Оконный интерфейс пользователя(WUI) — среда, управляемая событиями. Действия в среде инициируются функциями обратного вызова, которые вызываются в ответ на событие — пользовательский ввод. Ядром WUI является цикл обработки событий, который организуется менеджером ввода.

WUI должен обеспечивать следующие типы неперекрывающихся окон:

q       простое окно, в которое может быть выведен текст;

q       окно меню, в котором пользователь может задать вариант действий — выбор подменю или функции обратного вызова.

Идентификация актеров

 

Актерами для WUI являются:

q       пользователь прикладной программы, использующей WUI;

q       администратор системы, управляющий работой WUI.

Внешнее окружение WUI имеет вид, представленный на рис. 15.5.

 

Рис. 15.5. Внешнее окружение WUI

Идентификация элементов Use Case

 

В WUI могут быть выделены два элемента Use Case:

q       управление окнами;

q       использование окон.

Диаграмма Use Case для среды WUI представлена на рис. 15.6.

 

Рис. 15.6. Диаграмма Use Case для среды WUI

 

Описания элементов Use Case

 

Описание элемента Use Case Управление окнами.

Действия начинаются администратором системы. Администратор может создать, удалить или модифицировать окно.

Описание элемента Use Case Использование окон.

Действия начинаются пользователем прикладной программы. Обеспечивается возможность работы с меню и простыми окнами.


Этап РАЗВИТИЕ

 

На этом этапе создаются сценарии для элементов Use Case, разрабатываются диаграммы последовательности (формализующие текстовые представления сценариев), проектируются диаграммы классов и планируется содержание следующего этапа разработки.

Сценарии для элемента Use Case Управление окнами

 

В элементе Use Case Управление окнами заданы три потока событий — три сценария.

1. Сценарий Создание окна.

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

2. Сценарий Изменение стиля рамки.

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

3. Сценарий Уничтожение окна.

Менеджер окон получает указание удалить окно. Менеджер окон снимает окно с регистрации (в массиве управляемых окон WUI). Окно снимает отображение с экрана.

Развитие описания элемента Use Case Использование окон

 

Действия начинаются с ввода пользователем символа. Символ воспринимается менеджером ввода. В зависимости от значения введенного символа выполняется один из следующих вариантов:

при значении ENTER - вариант ОКОНЧАНИЯ ВВОДА;

при переключающем значении - вариант ПЕРЕКЛЮЧЕНИЯ;

при обычном значении - символ выводится в активное окно.

Вариант ОКОНЧАНИЯ ВВОДА:

при активном окне меню выбирается пункт меню. В ответ либо выполняется функция обратного вызова (закрепленная за этим пунктом меню), либо вызывается подменю (соответствующее данному пункту меню);

при активном простом окне выполняется переход на новую строку окна.

Вариант ПЕРЕКЛЮЧЕНИЯ.

При вводе переключающего символа:

ESC - активным становится окно меню;

TAB - активным становится следующее простое окно;

Ctrl-E - все окна закрываются и сеанс работы заканчивается.

Далее из описания элемента Use Case Использование окон выделяются два сценария: Использование простого окна и Использование окна меню.

На следующем шаге сценарии элементов Use Case преобразуются в диаграммы последовательности — за счет этого достигается формализация описаний, требуемая для построения диаграмм классов. Для построения диаграмм последовательности проводится грамматический разбор каждого сценария элемента Use Case: значащие существительные превращаются в объекты, а значащие глаголы — в сообщения, пересылаемые между объектами.

Диаграммы последовательности

 

Диаграммы изображены на рис. 15.7-15.11.

 

Рис. 15.7. Диаграмма последовательности Создание окна

 

 

Рис. 15.8. Диаграмма последовательности Изменение стиля рамки

 

 

15.9. Диаграмма последовательности Уничтожение окна

 

 

Рис. 15.10. Диаграмма последовательности Использование простого окна

 

 

Рис. 15.11. Диаграмма последовательности Использование окна меню

 

Создание классов

 

Работа по созданию классов (и включению их в диаграмму классов) требует изучения содержания всех диаграмм последовательности. Проводится она в три этапа.

На первом этапе выявляются и именуются классы. Для этого просматривается каждая диаграмма последовательности. Любой объект в этой диаграмме должен принадлежать конкретному классу, для которого надо придумать имя. Например, резонно предположить, что объекту Менеджер окон должен соответствовать класс Window_Manager, поэтому класс Window_Manager следует ввести в диаграмму. Конечно, если в другой диаграмме последовательности опять появится подобный объект, то дополнительный класс не образуется.

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

На третьем этапе определяются отношения ассоциации между классами — они обеспечивают пересылки сообщений между соответствующими объектами.

В нашем примере анализ диаграмм последовательности позволяет выделить следующие классы:

q       Window — класс, объектами которого являются простые окна;

q       Menu — класс, объектами которого являются окна меню. Этот класс является потомком класса Window;

q       Menu_title — класс, объектом которого является окно главного меню. Класс является потомком класса Menu;

q       Screen — класс, объектом которого является экран. Этот класс обеспечивает позиционирование курсора, вывод изображения на экран дисплея, очистку экрана;

q       Input_Manager — объект этого класса управляет взаимодействием между пользователем и окнами интерфейса. Его обязанности: начальные установки среды WUI, запуск цикла обработки событий, закрытие среды WUI;

q       Window_Manager — осуществляет общее управление окнами, отображаемыми на экране. Используется менеджером ввода для получения доступа к конкретному окну.

Для оптимизации ресурсов создается абстрактный суперкласс Root_Window. Он определяет минимальные обязанности, которые должен реализовать любой тип окна (а (посылка символа в окно, перевод окна в активное/пассивное состояние, перерисовка окна, возврат информации об окне). Все остальные классы окон являются его потомками.

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

Начальное представление иерархии классов WUI показано на рис. 15.12. Результаты начальной оценки качества проекта сведены в табл. 15.2.

 

Рис. 15.12. Начальная диаграмма классов WUI

 

Таблица 15.2. Результаты начальпий оценки качества WUI

Метрика

Input_ Manager

Window_ Manager

Screen

Root_ Window

Window

Menu

Menu_ title

Среднее значение

WMC

NOC

3

-

3

-

3

-

0

1

9

1

4

1

3

0

3,57

0,43

Метрики, вычисляемые для системы

DIT

NC

NOM

3

7

25











 

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

Планирование итераций конструирования

 

На данном шаге составляется план итераций, который определяет порядок действий на этапе конструирования. Цель каждой итерации — уменьшить риск разработки конечного продукта. Для создания начального плана анализируются элементы Us Case, их сценарии и диаграммы последовательности. Устанавливается приоритет их реализации. При завершении каждой итерации будет повторно вычисляться риск. Оценка риска может привести к необходимости обновления плана итераций.

Положим, что максимальный риск связан с реализацией элемента Use Case Управление окнами, причем наиболее опасна разработка сценария Создание окна, среднюю опасность несет сценарий Уничтожение окна и малую опасность — Изменение стиля рамки.

В связи с этими соображениями начальный план итераций принимает вид:

Итерация 1 — реализация сценариев элемента Use Case Управление окнами:

1. Создание окна.

2. Уничтожение окна.

3. Изменение стиля рамки.

Итерация 2 — реализация сценариев элемента Use Case Использование окон:

4. Использование простого окна.

5. Использование окна меню.


Этап КОНСТРУИРОВАНИЕ

 

Рассмотрим содержание итераций на этапе конструирования.

Итерация 1 — реализация сценариев элемента Use Case Управление окнами

 

Для реализации сценария Создание окна программируются следующие операции класса Window:

q         framework — создание каркаса окна;

q         register — регистрация окна;

q         set_call_back — установка функции обратного вызова;

q         make_window — задание видимости окна.

Далее реализуются операции общего управления окнами, методы класса Window_Manager:

q       add_to_list — добавление нового окна в массив управляемых окон;

q       find — поиск окна с заданным переключающим символом.

Программируются операции класса Input-Manager:

q       window_prolog — инициализация WUI;

q       window_start — запуск цикла обработки событий;

q       window_epilog — закрытие WUI.

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

1. В классе Window_Manager:

q         write_to — форматный вывод сообщения в указанное окно;

q         hide_win — удаление окна с экрана;

q         switchAwayFromTop — подготовка окна к переходу в пассивное состояние;

q         switch_to_top — подготовка окна к переходу в активное состояние;

q         window_fatal — формирование донесения об ошибке;

q         top — переключение окна в активное состояние;

q         send_to_top — посылка символа в активное окно.

2. В классе Window:

q         put — три реализации для записи в окно символьной, строковой и числовой информации;

q         create — создание макета окна (используется операцией framework);

q         position — изменение позиции курсора в окне;

q         about — возврат информации об окне;

q         switch_to — пометка активного окна;

q         switch_away — пометка пассивного окна;

q         send_to — посылка символа в окно для обработки.

Второй шаг первой итерации ориентирован на реализацию сценария Уничтожение окна. Основная операция — finalize (метод класса Window), она выполняет разрушение окна. Для ее обеспечения создаются вспомогательные операции:

q       de_register — удаление окна из массива управляемых окон;

q       remove_from_list (метод класса Window_Manager) — вычеркивание окна из регистра.

Для реализации сценария Изменение стиля рамки создаются операции в классе Window:

q       mark_border — построение новой рамки окна;

q       refresh — перерисовка окна на экране.

В конце итерации создаются операции класса Screen:

q       dear_screen — очистка экрана;

q       position_cursor — позиционирование курсора;

q       put — вывод на экран дисплея строк, символов и чисел.

Результаты оценки качества первой итерации представлены в табл. 15.3.

Таблица 15.3. Оценки качества WUI после первой итерации

Метрика

lnput_ Manager

Window_ Manager

Screen

Root_ Window

Window

Среднее значение

WMC

0,12

0,42

0,11

0

0,83

0,3

NOC

-

-

-

1

0

0,2

СВО

3

3

0

1

2

1,8

RFC

6

11

0

0

23

8

LCOM

3

0

5

0

0

1,6

CS

3/2

10/8

5/1

0/2

18/22

7,2/7

NOO

-

-

-

0

0

0

NOA

-

-

-

0

18

3,6

SI

-

-

-

0

0

0

OSAVG

4

4,2

2,2

0

4,6

3

NPAVG

0

1,3

1

0

2,4

0,9

Метрики, вычисляемые для системы

DIT

1

 

 

 

 

 

NC

5

 

 

 

 

 

MOM

35

 

 

 

 

 

LOC

148

 

 

 

 

 

Итерация 2 — реализация сценариев элемента Use Case Использование окон

 

На этой итерации реализуем методы классов Menu и Menu_title, а также добавим необходимые вспомогательные методы в класс Window.

Отметим, что операции, обеспечивающие сценарий Использование простого окна, в основном уже реализованы (на первой итерации). Осталось запрограммировать следующие операции — методы класса Window:

q       call_call_back — вызов функции обратного вызова;

q       initialize — управляемая инициализация окна;

q       clear — очистка окна с помощью пробелов;

q       new_line — перемещение на следующую строку окна.

Для обеспечения сценария Использование окна меню создаются следующие операции.

1. В классе Menu:

q       framework — создание каркаса окна-меню;

q       send_to — обработка пользовательского ввода в окно-меню;

q       menu_spot — выделение выбранного элемента меню;

q       set_up — заполнение окна-меню именами элементов;

q       get_menu_name — возврат имени выбранного элемента меню;

q       get_cur_selected_detaits — возврат указателя на выбранное окно и функцию обратного вызова.

2. В классе Menu_title:

q       send_to — выделение новой строки меню или вызов функции обратного вызова;

q       switch_away — возврат в базовое окно-меню более высокого уровня;

q       set_up — установки окна меню-заголовка.

Результаты оценки качества второй итерации представлены в табл. 15.4.

Таблица 15.4. Оценки качества WUI после второй итерации

Метрика

lnput_ Manager

Window_ Manager

Screen

Root_ Window

Window

Menu

Menu title

Среднее значение

WMC

0,12

0,42

0,11

0

0,98

0,33

0,27

0,32

NOC

-

-

-

1

1

1

0

0,4

СВО

3

3

0

1

2

2

3

2

RFC

6

11

0

0

27

9

12

9,4

LCOM

3

0

5

0

0

0

0

1,1

CS

3/2

10/8

5/1

0/2

22/22

28/24

11/12

11,3/10,1

NOO

-

-

-

0

0

2

3

0,7

NOA

-

-

-

0

22

6

0

4

SI

-

-

-

0

0

0,23

0,46

0,1

oswe

4

4,2

2,2

0

4,45

4,13

9

4,0

NPAVG

0

1,3

1

0

2,18

4,63

1,67

1,5

Метрики, вычисляемые для системы

DIT

3

 

 

 

 

 

 

 

NC

7

 

 

 

 

 

 

 

MOM

48

 

 

 

 

 

 

 

LOCZ

223

 

 

 

 

 

 

 

 

Сравним оценки качества первой и второй итераций.

1.        Рост системных оценок LOC, NOM, а также средних значений метрик WMC, RFC, CS, СВО и NOO — свидетельство возрастания сложности продукта.

2.        Увеличение значения DIT и среднего значения NOC говорит об увеличении возможности многократного использования классов.

3.        На второй итерации в среднем была ослаблена абстракция классов, о чем свидетельствует увеличение средних значений NOC, NOA, SI.

4.        Рост средних значений OSAVG и NPAVG говорит о том, что сотрудничество между объектами усложнилось.

5.        Среднее значение СВО указывает на увеличение сцепления между классами (это нежелательно), зато снижение среднего значения LCOM свидетельствует, что связность внутри классов увеличилась (таким образом, снизилась вероятность ошибок в ходе разработки).

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

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

Для реализации этого требования вводится третья итерация конструирования.

Итерация 3 — разработка диалогового окна

 

Шаг 1: Спецификация представления диалогового окна.

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

1.      Диалоговое окно накапливает посылаемые в него символы, отображая их по мере получения.

2.      При получении символа конца сообщения (ENTER) полная строка текста принимается в функцию обратного вызова, связанную с диалоговым окном.

3.      Функция обратного вызова реализует обслуживание, требуемое пользователю.

4.      Функция обратного вызова обеспечивается прикладным программистом.

Шаг 2: Модификация диаграммы Use Case для WUI.

Очевидно, что дополнительное требование приводит к появлению дополнительного элемента Use Case, который находится в отношении «расширяет» с базовым г элементом Use Case Использование окон.

Диаграмма Use Case принимает вид, представленный на рис. 15.13.

 

Рис. 15.13. Модифицированная диаграмма Use Case для WUI

 

Шаг 3: Описание элемента Use Case Использование диалогового окна.

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

Шаг 4: Диаграмма последовательности Использование диалогового окна.

Диаграмма последовательности для сценария Использование диалогового окна показана на рис. 15.14.

 

Рис. 15.14. Диаграмма последовательности Использование диалогового окна

 

Шаг 5: Создание класса.

Для реализации сценария Использование диалогового окна создается новый класс Dialog, который является наследником класса Window. Объекты класса Dialog образуют диалоговые окна.

Класс Dialog переопределяет следующие операции, унаследованные от класса Window:

q       framework — формирование диалогового окна. Параметры операции: имя диалогового окна, координаты, ширина окна, заголовок окна и ссылка на функцию обратного вызова. Операция создает каркас окна, устанавливает для него функцию обратного вызова, делает окно видимым и регистрирует его в массиве управляемых окон;

q       send_to — обрабатывает пользовательский ввод, посылаемый в диалоговое окно. Окно запоминает символы, вводимые пользователем, а после нажатия пользователем клавиши ENTER вызывает функцию обратного вызова, обрабатывающую эти данные.

Конечное представление иерархии классов WUI показано на рис. 15.15. Результаты оценки качества проекта (в конце третьей итерации) сведены в табл. 15.5. Динамика изменения значений для метрик класса показана в табл. 15.6.

Таблица 15.5. Оценки качества WUI после третьей итерации

Метрика

lnput_ Manager

Window Manager

Screen

Root Window

Window

Menu

Menu-title

Dialog

Среднее значение

WMC

0,12

0,42

0,11

0

0,98

0,33

0,27

0,23

0,31

NOC

-

-

-

1

2

1

0

0

0,5

СВО

3

3

0

1

2

2

3

2

2

RFC

6

11

0

0

27

9

12

7

9,1

LCOM

3

0

5

0

0

0

0

0

1

CS

3/2

10/8

5/1

0/2

22/22

28/24

11/12

24/14

12,2/10,6

NOO

-

-

-

0

0

2

3

2

0,9

NOA

-

-

-

0

22

6

0

0

3,5

SI

-

-

-

0

0

0,23

0,46

0,27

0,14

OSAVG

4

4,2

2,2

0

4,45

4,13

9

11,5

4,9

NPAVG

0

1,3

1

0

2,18

4,63

1,67

4

1,8

Метрики, вычисляемые для системы

DIT

3

 

 

 

 

 

 

 

 

NC

8

 

 

 

 

 

 

 

 

NOM

50

 

 

 

 

 

 

 

 

LOC

246

 

 

 

 

 

 

 

 


















Таблица 15.6. Средние значения метрик класса на разных итерациях

Метрика

Итерация 1

Итерация 2

Итерация 3

WMC

0,3

0,32

0,31

NOC

0,2

0,4

0,5

СВО

1,8

2

2

RFC

8

9,4

9,1

LCOM

1,6

1,1

1

CS

7,2/7

11,3/10,1

12,2/10,6

NOO

0

0,7

0,9

NOA

3,6

4

3,5

SI

0

0,1

0,14

OSAVG

3

4,0

4,9

NPAVG

0,9

1,5

1,8

DIT

1

3

3

NC

5

7

8

NOM

35

48

50

LOC

148

223

246

 

Рис. 15.15. Конечная диаграмма классов WUI

 

Сравним средние значения метрик второй и третьей итераций:

1.      Общая сложность WUI возросла (увеличились значения LOC, NOM и NC), однако повысилось качество классов (уменьшились средние значения WMC и RFC).

2.      Увеличились возможности многократного использования классов (о чем свидетельствует рост среднего значения NOC и уменьшение среднего значения WMC).

3.      Возросла средняя связность класса (уменьшилось среднее значение метрики LCOM).

4.      Уменьшилось среднее значение сцепления класса (сохранилось среднее значение СВО и уменьшилось среднее значение RFC).

Вывод: качество проекта стало выше.

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

Таблица 15.7. Значения метрик Абреу для WUI

Метрика

Значение

МНF

0,49

AHF

0,49

MIF

0,49

AIF

0,29

POF

0,69

COF

0,25

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

Разработка в стиле экстремального программирования

 

Базовые понятия и методы ХР-процесса разработки обсуждались в разделе «ХР-процесс» главы 1. Напомним, что основная область применения ХР — небольшие проекты с постоянно изменяющимися требованиями заказчика [10], [11], [12], [75]. Заказчик может не иметь точного представления о том, что должно быть сделано. Функциональность разрабатываемого продукта может изменяться каждые несколько месяцев. Именно в этих случаях ХР позволяет достичь максимального успеха.

Основным структурным элементом ХР-процесса является ХР-реализация. Рассмотрим ее организацию.


ХР-реализация

 

Структура ХР-реализации показана на рис. 15.16.

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

 

Рис. 15.16. Структура ХР-реализации

 

Очень часто истории путают с традиционными требованиями к системе. Самое важное отличие состоит в уровне детализации. Истории обеспечивают только такие детали, которые необходимы для оценки времени их реализации (с минимальным риском). Когда наступит время реализовать историю, разработчики получают у заказчика более детальное описание требований.

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

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

Если разработчики не знают, как оценить историю, они создают выброс — быстрое решение, содержащее ответ на трудные вопросы. Выброс — минимальное решение, выполняемое в черновом коде и впоследствии выбрасываемое. Результат выброса — знание, достаточное для оценивания.

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

Следующий шаг — планирование реализации. Планирование устанавливает правила того, каким образом вовлеченные в проект стороны (заказчики, разработчики и менеджеры) принимают соответствующие решения. Главным итогом является план выпуска версий, охватывающий всю реализацию. Далее этот план используется для создания планов каждой итерации. Итерации детально планируются непосредственно перед началом каждой из них. Важнейшая задача — правильно оценить сроки выполнения работ по каждой из пользовательских историй. Часто при составлении плана заказчик пытается сократить сроки. Этого делать не стоит, чтобы не вызвать впоследствии проблем. Основная философия планирования строится на том, что имеются четыре параметра измерения проекта — объем, ресурсы, время и качество. Объем — сколько работы должно быть сделано, ресурсы — как много используется разработчиков, время — когда проект будет закончен, качество — насколько хорошо будет реализована и протестирована система. Можно, конечно, установить только три из четырех параметров, но равновесие всегда будет достигаться за счет оставшегося параметра.

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

В каждую ХР-реализацию многократно вкладывается базовый элемент — ХР-итерация. Рассмотрим организацию ХР-итерации.


ХР-итерация

 

Структура ХР-итерации показана на рис. 15.17.

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

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

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

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

 

Рис. 15.17. Структура ХР-итерации

 

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

Разработчики оценивают количество и длительность задач. Для определения фиксированной длительности итерации используют метрику «скорость проекта», вычисленную по предыдущей итерации (количество завершенных в ней задач/дней программирования). Если предполагаемая скорость превышает предыдущую скорость, заказчик выбирает истории, которые следует отложить до более поздней итерации. Если итерация слишком мала, к разработке принимается дополнительная история. Вполне допустимая практика — переоценка историй и пересмотр плана реализации после каждых трех или пяти итераций. Первоочередная реализация наиболее важных историй — гарантия того, что для клиента делается максимум возможного. Стиль разработки, основанный на последовательности итераций, улучшает подвижность процесса разработки.

В каждую ХР-итерацию многократно вкладывается строительный элемент — элемент ХР-разработки. Рассмотрим организацию элемента ХР-разработки.


Элемент ХР-разработки

 

Структура элемента ХР-разработки показана на рис. 15.18.

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

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

 

Рис. 15.18. Структура элемента ХР-разработки

 

Весь день ХР-разработчика проходит под лозунгом коллективного владения кодом программной системы. В результате этого происходит фиксация ошибок и добавление новой функциональности в систему.

Следует удерживаться от соблазна добавлять в продукт функциональность, которая будет востребована позже. Полагают, что только 10% такой функциональности будет когда-либо использовано, а потери составят 90% времени разработчика. В ХР считают, что дополнительная функциональность только замедляет разработку и исчерпывает ресурсы. Предлагается подавлять такие творческие порывы и концентрироваться на текущих, запланированных задачах.


Коллективное владение кодом

 

Организацию коллективного владения кодом иллюстрирует рис. 15.19.

Коллективное владение кодом позволяет каждому разработчику выдвигать новые идеи в любой части проекта, изменять любую строку программы, добавлять функциональность, фиксировать ошибку и проводить реорганизацию. Один человек просто не в состоянии удержать в голове проект нетривиальной системы. Благодаря коллективному владению кодом снижается риск принятия неверного решения (главным разработчиком) и устраняется нежелательная зависимость проекта от одного человека.

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

Кроме тестов модулей, создаются тесты приемки, они основываются на пользовательских историях. Эти тесты испытывают систему как «черный ящик» и ориентированы на требуемое поведение системы.

 

Рис. 15.19. Организация коллективного владения кодом

 

На основе результатов тестирования разработчики включают в очередную итерацию работу над ошибками. Вообще, следует помнить, что тестирование — один из краеугольных камней ХР.

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

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

Во время очередной итерации всех сотрудников перемещают на новые участки работы. Такие перемещения помогают устранить изоляцию знаний и «узкие места». Особенно полезна смена одного из разработчиков при парном программировании.

Замечено, что программисты очень консервативны. Они продолжают использовать код, который трудно сопровождать, только потому, что он все еще работает. Боязнь модификации кода у них в крови. Это приводит к предельному понижению эффективности систем. В ХР считают, что код нужно постоянно обновлять — удалять лишние части, убирать ненужную функциональность. Этот процесс называют реорганизацией кода (refactoring). Поощряется безжалостная реорганизация, сохраняющая простоту проектных решений. Реорганизация поддерживает прозрачность и целостность кода, обеспечивает его легкое понимание, исправление и расширение. На реорганизацию уходит значительно меньше времени, чем на сопровождение устаревшего кода. Увы, нет ничего вечного — когда-то отличный модуль теперь может быть совершенно не нужен.

И еще одна составляющая коллективного владения кодом — непрерывная интеграция.

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

По возможности ХР-разработчики должны интегрировать и публично отображать, демонстрировать код каждые несколько часов. Интеграция позволяет объединить усилия отдельных пар и стимулирует повторное использование кода.


Взаимодействие с заказчиком

 

Одно из требований ХР — постоянное участие заказчика в проведении разработки. По сути, заказчик является одним из разработчиков.

Все этапы ХР требуют непосредственного присутствия заказчика в команде разработчиков. Причем разработчикам нужен заказчик-эксперт. Он создает пользовательские истории, на основе которых оценивается время и назначаются приоритеты работ. В ходе планирования реализации заказчик указывает, какие истории следует включить в план реализации. Активное участие он принимает и при планировании итерации.

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

Таким образом, активное участие заказчика не только предотвращает появление некачественной системы, но и является непременным условием выполнения разработки.

Стоимость изменения и проектирование

 

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

В основе адаптивного (облегченного) ХР-процесса лежит предположение, что экспоненциальную кривую можно сгладить (рис. 15.21) [25], [30], [36], [37], [62]. Такое сглаживание, с одной стороны, возникает при применении методологии ХР, а с другой стороны, оно же в ней и применяется. Это еще раз подчеркивает тесную взаимосвязь между методами ХР: нельзя использовать методы, которые опираются на сглаживание, не используя другие методы, которые это сглаживание производят.

 

Рис. 15.20. Экспонента стоимости изменения в прогнозирующем процессе

 

 

Рис. 15.21. Сглаживание стоимости изменения в адаптивном процессе

 

К числу основных методов, осуществляющих сглаживание, относят:

q       тотальное тестирование;

q       непрерывную интеграцию;

q       реорганизацию (рефакторинг).

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

О влиянии реорганизации (рефакторинга) очень интересно пишет Джим Хайсмит (Jim Highsmith) [37]. Он приводит аналогию с весами (см. рис. 15.22 и 15.23). На одной чаше весов лежит предварительное проектирование, на другой — реорганизация. В прогнозирующих процессах разработки перевешивает предварительное проектирование, поскольку скорость изменений низкая (на рисунке скорость иллюстрируется положением точки равновесия). В адаптивных, облегченных процессах перевешивает реорганизация, так как скорость изменений высокая. Это не означает отказа от предварительного проектирования. Однако теперь можно говорить о существовании баланса между двумя подходами к проектированию, из которых можно выбрать наиболее подходящий подход.

 

Рис. 15.22. Балансировка проектирования и реорганизации при прогнозе

 

 

Рис. 15.23. Балансировка проектирования и реорганизации при адаптации

 

Итак, в адаптивных процессах вообще и в ХР-процессе в частности приветствуется простое проектирование.

Простое проектирование основывается на двух принципах:

q       это вам не понадобится;

q       ищите самое простое решение, которое может сработать.

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

Тем не менее ХР не советует заниматься созданием гибких элементов заранее. Лучше, если они будут добавляться по мере необходимости. Если сегодня нужен класс Арифметика, который выполняет только сложение, то сегодня я буду встраивать в этот класс именно сложение. Даже если я уверен, что уже на следующей итерации понадобится умножение и мне легко реализовать его сейчас, все равно следует отложить эту работу до следующей итерации — когда в ней появится реальная необходимость.

Экономически такое поведение оправданно — незапланированная работа всегда крадет ресурсы у запланированной. Кроме того, отклонение от плана — это нарушение соглашений с заказчиком. К тому же появляется риск сорвать выполнение текущей работы. И даже если появилось свободное время, решение о его заполнении принимает заказчик («он сверху видит все, ты так и знай!»).

И еще одно оправдание — возможность ошибиться, ведь у нас еще нет подробных требований заказчика. А чем раньше мы введем в проект ошибочное решение, тем хуже.

Теперь о простоте решения. ХР-идеолог Кент Бек приводит четыре критерия простой системы:

q       система успешно проходит все тесты;

q       код системы ясно раскрывает все изначальные замыслы;

q       в ней отсутствует дублирование кода;

q       используется минимально возможное количество классов и методов.

Успешное тестирование — довольно понятный критерий. Отсутствие дублирования кода, минимальное количество классов/методов — тоже ясные требования. А как расшифровать слова «раскрывает изначальные замыслы»?

ХР всячески подчеркивает, что хороший код — это Код, который можно легко прочесть и понять. Если вы хотите сделать комплимент ХР-разработчику и скажете, что он пишет «умный код», будьте уверены — вы оскорбили человека.

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

Такой стиль работы абсурден, если внедрять его в прогнозирующий, обычный процесс и игнорировать остальные методы ХР. В комплексе с остальными ХР-причудами он может стать действительно полезным.

Контрольные вопросы

 

1.                 Что является критерием управления унифицированным процессом разработки? Как он применяется?

2.                 Какую структуру имеет унифицированный процесс разработки?

3.                 Какие этапы входят в унифицированный процесс разработки? Поясните назначение этих этапов.

4.                 Какие рабочие потоки имеются в унифицированном процессе разработки? Поясните назначение этих потоков.

5.                 Какие модели предусмотрены в унифицированном процессе разработки? Поясните назначение этих моделей.

6.                 Какие технические артефакты определены в унифицированном процессе разработки? Поясните назначение этих артефактов.

7.                 В чем суть управления риском?

8.                 Какие действия определяют управление риском?

9.                 Какие источники проектного риска вы знаете? 10. Какие источники технического риска вы знаете? И. Какие источники коммерческого риска вы знаете?

12.      В чем суть анализа риска?

13.      В чем состоит ранжирование риска?

14.      В чем состоит планирование управления риском?

15.      Что означает разрешение и наблюдение риска? Поясните методику «Отслеживание 10 верхних элементов риска».

16.      Дайте характеристику целей, действий и результатов этапа НАЧАЛО.

17.      Дайте характеристику целей, действий и результатов этапа РАЗВИТИЕ.

18.      Дайте характеристику целей, действий и результатов этапа КОНСТРУИРОВАНИЕ.

19.      Дайте характеристику целей, действий и результатов этапа ПЕРЕХОД.

20.      Какие метрики используют для оценки качества унифицированного процесса разработки?

21.      Охарактеризуйте содержание ХР-реализации.

22.      В чем разница между пользовательскими историями и обычными требованиями к системе?

23.      Что такое выброс?

24.      Как создаются тесты приемки?

25.      Поясните содержание ХР-итерации.

26.      В чем заключается планирование ХР-итерации?

27.      Что такое скорость проекта?

28.      Поясните структуру элемента ХР-разработки.

29.      В чем заключается коллективное владение кодом? Охарактеризуйте содержание такого владения.

30.      Как организуется взаимодействие с ХР-заказчиком?

31.      Прокомментируйте стоимость ХР-изменения.

32.      Поясните особенности ХР-проектирования.

ГЛАВА 16. Объектно-ориентированное тестирование

 

Необходимость и важность тестирования ПО трудно переоценить. Вместе с тем следует отметить, что тестирование является сложной и трудоемкой деятельностью. Об этом свидетельствует содержание глав 6, 7 и 8 глав, в которых описывались классические основы тестирования, разработанные в расчете (в основном) на процедурное ПО. В этой главе рассматриваются вопросы объектно-ориентированного тестирования [17], [18], [42], [50], [51]. Существует мнение, что объектно-ориентированное тестирование мало чем отличается от процедурно-ориентированного тестирования. Конечно, многие понятия, подходы и способы тестирования у них общие, но в целом это мнение ошибочно. Напротив, особенности объектно-ориентированных систем должны вносить и вносят существенные изменения как в последовательность этапов, так и в содержание этапов тестирования. Сгруппируем эти изменения по трем направлениям:

q       расширение области применения тестирования;

q       изменение методики тестирования;

q       учет особенностей объектно-ориентированного ПО при проектировании тестовых вариантов.

Обсудим каждое из выделенных направлений отдельно.

Расширение области применения объектно-ориентированного тестирования

 

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

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

Критерии тестирования моделей: правильность, полнота, согласованность [18], [51].

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

О согласованности судят путем рассмотрения противоречий между элементами в модели. Несогласованная модель имеет в одной части представления, которые противоречат представлениям в других частях модели.

Для оценки согласованности нужно исследовать каждый класс и его взаимодействия с другими классами. Для упрощения такого исследования удобно использовать модель Класс— Обязанность— Сотрудничество CRC (Class— Responsibility — Collaboration). Основной элемент этой модели — CRC-карта [9], [76]. Кстати, CRC-карты — любимый инструмент проектирования в ХР-процессах.

По сути, CRC-карта — это расчерченная карточка размером 6 на 10 сантиметров. Она помогает установить задачи класса и выявить его окружение (классы-собеседники). Для каждого класса создается отдельная карта.

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

CRC-карта намеренно сделана простой, даже ее ограниченный размер имеет определенный смысл: если список обязанностей и сотрудников не помещается на карте, то, наверное, данный класс надо разделить на несколько классов.

Для оценки модели (диаграммы) классов на основе CRC-карт рекомендуются следующие шаги [50].

1.        Выполняется перекрестный просмотр CRC-карты и диаграммы сотрудничества (последовательности) объектов. Цель — проверить наличие сотрудников, согласованность информации в обеих моделях.

2.        Исследуются обязанности CRC-карты. Цель — определить, предусмотрена ли в карте сотрудника обязанность, которая делегируется ему из данной карты. Например, в табл. 16.1 показана CRC-карта Банкомат. Для этой карты выясняем, выполняется ли обязанность Читать карту клиента, которая требует использования сотрудника Карта клиента. Это означает, что класс Карта клиента должен иметь операцию, которая позволяет ему быть прочитанным.

Таблица 16.1. CRC-карта Банкомат

Имя класса: Банкомат

Обязанности:

Сотрудники:

Читать карту клиента

Идентификация клиента

Проверка счета

Выдача денег

Выдача квитанции

Захват карты

Карта клиента

База данных клиентов

База данных счетов

Блок денег

Блок квитанций

Блок карт

 

3.        Организуется проход по каждому соединению CRC-карты. Проверяется корректность запросов, выполняемых через соединения. Такая проверка гарантирует, что каждый сотрудник, предоставляющий услугу, получает обоснованный запрос. Например, если произошла ошибка и класс База данных клиентов получает от класса Банкомат запрос на Состояние счета, он не сможет его выполнить — ведь База данных клиентов не знает состояния их счетов.

4.        Определяется, требуются ли другие классы, или правильно ли распределены обязанности по классам. Для этого используют проходы по соединениям, исследованные на шаге 3.

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

6.        Шаги 1-5 применяются итеративно, к каждому классу и на каждом шаге эволюции объектно-ориентированной модели.

Изменение методики при объектно-ориентированном тестировании

 

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


Особенности тестирования объектно-ориентированных «модулей»

 

При рассмотрении объектно-ориентированного ПО меняется понятие модуля. Наименьшим тестируемым элементом теперь является класс (объект). Класс содержит несколько операций и свойств. Поэтому сильно изменяется содержание тестирования модулей.

В данном случае нельзя тестировать отдельную операцию изолированно, как это принято в стандартном подходе к тестированию модулей. Любую операцию приходится рассматривать как часть класса.

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

Выводы:

q       тестированию модулей традиционного ПО соответствует тестирование классов объектно-ориентированного ПО;

q       тестирование традиционных модулей ориентировано на поток управления внутри модуля и поток данных через интерфейс модуля;

q       тестирование классов ориентировано на операции, инкапсулированные в классе, и состояния в пространстве поведения класса.

Тестирование объектно-ориентированной интеграции

 

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

Р. Байндер предлагает две методики интеграции объектно-ориентированных систем [16]:

q       тестирование, основанное на потоках;

q       тестирование, основанное на использовании.

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

Д. МакГрегор считает, что одним из шагов объектно-ориентированного тестирования интеграции должно быть кластерное тестирование [50]. Кластер сотрудничающих классов определяется исследованием CRC-модели или диаграммы сотрудничества объектов. Тестовые варианты для кластера ориентированы на обнаружение ошибок сотрудничества.


Объектно-ориентированное тестирование правильности

 

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

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

Для подтверждения правильности может проводиться обычное тестирование «черного ящика».

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

Проектирование объектно-ориентированных тестовых вариантов

 

Традиционные тестовые варианты ориентированы на проверку последовательности: ввод исходных данных — обработка — вывод результатов — или на проверку внутренней управляющей (информационной) структуры отдельных модулей. Объектно-ориентированные тестовые варианты проверяют состояния классов. Получение информации о состоянии затрудняют такие объектно-ориентированные характеристики, как инкапсуляция, полиморфизм и наследование.

Инкапсуляция

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

Полиморфизм

При вызове полиморфной операции трудно определить, какая реализация будет проверяться. Пусть нужно проверить вызов функции

y=functionA(x).

В стандартном ПО достаточно рассмотреть одну реализацию поведения, которая обеспечивает вычисление функции. В объектно-ориентированном ПО придется рассматривать поведение реализации Базовый_класс :: functionA(х), Производный_клacc :: functionA(x), Наследиик_Производного_класса :: functionA(х). Здесь двойным двоеточием от имени операции отделяется имя класса, в котором размещена операция (это обозначение UML). Таким образом, в объектно-ориентированном контексте каждый раз при вызове functionA(x) следует рассматривать набор различных поведений. Конечно, подход к тестированию базовых и производных классов в основном одинаков. Разница состоит только в учете используемых системных ресурсов.

Наследование

Наследование также может усложнить проектирование тестовых вариантов. Пусть Родительский_класс содержит операции унаследована() и переопределена(). Дочерний_класс переопределяет операцию переопределена() по-своему. Очевидно, что реализация Дочерний_класс::переопределена() должна повторно тестироваться, ведь ее содержание изменилось. Но надо ли повторно тестировать операцию Дочерний_класс::унаследована()?

Рассмотрим следующий случай. Положим, что операция Дочерний_класс::унаследована() вызывает операцию переопределен(). К чему это приводит? Поскольку реализация операции переопределен() изменена, операция Дочерний_класс::унаследована() может не соответствовать этой новой реализации. Поэтому нужны новые тесты, хотя содержание операции унаследована() не изменено.

Важно отметить, что для операции Дочерний_класс::унаследована() может проводиться только подмножество тестов. Если часть операции унаследована() не зависит от операции переопределен(), то есть нет ее вызова или вызова любого кода, который косвенно ее вызывает, то ее не надо повторно тестировать в дочернем классе.

Родительский_класс::переопределена() и Дочерний_класс::переопределена() —это две разные операции с различными спецификациями и реализациями. Каждая из них проверяется самостоятельным набором тестов. Эти тесты нацелены на вероятные ошибки: ошибки интеграции, ошибки условий, граничные ошибки и т. д. Однако сами операции, как правило, похожи. Наборы их тестов будут перекрываться. Чем лучше качество объектно-ориентированного проектирования, тем больше перекрытие. Таким образом, новые тесты надо формировать только для тех требований к операции Дочерний_класс::переопределена(), которые не покрываются тестами для операции Родительский_класс::переопределена().

Выводы:

q       тесты для операции Родительский_класс::переопределена() частично применимы к операции Дочерний_класс::переопределена();

q       входные данные тестов подходят к операциям обоих классов;

q       ожидаемые результаты для операции Родительского_класса отличаются от ожидаемых результатов для операции Дочернего_класса.

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

Способы тестирования «черного ящика» также применимы к объектно-ориентированным системам. Полезную входную информацию для тестирования «черного ящика» и тестирования состояний обеспечивают элементы Use Case.


Тестирование, основанное на ошибках

 

Цель тестирования, основанного на ошибках, — проектирование тестов, ориентированных на обнаружение предполагаемых ошибок [46]. Разработчик выдвигает гипотезу о предполагаемых ошибках. Для проверки его предположений разрабатываются тестовые варианты.

В качестве примера рассмотрим булево выражение

if (X and not Y or Z).

Инженер по тестированию строит гипотезу о предполагаемых ошибках выражения:

q       операция not сдвинулась влево от нужного места (она должна применяться к Z);

q       вместо and должно быть or;

q       вокруг not Y or Z должны быть круглые скобки.

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

Обсудим первую предполагаемую ошибку. Значения (X = false, Y = false, Z = false) устанавливают указанное выражение в false. Если вместо not Y должно быть not Z, то выражение принимает неправильное значение, которое приводит к неправильному ветвлению. Аналогичные рассуждения применяются к генерации тестов по двум другим ошибкам.

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

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

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

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


Тестирование, основанное на сценариях

 

Тестирование, основанное на ошибках, оставляет в стороне два важных типа ошибок:

q       некорректные спецификации;

q       взаимодействия между подсистемами.

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

Ошибки, связанные с взаимодействием подсистем, происходят, когда поведение одной подсистемы создает предпосылки для отказа другой подсистемы.

Тестирование, основанное на сценариях, ориентировано на действия пользователя, а не на действия программной системы [47]. Это означает фиксацию задач, которые выполняет пользователь, а затем применение их в качестве тестовых вариантов. Задачи пользователя фиксируются с помощью элементов Use Case.

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

Рассмотрим, например, проектирование тестов, основанных на сценариях, для текстового редактора.

Рабочие сценарии опишем в виде спецификаций элементов Use Case.

Элемент Use Case: Исправлять черновик.

Предпосылки: обычно печатают черновик, читают его и обнаруживают ошибки, которые не видны на экране. Данный элемент Use Case описывает события, которые при этом происходят.

1. Печатать весь документ.

2. Прочитать документ, изменить определенные страницы.

3. После внесения изменения страница перепечатывается.

4. Иногда перепечатываются несколько страниц.

Этот сценарий определяет как требования тестов, так и требования пользователя. Требования пользователя очевидны, ему нужны:

q       метод для печати отдельной страницы;

q       метод для печати диапазона страниц.

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

Элемент Use Case: Печатать новую копию.

Предпосылки: кто-то просит пользователя напечатать копию документа.

1. Открыть документ.

2. Напечатать документ.

3. Закрыть документ.

И в этом случае подход к тестированию почти очевиден. За исключением того, что не определено, откуда появился документ. Он был создан в ранней задаче. Означает ли это, что только эта задача влияет на сценарий?

Во многих современных редакторах запоминаются данные о последней печати документа. По умолчанию эту печать можно повторить. После сценария Исправлять черновик достаточно выбрать в меню Печать, а затем нажать кнопку Печать в диалоговом окне — в результате повторяется печать последней исправленной страницы. Таким образом, откорректированный сценарий примет вид:

Элемент Use Case: Печатать новую копию.

1.      Открыть документ.

2.      Выбрать в меню пункт Печать.

3.      Проверить, что печаталось, и если печатался диапазон страниц, то выбрать опцию Печатать целый документ.

4.      Нажать кнопку Печать.

5.      Закрыть документ.

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

Разработчик может опустить эту зависимость в тестовом варианте, но, вероятно, проблема обнаружится в ходе тестирования. И тогда разработчик будет выкрикивать: «Я предполагал, я предполагал это учесть!!!».


Тестирование поверхностной и глубинной структуры

 

Поверхностная структура — это видимая извне структура объектно-ориентированной системы. Она отражает взгляд пользователя, который видит не функции, а объекты для обработки. Тестирование поверхностной структуры основывается на задачах пользователя. Главное — выяснить задачи пользователя. Для разработчика это нетривиальная проблема, поскольку требует отказа от своей точки зрения.

Глубинная структура отражает внутренние технические подробности объектно-ориентированной системы (на уровне проектных моделей и программного текста). Тесты глубинной структуры исследуют зависимости, поведение и механизмы взаимодействия, которые создаются в ходе проектирования подсистем и объектов.

В качестве базиса для тестирования глубинной структуры используются модели анализа и проектирования. Например, разработчик исследует диаграмму взаимодействия (невидимую извне) и спрашивает: «Проверяет ли тест сотрудничество, отмеченное на диаграмме?»

Диаграммы классов обеспечивают понимание структуры наследования, которая используется в тестах, основанных на ошибках. Рассмотрим операцию ОБРАБОТАТЬ (Ссылка_на_РодительскийКласс). Что произойдет, если в вызове этой операции указать ссылку на дочерний класс? Есть ли различия в поведении, которые должны отражаться в операции ОБРАБОТАТЬ? Эти вопросы инициируют создание конкретных тестов.

Способы тестирования содержания класса

 

Описываемые ниже способы ориентированы на отдельный класс и операции, которые инкапсулированы классом.

Стохастическое тестирование класса

 

При стохастическом тестировании исходные данные для тестовых вариантов генерируются случайным образом. Обсудим методику, предложенную С. Кирани и В.Тсай[43].

Рассмотрим класс Счет, который имеет следующие операции: Открыть, Установить, Положить, Снять, Остаток, Итог, ОграничитьКредит, Закрыть.

Каждая из этих операций применяется при определенных ограничениях:

q       счет должен быть открыт перед применением других операций;

q       счет должен быть закрыт после завершения всех операций.

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

Открыть ? Установить ? Положить ? Снять ? Закрыть.

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

Открыть ? Установить ? Положить ? [Остаток?Снять?Итог?ОграничитьКредит?Положить]n ? Снять ? Закрыть.

Здесь приняты дополнительные обозначения: точка означает операцию И/ИЛИ, пара квадратных скобок — группировку, а показатель степени — количество повторений группировки.

Набор различных последовательностей может генерироваться случайным образом:

Тестовый вариант N:

Открыть ? Установить ? Положить ? Остаток ? Снять ?Итог ? Снять ? Закрыть.

Тестовый вариант М:

Открыть ? Установить ? Положить ? Итог ? ОграничитьКредит ? Снять ? Остаток ? Снять ? Закрыть.

Эти и другие тесты случайных последовательностей проводятся для проверки различных вариантов жизни объектов.


Тестирование разбиений на уровне классов

 

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

Обычно используют одну из трех категории разбиения [43]. Категории образуются операциями класса.

Первый способ — разбиение на категории по состояниям. Основывается на способности операций изменять состояние класса. Обратимся к классу Счет. Операции Снять, Положить изменяют его состояние и образуют первую категорию. Операции Остаток, Итог, ОграничитьКредит не меняют состояние Счета и образуют вторую категорию. Проектируемые тесты отдельно проверяют операции, которые изменяют состояние, а также те операции, которые не изменяют состояние. Таким образом, для нашего примера:

Тестовый вариант 1:

Открыть ?Установить ?Положить ?Положить ?Снять ?Снять ?Закрыть.

Тестовый вариант 2:

Открыть ?Установить ?Положить ?Остаток ?Итог ?ОграничитьКредит ?Снять ?Закрыть.

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

Второй способ —разбиение на категории по свойствам. Основывается на свойствах, которые используются операциями. В классе Счет для определения разбиений можно использовать свойства остаток и ограничение кредита. Например, на основе свойства ограничение кредита операции подразделяются на три категории:

1) операции, которые используют ограничение кредита;

2) операции, которые изменяют ограничение кредита;

3) операции, которые не используют и не изменяют ограничение кредита.

Для каждой категории создается тестовая последовательность.

Третий способ — разбиение на категории по функциональности. Основывается на общности функций, которые выполняют операции. Например, операции в классе Счет могут быть разбиты на категории:

q       операции инициализации (Открыть, Установить);

q       вычислительные операции (Положить, Снять);

q       запросы (Остаток, Итог, ОграничитьКредит);

q       операции завершения (Закрыть).

Способы тестирования взаимодействия классов

 

Для тестирования сотрудничества классов могут использоваться различные способы [43]:

q       стохастическое тестирование;

q       тестирование разбиений;

q       тестирование на основе сценариев;

q       тестирование на основе состояний.

В качестве примера рассмотрим программную модель банковской системы, в состав которой входят классы Банк, Банкомат, ИнтерфейсБанкомата, Счет, Работа с наличными, ПодтверждениеПравильности, имеющие следующие операции:

Банк:

 

 

ПроверитьСчет( );

ЗапросДепозита ( );

РазрешитьКарту( );

ПроверитьРIN( );

ИнфоСчета( );

СнятьРазрешен( );

ПроверитьПолис( );

ОткрытьСчет( );

ЗакрытьСчет( ).

ЗапросСнятия( );

НачальнДепозит( );

 

Банкомат:

 

 

КартаВставлена( );

Положить( );

СостояниеСчета( );

Пароль( );

Снять( );

Завершить( ).

ИнтерфейсБанкомата:

 

 

ПроверитьСостояние( );

ВыдатьНаличные( );

ЧитатьИнфоКарты( );

СостояниеПоложить( );

ПечатьСостСчета( );

ПолучитьКолвоНалич( ).

Счет:

 

 

ОграничКредит( );

Остаток) );

Положить( );

ТипСчета( );

Снять( );

Закрыть( ).

ПодтверждениеПравильности:

ПодтвРIN( );

ПодтвСчет( ).

 

 

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

 

Рис. 16.1. Диаграмма сотрудничества банковской системы


Стохастическое тестирование

 

Стохастические тестовые варианты генерируются следующей последовательностью шагов.

1.           Для создания тестов используют списки операций каждого класса-клиента. Операции будут посылать сообщения в классы-серверы.

2.           Для каждого созданного сообщения определяется класс-сотрудник и соответствующая операция в классе-сервере.

3.           Для каждой операции в классе-сервере, которая вызывается сообщением из класса-клиента, определяются сообщения, которые она, в свою очередь, посылает.

4.           Для каждого из сообщений определяется следующий уровень вызываемых операций; они вставляются в тестовую последовательность.

В качестве примера приведем последовательность операций для класса Банк, вызываемых классом Банкомат:

ПроверитьСчет ?ПроверитьРIN ?[[ПроверитьПолис ?

ЗапросСнятия]?ЗапросДепозита?ИнфоСчета]n.

ПРИМЕЧАНИЕ

Здесь приняты следующие обозначения: стрелка означает операцию следования, точка — операцию И/ИЛИ, пара квадратных скобок — группировку операций классов, показатель степени — количество повторений группировки из операций классов.

 

Случайный тестовый вариант для класса Банк может иметь вид

Тестовый вариант N: ПроверитьСчет ?ПроверитьРШ ?ЗапросДепозита.

Для выявления сотрудников, включенных в этот тест, рассматриваются сообщения, связанные с каждой операцией, записанной в ТВ N. Для выполнения заданий ПроверитьСчет и ПроверитьРТМ Банк должен сотрудничать с классом ПодтверждениеПравильности. Для выполнения задания ЗапросДепозита Банк должен сотрудничать с классом Счет. Отсюда новый ТВ, который проверяет отмеченные сотрудничества:

Тестовый вариант М: ПроверитьСчетБанк ?(ПодтвСчетПодтвПрав) ?ПроверитьРINБанк ?(ПодтвРШПодтвПрав) ?ЗапросДепозитаБанк ?(ПоложитьСчет).

В этой последовательности операции классов-сотрудников Банка помещены в круглые скобки, индексы отображают принадлежность операций к конкретным классам.


Тестирование разбиений

 

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

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

Тестирование на основе состояний

 

В качестве источника исходной информации используют диаграммы схем состояний, фиксирующие динамику поведения класса. Данный способ позволяет получить набор тестов, проверяющих поведение класса и тех классов, которые сотрудничают с ним [43]. В качестве примера на рис. 16.2 показана диаграмма схем состояний класса Счет.

 

Рис. 16.2. Диаграмма схем состояний класса Счет

 

Видим, что объект Счета начинает свою жизнь в состоянии Пустой счет, а заканчивает жизнь в состоянии Удалить счет. Наибольшее количество событий (и действий) связано с состоянием Работа счета. Для упрощения рисунка здесь принято, что имена событий совпадают с именами действий (поэтому действия не показаны).

Проектируемые тесты должны обеспечить покрытие всех состояний. Это значит, что тестовые варианты должны инициировать переходы через все состояния объекта:

Тестовый вариант 1: Открыть ?УстановитьСчет ?Положить (начальное) ?Снять (конечное) ?Закрыть.

Отметим, что эта последовательность аналогична минимальной тестовой последовательности. Добавим к минимальной последовательности дополнительные тестовые варианты:

Тестовый вариант 2: Открыть ?УстановитьСчет ?Положить (начальное) ?Положить ?Остаток ?Кредит ?Снять (конечное) ?Закрыть

Тестовый вариант 3: Открыть ?Установить ?Положить (начальное) ?Положить ?Снять ?ИнфоСчета ?Снять (конечное) ?Закрыть

Для гарантии проверки всех вариантов поведения количество тестовых вариантов может быть увеличено. Когда поведение класса определяется в сотрудничестве с несколькими классами, для отслеживания «потока поведения» используют набор диаграмм схем состояний, характеризующих смену состояний других классов.

Возможна другая методика исследования состояний.— «преимущественно в ширину».

В этой методике:

q       каждый тестовый вариант проверяет один новый переход;

q       новый переход можно проверять, если полностью проверены все предшествующие переходы, то есть переходы между предыдущими состояниями.

Рассмотрим объект Карта клиента (рис. 16.3). Начальное состояние карты Не определена, то есть не установлен номер карты. После чтения карты (в ходе диалога с банкоматом) объект переходит в состояние Определена. Это означает, что определены банковские идентификаторы Номер Карты и Дата Истечения Срока. Карта клиента переходит в состояние Предъявляется на рассмотрение, когда проводится ее авторизация, и в состояние Разрешена, когда авторизация подтверждается. Переход карты клиента из одного состояния в другое проверяется отдельным тестовым вариантом.

 

Рис. 16.3. Тестирование «преимущественно в ширину»

 

Подход «преимущественно в ширину» требует: нельзя проверять Разрешена перед проверкой Не определена, Определена и Предъявляется на рассмотрение. В противном случае нарушается условие этого подхода: перед тестированием текущего перехода должны быть протестированы все переходы, ведущие к нему.

Предваряющее тестирование при экстремальной разработке

 

Предваряющее тестирование и рефакторинг (реорганизация) — основной способ разработки при экстремальном программировании.

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

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

Для демонстрации такого подхода рассмотрим пример конкретной разработки. Будем опираться на технику, описанную Робертом Мартином (с любезного разрешения автора)*.

* Robert C. Martin. RUP/XP Guidelines: Test-first Design and Refactoring. - Rational Software White Paper, 2000.

 

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

Для экстремального тестирования удобно использовать среду Junit, авторами которой являются Кент Бек и Эрик Гамма (Kent Beck и Erich Gamma). Прежде всего создадим среду для хранения тестов модулей. Это очень важно для предваряющего тестирования: вначале пишется тестовый вариант, а только потом — код программы. Необходимый код имеет следующий вид:


Листинг 16.1.
ТестЛакомки. java

import junit.framework.*;

public class ТестЛакомки extends TestCase

{

public ТестЛакомки (String name)

{

super(name);

}

}

Видно, что при использовании среды Junit класс-контейнер тестовых вариантов должен быть наследником от класса TestCase. Кстати, условимся весь новый код выделять полужирным шрифтом.

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

Листинг 16.2. ТестЛакомки.jауа

import junit.framework.*;

public class ТестЛакомки extends TestCase

{

public ТестЛакомки (String name)

{

super(name);

}

public void тестСоздатьПосещениеКафе()

{

ПосещениеКафе v = new-ПосещениеКафе();

}

}

Для компиляции этого фрагмента подключим класс ПосещениеКафе.

Листинг 16.3. ТестЛакомки.jаvа и ПосещениеКафе.jаvа

ТестЛакомки.jаvа

import junit. framework.*;

import ПосещениеКафе;

public class ТестЛакомки extends TestCase

{

public ТестЛакомки (String name)

{

super(name);

}

public void тестСоздатьПосещениеКафе()

{

ПосещениеКафе v = new ПосещениеКафе();

}

}

ПосещениеКафе.java

public class ПосещениеКафе

{

}

Этот код компилируется, тест проходит, и мы готовы добавить необходимую функциональность.

Листинг 16.4. ТестЛакомки.jауа и ПосещениеКафе.jауа

ТестЛакомки.java

import junit.framework.*;

import ПосещениеКафе;

import java.util.Date

public class ТестЛакомки extends TestCase

{

public TecтЛакомки(String name)

{

super(name):

}

public void тестСоздатьПосещениеКафе()

{

Date дата = new Date();

double булочки = 7.0; // 7 булочек

double стоимость = 12.5 * 7;

// цена 1 булочки - 12.5 руб.

double вес = 60.0; // взвешивание лакомки

double дельта = 0.0001; // точность

ПосещениеКафе v =

new ПосещениеКафе(дата, булочки, стоимость, вес);

assertEquals(дата, v.получитьДату( ));

assertEquals(12.5 * 7, v.получитьСтоииость(), дельта);

assertEquals(7.0, v.получитьБулочки(), дельта);

assertEquals(60.0, v.получитьВес(), дельта);

assertEquals(12.5, v.получитьЦену(). дельта);

}

}

ПосещениеКафе.java

import Java.uti1.Date;

public class ПосещениеКафе

{

private Date егоДата;

private double егоБулочки;

private double егоСтоимость;

private double eroBec;

public ПосещениеКафе(Date дата, double булочки,

double стоимость, double вес)

{

егоДата = дата;

егоБулочки = булочки;

егоСтоимость = стоимость;

егоВес = вес;

}

public Date получитьДату() {return егоДата;}

public double получитьБулочки() {return егоБулочки;}

public double получитьСтоимость() {return егоСтоимость;}

public double получитьЦену(){return егоСтоимость/егоБулочки;}

public double получитьВес() {return eroBec;}

}

На этом шаге мы добавили тесты в класс ТестЛакомки, а также добавили методы в класс ПосещениеКафе. Унаследованные методы assertEquals позволяют проводить сравнение ожидаемых и фактических результатов тестирования.

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

Далее определимся с хранением объектов класса ПосещениеКафе. Очевидно, что свойство егоВес характеризует лакомку. Таким образом, объект ПосещениеКафе записывает часть состояния лакомки па момент посещения кафе. Следовательно, нужно создать объект Лакомка и содержать объекты класса ПосещениеКафе в нем.


Листинг 16.5.
ТестЛакомки.java и Лакомка.java

ТестЛакомки.java

import junit.framework.*;

import ПосещениеКафе;

import java.util.Date

public class ТестЛакомки extends TestCase

{

public TecтЛакомки(String name)

{

super(name);

}

public void тестСоздатьЛакомку()

{

Лакомка g = new Лакомка();

assertEquals(0, д.получитьЧислоПосещений());

}

}

Лакомка.Java

public class Лакомка

{

public int получитьЧислоПосещений()

{

return 0;

}

}

Листинг 16.5 показывает начальный шаг. Мы написали новую тестовую функцию тестСоздатьЛакомку. Эта функция создает объект класса Лакомка и затем убеждается, что хранимое количество посещений равно 0. Конечно, реализация метода получитьЧислоПосещений неверна, но она обеспечивает прохождение теста. Это позволит нам в будущем выполнить рефакторинг (для улучшения решения).

Введем в класс Лакомку объект-контейнер, хранящий данные о разных посещениях (как элементы списка в массиве изменяемого размера). Для его создания используем класс-контейнер Array List из библиотеки Java 2. В будущем нам потребуются три метода контейнера: add (добавить элемент в контейнер), get (получить элемент из контейнера), size (вернуть количество элементов в контейнере).

Листинг 16.6. ЛАKOMKА.java

import java.util.ArrayList;

public class Лакомка

{

private ArrayList егоПосещения = new ArrayList();

// создание объекта егоПосещения - контейнера посещений

public int получитьЧислоПосещений ()

{

return егоПосещения.size();

// возврат количества элементов в контейнере

// оно равно количеству посещений кафе

}

}

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

На следующем шаге следует определить, как к Лакомке добавляется посещение кафе. Так будет выглядеть простейший тестовый вариант:

Листинг 16.7. TecтЛакомки.java

public void тестДобавитьПосещение()

{

double булочки = 7.0; // 7 булочек

double стоимость = 12.5 * 7; // цена 1 булочки = 12.5 руб.

double вес = 60.0; // взвешивание лакомки

double дельта = 0.0001; // точность

Лакомка g = new Лакомка();

g.добавитьПосещениеКафе(булочки, стоимость, вес);

assertEquals(1, g.получитьЧислоПосещений());

}

В этом тесте объект класса ПосещениеКафе не создается. Очевидно, что создавать объект и добавлять его в список должен метод добавитьПосещениеКафе объекта Лакомка.

Листинг 16.8. Лакомка.jауа

public void добавитьПосещениеКафе((double булочки, double стоимость, double вес)

{

ПосещениеКафе v =

new ПосещениеКафе(new Date(), булочки, стоимость, вес);

егоПосещения.add(v);

// добавление эл-та v в контейнер посещений

}

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

Листинг 16.9. ТестЛакомки.jауа

import junit.framework.*;

import ПосещениеКафе;

import java.util.Date;

public class ТестЛакомки extends TestCase

{

private double булочки - 7.0; // 7 булочек

private double стоимость = 12.5 * 7;

// цена 1 булочки = 12.5 p.

private double вес = 60.0; // взвешивание лакомки

private double дельта = 0.0001; // точность

public ТестЛакомки(String name)

{

super(name);

}

public void тестСоздатьПосещениеКафе()

{

Date дата = new Date();

ПосещениеКафе v = new ПосещениеКафе(дата. булочки.

стоимость, вес);

assertEquals(date, v.получитьДату());

assertEquals(12.5 * 7. v.получитьСтоимость(). дельта);

assertEquals(7.0. v.получитьБулочки(). дельта);

assertEquals(60.0. v.получитьВес(), дельта);

assertEquals(12.5. v.получитьЦену(). дельта):

}

public void тестСоздатьЛакомку()

{

Лакомка g = new Лакомка ();

assertEquals(0. g.получитьЧислоПосещений());

}

public void тестДобааитьПосещение()

{

Лакомка g = new Лакомка();

g.добавитьПосещениеКафе(булочки. стоимость, вес);

assertEquals(1. g.получитьЧислоПосещениРК));

}

}

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

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


Листинг 16.10.
TecтЛакомки.java

public void тестОтчетаОдногоПосещения()

{

Лакоика g = new Лакоика();

g.добавитьПосещениеКафе(булочки. стоимость, вес);

Отчет r = g.создатьОтчет();

assertEquals(0. r.получитьИзменениеВеса(), дельта);

assertEqualз(булочки, г.получитьПотреблениеБулочек(),

дельта);

assertEquals(0, r.получитьВесНаБулочку(), дельта);

assertEquals(стоимость. r.получитьСтоимостьБулочек(),

дельта);

}

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

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

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

Листинг 16.11. Лакомка.java, TecтЛакомки.java и Отчет.jаvа

Лакомка.java

public Отчет создатьОтчет()

{

return new Отчет();

}

ТестЛакомки.java

public void тестОтчетаОдногоПосещения()

{

Лакомка g = new Лакомка();

g.добавитьПосещениеКафе(булочки, стоимость, вес);

Отчет r = g.создатьОтчет();

assertEquals(0, r.получитьИзменениеВеса(), дельта);

assertEquals(булочки. r.получитьПотреблениеБулочек(),

дельта);

assertEquals(0. r.получитьВесНаБулочку(), дельта);

assertEquals(cтоимость,. r.получитьСтоимостьБулочек(),.

дельта);

}

Отчет.java

public class Отчет

{

public double получитьИзменениеВеса()

{return егоИзменениеВеса;}

public double получитьВесНаБулочку()

{return егоВесНаБулочку;}

public double получитьСтоииостьБулочек()

{return егоСтоимостьБулочек;}

public double получитьЛотреблениеБулочек()

{return егоПотреблениеБулочек;}

private double егоИзменениеВеса;

private double егоВесНаБулочку;

private double егоСтоимостьБулочек;

private double егоПотреблениеБулочек;

}

Код в листинге 16.11 компилируется и запускается, но его недостаточно для того, чтобы прошли тесты. Нужен рефакторинг кода. Для начала сделаем минимально возможные изменения.

Листинг 16.12. Лакомка.java и Отчет.java

Лакомка.java

public Отчет создатьОтчет()

{

Отчет r = new Отчет();

ПосещениеКафе v = (ПосещениеКафе) егоПосещения. Get(0);

// занести в v первый элемент из контейнера посещений

r.устВесНаБулочку(0);

r.устИзменениеВеса(0);

r.устСтоимостьБулочек(v.получитьСтоимость());

r.устПотреблениеБулочек(v.получитьБулочки()):

return r;

}

Отчет.java

public void устВесНаБулочку (double wpb)

{егоВесНаБулочку = wpb;}

public void устИзменениеВес(double kg)

{егоИзменениеВеса = kg;}

public void устСтоимостьБулочек(double ct)

(егоСтоимостьБулочек = ct;}

public void устПотреблениеБулочек (double b)

{егоПотреблениеБулочек = b;}

Предполагаем, что Лакомке разрешено только одно посещение. В этой версии метода создатьОтчет устанавливаются и возвращаются значения свойств Отчета.

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

Для завершения кода продумаем тесты для Лакомки без посещений и с несколькими посещениями кафе. Начнем с теста и кода для варианта без посещений.

Листинг 16.13. TecтЛакомки.java и Лакомка.jауа

ТестЛакомки.java

public void тестОтчетаБезПосещений()

{

Лакомка g = new Лакомка();

Отчет r= g.создатьОтчет();

assertEquals(0, r.получитьИзменениеВеса(). дельта);

assertEquals(0, r.получитьПотреблениеБулочек(), дельта);

assertEquals(0, r.получитьВесНаБулочку()), дельта;

assertEquals(0, r.получитьСтоимостьБулочек(), дельта);

}

Лакомка.Java

public Отчет создатьОтчет()

{

Отчет r = new Отчет();

if (егоПосещения.size() = 0)

{

r.устВесНаБулочку(0);

r.устИзиенениеВеса(0);

r.устСтоимостьБулочек(0);

r.устПотреблениеБулочек(0);

}

else

{

ПосещениеКафе v = (ПосещениеКафе) егоПосещения.get(0);

// занести в v первый элемент из контейнера посещений

r.устВесНаБулочку(0);

r.устИзменениеВеса(0);

r.устСтоимостьБулочек(v.получитьСтоимость());

r. устПотреблениеБулочек (v.получитьБулочки ()):

}

return r;

}

Теперь начнем создавать тестовый вариант для нескольких посещений.

Листинг 16.14. ТестЛакомки.jауа

public void тестОтчетаНесколькихПосещений()

{

Лакомка g = new Лакомка();

g.добавить(ПосещениеКафе(7. 87.5, 60.7);

g.добавитьПосещениеКафе(14. 175, 62.1);

g.добавитьПосещениеКафе(28, 350. 64.9);

Отчет r= g.создатьОтчет();

assertEquals(4.2, r.получитьИзменениеВеса(), дельта);

assertEquals(49. r.получитьПотреблениеБулочек(), дельта);

assertEquals(0.086, r.получитьВесНаБулочку(), дельта);

assertEquals(612.5, r.получитьСтоииостьБулочек(), дельта);

}

Мы установили число посещений для Лакомки равным трем. Предполагается, что цена булочки составляет 12,5 руб., а изменение веса — 0,1 кг на одну булочку. Таким образом, за 175 руб. лакомка покупает и съедает 14 булочек, полнея на 1,4 кг.

Но здесь какая-то ошибка. Скорость изменения веса должна определяться коэффициентом 0,1 кг на одну булочку. А если разделить 4,2 (изменение веса) на 49 (количество булочек), то получаем коэффициент 0,086. В чем причина несоответствия?

После размышлений становится понятно, что вес лакомки регистрируется на выходе из кафе. Поэтому приращение веса и потребление булочек во время первого посещения не учитывается. Изменим исходные данные теста.


Листинг 16.15.
ТестЛакомки.jауа

public void тестОтчетаНесколькихПосещений()

{

Лакомка g = new Лакомка();

g.добавитьПосещениеКафе(7. 87.5. 60.7);

g.добавитьПосещениеКафе(14. 175. 62.1);

g.добавитьПосещениеКафе(28. 350. 64.9);

Отчет r - g.создатьОтчет();

assertEquals(4.2, r.получитьИзменениеВеса(), дельта);

assertEquals(42, r.получитьПотреблениеБулочек(), дельта);

assertEquals(0.1, r.получитьВесНаБулочку(), дельта);

assertEquals(612.5, r.получитьСтоимостьБулочек(), дельта);

}

Этот тест корректен. Никогда не известно, с чем встретишься при написании тестов. Можно быть уверенным лишь в том, что, определяя понятия дважды (при написании тестов и кода), вы найдете больше ошибок, чем при простом написании кода.

Теперь добавим код, обеспечивающий прохождение теста из листинга 16.15.

Листинг 16.16. Лакомка.java

public Отчет создатьОтчет()

{

Отчет r = new Отчет ();

if (егоПосещения.size() = 0)

{

r.устВесНаБулочку(0);

r.устИзменениеВеса(0);

r.устСтоимостьБулочек(0);

r.устПотреблениеБулочек(0);

}

else if (егоПосещения.size() = 1)

{

ПосещениеКафе v = (ПосещениеКафе) егоПосещения.get(0);

// занести в v первый элемент из контейнера посещений

r.устВесНаБулочку(0);

r.устИзменениеВеса(0);

r.устСтоимостьБулочек(v.получитьСтоимость());

r.устПотреблениеБулочек(v.получитьБулочки());

}

else

{

double первыйЗамер = 0;

double последнийЗамер = 0;

double общаяСтоиность = 0;

double потреблениеБулочек = 0;

for (int i = 0; i < егоПосещения.size(); i++)

// проход по всем элементам контейнера посещений

{

ПосещениеКафе v = (ПосещениеКафе)

егоПосещения.get(i);

// занести в v 1-й элемент из контейнера посещений

if (i = = 0)

{

первыйЗамер = v.получитьВес();

// занести в первыйЗамер вес при 1-м посещении

потреблениеБулочек -= v.получитьБулочки();

}

if (i= = егоПосещения.size()- 1) последнийЗамер =

v.получитьВес();

// занести в последнийЗамер вес при послед, посещении

общаяСтоимость += v.получитьСтоиность();

потреблениеБулочек += v.получитьБулочки();

}

double изменение = последнийЗамер - первыйЗамер;

r.устВесНаБулочкуСизменение/потреблениеБулочек);

r.устИзненениеВеса(иэненение);

r.устСтоиностьБулочек(общаяСтоиность):

r.устПотреблениеБулочек(потреблениеБулочек);

}

return r;

}

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

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

Листинг 16.17. Лакомка.java

public Отчет создатьОтчет()

{

Отчет r = new Отчет();

double первыйЗамер = 0;

double последнийЗамер = 0;

double общаяСтоимость = 0;

double потреблениеБулочек = 0;

for (int i= 0; i< егоПосещения.size(); i++)

// проход по всем элементам контейнера посещений

{

ПосещениеКафе v = (ПосещениеКафе) егоПосещения.get(i);

// занести в v i-й элемент из контейнера посещений

if (i = = 0)

{

первыйЗамер = v. ПолучитьВес();

//занести в первыйЗамер вес при 1-м посещении

потреблениеБулочек -= v.получитьБулочки();

}

if (i= = егоПосещения.size()- 1) последнийЗамер =

v. получитьВес();

// занести в последнийЗамер вес при послед. посещении

общаяСтоимость += v.получитьСтоимость();

потреблениеБулочек += v.получитьБулочки();

}

double изменение = последнийЗамер – первыйЗамер;

r.устВесНаБулочку(изменение/потреблениеБулочек);

r.устИзменениеВеса(изменение);

r.устСтоимостьБулочек(общаяСтоимость);

r.устПотреблениеБулочекСпотреблениеБулочек);

return r;

}

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

Листинг 16.18. Лакомка.java

public Отчет создатьОтчет()

{

Отчет г = new Отчет();

double изменение = 0;

double общаяСтоимость = 0;

double потреблениеБулочек =0;

double первыеБулочки = 0; double wpb =0;

if (егоПосещения.size() > 0)

{

ПосещениеКафе первоеПосещение =

(ПосещениеКафе) егоПосещения.get(0);

ПосещениеКафе последнееПосещение = (ПосещениеКафе)

егоПосещения.get(егоПосещения.size() - 1);

double первыйЗамер = первоеПосещение.получитьВес();

double последнийЗамер =

последнееПосещение. ПолучитьВес();

изменение = последнийЗамер – первыйЗамер;

первыеБулочки = первоеПосещение.получитьБулочки();

for (int i = 0; i < егоПосещения.size(); i++)

{

ПосещениеКафе v = (ПосещениеКафе)

егоПосещения.get(i);

общаяСтоимость += v.получитьСтоимость();

потреблениеБулочек += v.получитьБулочки();

}

потреблениеБулочек -= первыеБулочки;

if (потреблениеБулочек > 0)

wpb = изменение / потреблениеБулочек;

}

r.устВесНаБулочку(wpb );

r.устИзменениеВеса(изненение);

r.устСтоимостьБулочек(общаяСтоиность);

r.устПотреблениеБулочек(потреблениеБулочек);

return r;

}

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

Листинг 16.19. Лакомка.java

if (егоПосещения.size() > 0)

{

ПосещениеКафе первоеПосещение =

(ПосещениеКафе) егоПосещения.get(0);

ПосещениеКафе последнееПосещение = (ПосещениеКафе)

егоПосещения.get(егоПосещения.size() - 1):

double первыйЗамер = первоеПосещение.получитьВес();

double последнийЗамер = последнееПосещение.получитьВес();

изменение = последнийЗамер – первыйЗамер;

первыеБулочки = первоеПосещение.получитьБулочки();

for (int i =0; i < егоПосещения.size(); i++)

{

ПосещениеКафе v = (ПосещениеКафе) егоПосещения.get(i);

потреблениеБулочек += v.получитьБулочки();

}

for (int i =0; i < егоПосещения.size(); i++)

{

ПосещениеКафе v = (ПосещениеКафе) егоПосещения.get(i);

общаяСтоимость += v.получитьСтоимость();

}

потреблениеБулочек -= первыеБулочки;

if (потреблениеБулочек > 0)

wpb = изменение / потреблениеБулочек;

}

Выполним тестирование. На следующем шаге поместим каждый цикл в отдельный приватный метод.


Листинг 16.20.
Лакомка.java

public Отчет создатьОтчет()

{

Отчет г = new Отчет();

double изменение = 0;

double общаяСтоимость = 0;

double потреблениеБулочек = 0;

double первыеБулочки =0;

double wpb = 0;

if (егоПосещения. Size() > О)

{

ПосещениеКафе первоеПосещение =

(ПосещениеКафе) егоПосещения.get(0);

ПосещениеКафе последнееЛосещение = (ПосещениеКафе)

егоПосещения.get(егоПосещения.size() - 1);

double первыйЗамер = первоеПосещение.получитьВес();

double последнийЗамер =

последнееПосещение.получитьВес();

изменение - последнийЗамер – первыйЗамер;

первыеБулочки = первоеПосещение.получитьБулочки();

потреблениеБулочек = вычПотреблениеБулочек();

общаяСтоимость = вычОбщуюСтоимость();

потреблениеБулочек -= первыеБулочки;

if (потреблениеБулочек > 0)

wpb = изменение / потреблениеБулочек;

}

r.устВесНаБулочку(wpb);

r.устИзменениеВеса(изменение);

r.устСтоимостьБулочек(общаяСтоимость);

r.устПотреблениеБулочек(потреблениеБулочек);

return r;

}

private double вычОбщуюСтоимость()

{

double общаяСтоииость = 0;

for (int i = 0; i < егоПосещения.size(); i++);

{

ПосещениеКафе v = (ПосещениеКафе) егоПосещения.get(i);

общаяСтоимость += v.получитьСтоимость();

}

return общаяСтоимость;

}

private double вычПотреблениеБулочек()

{

double потреблениеБулочек = 0;

for (int i - 0; i < егоПосещения.size(); i++)

{

ПосещениеКафе v = (ПосещениеКафе) егоПосещения.get(i);

потреблениеБулочек += v.получитьБулочки();

}

return потреблениеБулочек;

}

После соответствующего тестирования перенесем обработку вариантов потребления булочек в метод вычПотреблениеБулочек.

Листинг 16.21. Лакомка.java

public Отчет создатьОтчет()

{

if (егоПосещения.size() > 0)

{

ПосещениеКафе первоеПосещение =

(ПосещениеКафе) егоПосещения.get(0);

ПосещениеКафе последнееПосещение - (ПосещениеКафе)

егоПосещения.get(егоПосещения.size() - 1);

double первыйЗамер = первоеПосещение.получитьВес();

double последнийЗамер =

последнееПосещение.получитьВес();

изменение = последнийЗамер - первыйЗамер;

потреблениеБулочек = вычПотреблениеБулочек();

общаяСтоимость - вычОбщуюС тонкость ();

if (потреблениеБулочек > 0)

wpb = изменение / потреблениеБулочек;

}

return r;

}

private double вычОбщуюСтоимость()

{

double общаяСтоимость = 0;

for (int i= 0; i < егоПосещения.size(); i++);

{

ПосещениеКафе v = (ПосещениеКафе) егоПосещения.get(i);

общаяСтоимость += v.получитьСтоимость();

}

return общаяСтоимость;

}

private double вычПотреблениеБулочек()

{

double потреблениеБулочек = 0;

if (егоПосещения.size() > 0)

{

for (int i = 1; i < егоПосещения.size(); i++)

{

ПосещениеКафе v = (ПосещениеКафе)

егоПосещения.get(i);

потреблениеБулочек += v.получитьБулочки();

}

}

return потреблениеБулочек;

}

Заметим, что функция вычПотреблениеБулочек теперь суммирует потребление булочек, начиная со второго посещения. И опять выполняем тестирование. На следующем шаге выделим функцию для расчета изменения веса.

Листинг 16.22. Лакомка.java

public Отчет создатьОтчет()

{

Отчет r = new Отчет ();

double изменение = 0;

double общаяСтоимость = 0;

double потреблениеБулочек = 0;

double первыеБулочки = 0;

double wpb = 0;

if (егоПосещения.size() > 0)

{

изменение = вычИзменение();

потреблениеБулочек = вычПотреблениеБулочек();

общаяСтоимость = вычОбщуюСтоимость();

if (потреблениеБулочек > 0)

wpb = изменение / потреблениеБулочек;

}

r.устВесНаБулочку(wpb);

r.устИзменениеВеса(изменение);

r.устСтоимостьБулочек(общаяСтоимость);

r.устПотреблениеБулочек(потреблениеБулочек):

return r;

}

private double вычИзменение()

{

double изменение = 0;

if (егоПосещения.size() > 0)

{

ПосещениеКафе первоеПосещение =

(ПосещениеКафе) егоПосещения.get(0);

ПосещениеКафе последнееПосещение = (ПосещениеКафе)

егоПосещения.get;(егоПосещения.sizе() - 1);

double первыйЗамер = первоеПосещение.получитьВес();

double последнийЗамер =

последнееПосещение. получитьВес();

изменение = последнийЗамер - первыйЗамер;

}

return изменение;

}

После очередного запуска тестов переместим условия в главном методе создатьОтчет и подчистим лишние места.

Листинг 16.23. Лакомка.java

public Отчет создатьОтчет()

{

double изменение = вычИзменение();

double потреблениеБулочек = вычПотреблениеБулочек();

double общаяСтоимость = вычОбщуюСтоимость();

double wpb = 0;

if (потреблениеБулочек > 0)

wpb = изменение / потреблениеБулочек;

Отчет г = new Отчет ();

r.устВесНаБулочку(wpb);

r.устИзменениеВеса(изменение);

r.устСтоимостьБулочек(общаяСтоимость);

r.устПотреблениеБулочек(потреблениеБулочек);

return r;

}

private double вычИзменение()

{

double изменение = 0;

if (eroПосещения.size() > 1)

{

ПосещениеКафе первоеПосещение =

(ПосещениеКафе) егоПосещения.get(0);

ПосещениеКафе последнееПосещение = (ПосещениеКафе)

егоПосещения.get(егоПосещения.size() - 1);

double первыйЗамер = первоеПосещение.получитьВес(0;

double последнийЗамер =

последнееПосещение. получитьВес();

изменение = последнийЗамер – первыйЗамер;

}

return изменение;

}

private double вычОбщуюСтоимость()

{

double общаяСтоимость =0;

for (int i= 0; i < егоПосещения.size(); i++);

{

ПосещениеКафе v = (ПосещениеКафе) егоПосещения.get(i);

общаяСтоимость += v.получитьСтоимость();

}

return общаяСтоимость;

}

private double вычПотреблениеБулочек()

{

double потреблениеБулочек = 0;

if (егоПосещения.size() > 1)

{

for (int i = 1; i < егоПосещения.size(); i++)

{

ПосещениеКафе v = (ПосещениеКафе)

егоПосещения.get(i);

потреблениеБулочек += v.получитьБулочки();

}

}

return потреблениеБулочек;

}

После окончательного прогона тестов констатируем, что цель достигнута — код стал компактным и понятным, обязанности разнесены по отдельным функциям.

Таким образом, в рассмотренном подходе программа считается завершенной не тогда, когда она заработала, а когда она стала максимально простой и ясной.

Контрольные вопросы

 

1.      Что такое CRC-карта? Как ее применить для тестирования визуальных моделей?

2.      Поясните особенности тестирования объектно-ориентированных модулей.

3.      В чем состоит суть методики тестирования интеграции объектно-ориентированных систем, основанной на потоках?

4.      Поясните содержание методики тестирования интеграции объектно-ориентированных систем, основанной на использовании.

5.      В чем заключаются особенности объектно-ориентированного тестирования правильности?

6.      К чему приводит учет инкапсуляции, полиморфизма и наследования при проектировании тестовых вариантов?

7.      Поясните содержание тестирования, основанного на ошибках.

8.      Поясните содержание тестирования, основанного на сценариях.

9.      Чем отличается тестирование поверхностной структуры от тестирования глубинной структуры системы?

10.  В чем состоит стохастическое тестирование класса?

11.  Охарактеризуйте тестирование разбиений на уровне классов. Как в этом случае получить категории разбиения?

12.  Поясните на примере разбиение на категории по состояниям.

13.  Приведите пример разбиения на категории по свойствам.

14.  Перечислите известные вам методы тестирования взаимодействия классов. Поясните их содержание.

15.  Приведите пример стохастического тестирования взаимодействия классов.

16.  Приведите пример тестирования взаимодействия классов путем разбиений.

17.  Приведите пример тестирования взаимодействия классов на основе состояний. В чем заключается особенность методики «преимущественно в ширину»?

18.  Поясните суть предваряющего тестирования.

19.  Какую роль в процессе экстремальной разработки играет рефакторинг?

ГЛАВА 17. Автоматизация конструирования визуальной модели программной системы

 

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

Общая характеристика CASE-системы Rational Rose

 

Rational Rose — это CASE-система для визуального моделирования объектно-ориентированных программных продуктов. Визуальное моделирование — процесс графического описания разрабатываемого программного обеспечения. Экран среды Rational Rose показан на рис. 17.1.

В его составе выделим шесть элементов: строку инструментов, панель «инструменты диаграммы», окно диаграммы, браузер, окно спецификации, окно документации.

Как показано на рис. 17.2, кнопки строки инструментов позволяют выполнять стандартные и специальные действия.

Содержание панели инструментов диаграммы меняется в зависимости от активной диаграммы. Окно активной диаграммы имеет синюю строку заголовка (рис. 17.3).

В окне диаграммы можно создавать, отображать и изменять диаграмму на языке UML.

 

Рис. 17.1. Экран среды Rational Rose

 

 

Рис. 17.2. Кнопки строки инструментов Rational Rose

 

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

Знак плюс (+) рядом с папкой означает, что внутри папки находятся дополнительные элементы. Для «разворачивания» папки надо нажать на знак +. Если папка «развернута», то слева от нее появляется знак минус (-). Для «сворачивания» структуры папки нажимается знак минус.

Окно спецификации позволяет задавать характеристики элемента диаграммы (рис. 17.5).

В поле Documentation этого окна вводится словесное описание данного элемента. Это же описание можно вводить в Окно документации Rational Rose (когда данный элемент выделен в диаграмме).

 

Рис. 17.3. Панель инструментов и окно активной диаграммы

 

 

Рис. 17.4. Браузер Rational Rose

 

В качестве примера работы с Rational Rose рассмотрим построение модели университетской системы для регистрации учебных курсов (классический пример компании Rational), автор которой — Терри Кватрани [57].

Эта система используется:

q       профессором — для задания читаемого курса;

q       студентом — для выбора изучаемого курса;

q       регистратором — для формирования учебного плана и расписания;

q       учетной системой — для определения денежных затрат.

 

Рис. 17.5. Окно спецификации и окно документации Rational Rose

 

Создание диаграммы Use Case

 

Моделирование проблемы регистрации курсов начнем с создания диаграммы Use Case. Этот тип диаграммы представляется актерами, элементами Use Case и отношениями между ними. Откроем главную диаграмму Use Case (рис. 17.6).

1. В окне браузера щелкнем по значку + слева от пакета Use Case View.

2. Для открытия диаграммы выполним двойной щелчок по значку Main.

Первый шаг построения этой диаграммы состоит в определении актеров, фиксирующих роли внешних объектов, взаимодействующих с системой. В нашей проблемной области можно выделить 4 актера — Student (Студент), Professor (Профессор), Registrar (Регистратор) и Billing System (Учетная система) (рис. 17.7).

1. На панели инструментов щелкните по значку актера.

2. Для добавления актера в диаграмму щелкните в нужном месте диаграммы.

3. Пока актер остается выделенным, введите имя Student (Студент).

 

Рис. 17.6. Главная диаграмма Use Case

 

 

Рис. 17.7. Четыре актера

 

4. Повторите предыдущие шаги для ввода трех других актеров (Professor, Registrar и Billing System — Профессор, Регистратор, Учетная система).

Далее для каждого актера нужно определить соответствующие элементы Use Case. Элемент Use Case представляет определенную часть функциональности, обеспечиваемой системой. Вы можете идентифицировать элементы Use Case путем рассмотрения каждого актера и его взаимодействия с системой. В нашей модели актер Student хочет регистрироваться на курсы (Register for Courses). Актер Billing System получает информацию о регистрации. Актер Professor хочет запросить список курса (Request a Course Roster). Наконец, актер Registrar должен управлять учебным планом (Manage Curriculum) (рис. 17.8).

 

Рис. 17.8. Элементы Use Case для актеров

 

1.      На панели инструментов щелкните по значку элемента Use Case.

2.      Для добавления элемента Use Case в диаграмму щелкните в нужном месте диаграммы.

3.      Пока элемент Use Case остается выделенным, введите имя Register for Courses.

4.      Повторите предыдущие шаги для ввода других элементов Use Case (Request Course Roster, Manage Curriculum).

Далее между актерами и элементами Use Case рисуются отношения. Чтобы показать направление взаимодействия (кто инициирует взаимодействие), используются однонаправленные стрелки (uni-directional arrows). В системе регистрации курсов актер Student инициирует элемент Use Case Register for Courses, который, в свою очередь, взаимодействует с актером Billing System. Актер Professor инициирует элемент Use Case Request Course Roster. Актер Registrar инициирует элемент Use Case Manage Curriculum (рис. 17.9).

 

Рис. 17.9. Отношения между актерами и элементами Use Case

 

1.      На панели инструментов щелкните по значку однонаправленной ассоциации (стрелке).

2.      Щелкните по актеру Student и перетащите линию на элемент Use Case Register for Courses.

3.      На панели инструментов щелкните по значку однонаправленной ассоциации (стрелке).

4.      Щелкните по элементу Use Case Register for Courses и перетащите линию на актера Billing System.

5.      Повторите предыдущие шаги для ввода других отношений (от актера Professor к элементу Use Case Request Course Roster и от актера Registrar к элементу Use Case Manage Curriculum).


Создание диаграммы последовательности

 

Функциональность элемента Use Case отображается графически в диаграмме последовательности (Sequence Diagram). Эта диаграмма отображает один из возможных путей в потоках событий элемента Use Case — например, добавление студента к курсу. Диаграммы последовательности содержат объекты и сообщения между объектами, которые показывают реализацию поведения. Рассмотрим диаграмму последовательности Add a Course для элемента Use Case Register for Courses (рис. 17.10).

1.      В окне браузера щелкните правой кнопкой по элементу Use Case Register for Courses.

2.      В появившемся контекстном меню выберите команду New:Sequence Diagram.

3.      В результате в дерево окна браузера будет добавлена диаграмма последовательности с именем New Diagram.

4.      Пока значок новой диаграммы остается выделенным, введите имя Add a Course.

 

Рис. 17.10. Создание диаграммы последовательности Add a Course

 

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

1.         Для открытия диаграммы последовательности выполним двойной щелчок по ее значку в окне браузера.

2.         Щелкнем по значку актера Student в браузере и перетащим его в диаграмму последовательности.

3.         Щелкнем по значку актера в диаграмме последовательности и введем его имя — Joe.

Сценарий, который мы собираемся формализовать с помощью диаграммы последовательности, уже существует — он является фрагментом текста, который содержит спецификация элемента Use Case Register for Courses.

В этом сценарии студент должен заполнить информацией (fill in info) регистрационную форму (registration form), а затем форма предъявляется на рассмотрение (submitted). Очевидно, что необходим объект registration form, который принимает информацию от студента (рис. 17.12). Создадим форму и добавим два сообщения, «fillin info» и «submit».

1.      На панели инструментов щелкните по значку объекта (прямоугольнику).

2.      Для добавления объекта в диаграмму щелкните в нужном месте диаграммы.

3.      Пока объект остается выделенным, введите имя registration form.

4.      На панели инструментов щелкните по значку объектного сообщения (стрелке).

5.      Щелкните по пунктирной линии под актером Student и перетащите стрелку на пунктирную линию под объектом registration form.

6.      Пока стрелка остается выделенной, введите имя сообщения — fill in information.

7.      Повторите шаги 4-6 для создания сообщения submit.

 

Рис. 17.11. Диаграмма последовательности — Joe

 

 

Рис. 17.12. Диаграмма последовательности — Registration Form

 

Очевидно, что объект регистрационная форма является промежуточным звеном в цепи передачи информации. Следующее звено — объект-менеджер. Его надо добавить в диаграмму.

Далее форма должна послать сообщение менеджеру (manager) (рис. 17.13). Таким образом менеджер узнает, что студент должен быть добавлен к курсу, — положим, что Joe хочет получить курс math 101.

 

Рис. 17.13. Диаграмма последовательности — Manager

 

1.      На панели инструментов щелкните по значку объекта (прямоугольнику).

2.      Для добавления объекта в диаграмму щелкните в нужном месте диаграммы.

3.      Пока объект остается выделенным, введите имя manager.

4.      На панели инструментов щелкните по значку объектного сообщения (стрелке).

5.      Щелкните по пунктирной линии под объектом registration form и перетащите стрелку на пунктирную линию под объектом manager.

6.      Пока стрелка остается выделенной, введите имя сообщения — add Joe to Math 101.

Менеджер — один из объектов системы, который взаимодействует как с регистрационной формой, так и с набором объектов — учебных курсов. Для обслуживания нашего студента должен быть объект Math 101. Если такой объект существует, то Менеджер обязан передать объекту-курсу Math 101, что Joe должен быть добавлен к курсу (рис. 17.14).

1.      На панели инструментов щелкните по значку объекта (прямоугольнику).

2.      Для добавления объекта в диаграмму щелкните в нужном месте диаграммы.

3.      Пока объект остается выделенным, введите имя math 101.

4.      На панели инструментов щелкните по значку объектного сообщения (стрелке).

5.      Щелкните по пунктирной линии под объектом manager и перетащите стрелку на пунктирную линию под объектом math 101.

6.      Пока стрелка остается выделенной, введите имя сообщения — add Joe.

 

Рис. 17.14. Диаграмма последовательности — Math 101

 

Объект-курс не принимает самостоятельных решений о возможности добавления студентов. Этим занимается экземпляр класса Предложение курса (course offering). Назовем такой экземпляр (объект) Section 1.

Объект-курс обращается к объекту Section 1, если он открыт (в этом сценарии — ответ «да») с предложением добавить студента Joe (рис. 17.15).

1.      На панели инструментов щелкните по значку объекта (прямоугольнику).

2.      Для добавления объекта в диаграмму щелкните в нужном месте диаграммы.

3.      Пока объект остается выделенным, введите имя — section 1.

4.      На панели инструментов щелкните по значку объектного сообщения (стрелке).

5.      Щелкните по пунктирной линии под объектом math 101 и перетащите стрелку на пунктирную линию под объектом section 1.

6.      Пока стрелка остается выделенной, введите имя сообщения — accepting students?

7.      Повторите шаги 4-6 для создания сообщения add Joe.

 

Рис. 17.15. Диаграмма последовательности — Section 1

 

 

Рис. 17.16. Диаграмма последовательности — Billing System

 

Конечно, за учебу надо платить. Вопросами оплаты занимается Учетная система, а уведомляет ее о необходимости выписки счета менеджер. После того как менеджер удостоверился, что студенту Joe предоставляется возможность изучать курс math 101 (на диаграмме рис. 17.16 это не показано), уведомляется Учетная система (billing system).

1.      На панели инструментов щелкните по значку объекта (прямоугольнику).

2.      Для добавления объекта в диаграмму щелкните в нужном месте диаграммы.

3.      Пока объект остается выделенным, введите имя — bill.

4.      На панели инструментов щелкните по значку объектного сообщения (стрелке).

5.      Щелкните по пунктирной линии под объектом manager и перетащите стрелку на пунктирную линию под объектом bill.

6.      Пока стрелка остается выделенной, введите имя сообщения — send bill for Math 101 to Joe.


Создание диаграммы классов

 

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

q       registration form является объектом класса RegForm;

q       manager является объектом класса Manager;

q       math 101 является объектом класса Course;

q       section 1 является объектом класса CourseOffering;

q       bill является интерфейсом к внешней учетной системе, поэтому мы будем использовать имя BillingSystem как имя его класса.

Классы создаются в логическом представлении системы (рис. 17.17).

1.      В окне браузера щелкните правой кнопкой по значку пакета Logical View.

2.      В появившемся контекстном меню выберите команду New:Class. В результате в дерево окна браузера будет добавлен класс с именем NewClass.

3.      Пока значок класса остается выделенным, введите имя RegForm.

4.      Повторите предыдущие шаги для добавления других классов: Manager, Course, CourseOffering и BillingSystem.

После создания классов они описываются (документируются). Описания добавляются с помощью Documentation Window (рис. 17.18).

1. В окне браузера щелкните по значку класса CourseOffering.

2. Введите описание класса в Documentation Window.

Процесс построения сценариев и нахождения классов продолжается до тех пор, пока вы не скажете: «Больше находить нечего — нет ни новых классов, ни новых сообщений».

Следующий шаг — построение диаграммы классов. Откроем главную диаграмму (рис. 17.19) классов и добавим в нее классы.

 

Рис. 17.17. Логическое Рис. 17.18. Окно документации —

представление — Logical View Documentation Window

 

 

Рис. 17.19. Главная диаграмма классов

 

1.      Для открытия диаграммы выполним двойной щелчок по значку Main в окне браузера.

2.      В главном меню выберем команду Query:Add Classes.

3.      Для добавления всех классов нажмем кнопку АИ» (выбрать все).

4.      Для закрытия окна и добавления классов в диаграмму нажмем кнопку ОК.

5.      Переупорядочим классы в диаграмме (выделяя конкретный класс и перетаскивая, его на новое место).

ПРИМЕЧАНИЕ

Классы можно добавлять в диаграмму перетаскиванием их из окна браузера (по одному классу в единицу времени).

 

Для создания новых типов моделирующих элементов в UML используется понятие стереотипа. С помощью стереотипа можно «нагрузить» элемент новым смыслом. Используем предопределенный стереотип Interface для класса BillingSystem (рис. 17.20), так как этот класс определяет только интерфейс к внешней учетной системе (billing system).

 

Рис. 17.20. Класс Billing System

 

1.           В главной диаграмме классов выполним двойной щелчок по значку класса BillingSystem. В результате появляется окно спецификации класса (Class Specification).

2.           Щелкнем по стрелке раскрывающегося списка Stereotype.

3.           Наберем на клавиатуре слово-стереотип Interface.

4.           Закроем окно спецификации, нажав кнопку ОК.

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

Ассоциация определяет соединение между классами. Исследуя диаграмму последовательности Add a Course, мы можем определить существование следующих ассоциаций: от RegForm к Manager, от Manager к Course и от Manager к BHHngSystem (рис. 17.21).

 

Рис. 17.21. Ассоциации между классами

 

1.      На панели инструментов щелкните по значку однонаправленной ассоциации (стрелке).

2.      Щелкните по классу RegForm и перетащите линию ассоциации на класс Manager.

3.      Повторите предыдущие шаги для ввода следующих отношений:

q       от Manager к Course;

q       от Manager к BillingSystem.

Ассоциации задают пути между объектами-партнерами одинакового уровня.

Агрегация фиксирует неравноправные связи. Она показывает отношение между целым и его частями. Создадим отношение агрегации между классом Course и классом CourseOffering (рис. 11.22), так как предложение Курса CourseOfferings является частью агрегата — класса Course.

1. На панели инструментов щелкните по значку агрегации (линии с ромбиком).

2. Щелкните по классу, представляющему целое — Course.

3. Перетащите линию агрегации на класс, представляющий часть — CourseOffering.

 

Рис. 17.22. Отношение агрегации

 

 

Рис. 17.23. Индикаторы мощности

 

Для отображения того, «как много» объектов участвует в отношении, к ассоциациям и агрегациям диаграммы могут добавляться индикаторы мощности (рис. 17.23).

1. Щелкните правой кнопкой по линии агрегации возле класса CourseOffering.

2. Из контекстного меню выберите команду Multipticity:0ne or More.

3. Щелкните правой кнопкой по линии агрегации возле класса Course.

4. Из контекстного меню выберите команду Multiplicity:1.

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

Структура класса представляется набором его свойств. Структура находится путем исследования проблемных требований и соглашений между разработчиками и заказчиками. В нашей модели каждое предложение курса (CourseOffering) является свойством (attribute) класса-агрегата Course.

Конечно, класс CourseOffering тоже имеет свойства (рис. 17.24). Определим одно из них — количество студентов.

 

Рис. 17.24. Свойства

 

1.      В диаграмме классов щелкните правой кнопкой по классу CourseOffering.

2.      Из контекстного меню выберите команду Insert New Attribute. Это приведет к добавлению в класс свойства.

3.      Пока новое свойство остается выделенным, введите его имя — numberStudents.

Итак, два шага формирования класса сделаны. Перейдем к третьему шагу — заданию поведения класса.

Поведение класса представляется набором его операций. Исходная информация об операциях класса находится в диаграммах последовательности. В операции отображаются сообщения из диаграмм последовательности.

Первое действие этого шага заключается в привязке объектов (из диаграмм последовательности) к конкретным классам. Выполним такую привязку для нашей модели (рис. 17.25).

 

Рис. 17.25. Привязка объектов к классам

 

1.       Для открытия диаграммы последовательности Add a Course выполним двукратный щелчок по ее значку в окне браузера.

2.       В окне браузера щелкнем по значку класса CourseOffering.

3.       Перетащим класс CourseOffering на объект section 1.

Вот и все. Видим, что имя объекта удлинилось, в нем появились две части, разделенные двоеточием. Слева от двоеточия записывается имя объекта, а справа — имя класса.

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

 

Рис. 17.26. Новая операция

 

1.        Щелкните правой кнопкой по сообщению «add Joe». В результате станет видимым контекстное меню.

2.        Выберите команду new operation. В результате станет видимой спецификация операции Operation Specification.

3.        Введите имя новой операции — add.

4.        Перейдите на вкладку Detail.

5.        Щелкните правой кнопкой мышки по полю Arguments.

6.        Выберите в контекстном меню команду Insert. В появившейся рамке наберите имя аргумента — Joe. Щелкните вне рамки.

7.        Закройте окно спецификации, нажав кнопку ОК.

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

Возможно и обратное действие — отталкиваясь от имени операции, получить имя сообщения. При этом реализуется такая последовательность: отдельно создается новая операция класса, а затем она отображается на существующее сообщение (рис. 17.27).

1.      Щелкните правой кнопкой по классу в браузере.

2.      В появившемся контекстном меню выберите команду New: Operation. Появляется рамка с надписью opname.

3.      Вместо надписи opname наберите имя новой операции класса — offeringOpen.

4.      На диаграмме последовательности щелкните правой кнопкой по сообщению «accepting students?». В результате станет видимым контекстное меню.

5.      В меню выберите операцию offeringOpen() —сообщение переименовывается (на него отображается операция класса).

 

Рис. 17.27. Отображение операции на сообщение

 

Следующий шаг разработки состоит в настройке описаний классов на конкретный язык программирования. Сам язык выбирается по команде Tools:0ptions. В появившемся диалоговом окне переходят на вкладку Notation. Название языка выбирается из раскрывающегося списка Default Language. Для нашего примера используем язык Ada 95.

Итак, в ходе анализа и проектирования в визуальную модель добавляются проектные решения (рис. 17.28). После выбора языка программирования для свойств определяют типы данных, а для операций конкретизируют сигнатуры — имена и типы параметров, типы возвращаемых значений.

1.        Выполним двукратный щелчок по значку класса CourseOffering в окне браузера или диаграмме классов. В результате станет видимым окно спецификации класса.

2.        Выберите страницу Attributes (свойства).

3.        Щелкните по полю Туре. В результате станет видимым раскрывающийся список.

4.        Введите требуемый тип данных (Integer).

5.        Закройте окно спецификации, нажав кнопку ОК.

 

Рис. 17.28. Добавление проектных решений

 

ПРИМЕЧАНИЕ

Типы данных для свойств можно устанавливать, используя спецификацию Attribute или вводя их в строчке отображения свойства на диаграмме классов (формат attribute:data type).

 

Теперь зададим тип возвращаемого результата для операции offeringOpen (рис. 17.29).

1.        Выполним двукратный щелчок по значку класса CourseOffering в окне браузера или диаграмме классов. В результате станет видимым окно спецификации класса.

2.        Выберите страницу Operations.

3.        Щелкните по полю Return type. В результате станет видимым раскрывающийся список.

4.        Введите требуемый возвращаемый тип (Integer).

5.        Закройте окно спецификации, нажав кнопку ОК.

ПРИМЕЧАНИЕ

Аргументы операции устанавливают с помощью диалогового окна Operation Specification. Для перехода к этому окну нужно на вкладке (странице) Operations щелкнуть правой кнопкой по имени операции и в появившемся контекстном меню выбрать команду Specification. Далее в появившемся диалоговом окне следует перейти на вкладку Detail.

 

Аргументы операции и возвращаемый тип можно также установить, вводя их в строчке отображения операции на диаграмме классов (формат operation(argument name:data type):return type).

 

Рис. 17.29. Определение типа возвращаемого результата

 

Создание компонентной диаграммы

 

Допустим, что наступил момент, когда нужно генерировать коды для классов модели. Для определения компонентов исходного кода используют компонентное представление (Component View) (рис. 17.30). Среда Rational Rose автоматически создает главную компонентную диаграмму.

1. В окне браузера щелкнем по значку + слева от пакета Component View.

2. Для открытия главной компонентной диаграммы выполним двойной щелчок по значку Main.

В общем случае каждому классу должны соответствовать два компонента — компонент спецификации и компонент реализации. В будущем каждому компоненту будет соответствовать свой файл. Например, в языке C++ классу соответствуют два файла-компонента: h-файл (файл спецификации) и срр-файл (файл реализации).

В нашей модели мы создадим один компонент для представления файла спецификации по классу CourseOffering и один компонент для представления файла реализации по классу CourseOffering (рис. 17.31).

Эти файлы будут иметь расширения .ads и .adb соответственно. Файл .ads имеет стереотип Package Specification. Файл .adb имеет стереотип Package Body.

1.      На панели инструментов щелкните по значку спецификации пакета Package Specification.

2.      Для добавления компонента в диаграмму щелкните в нужном месте диаграммы.

3.      Пока новый компонент остается выделенным, введите его имя — CourseOffering.

4.      Повторите предыдущие шаги с использованием значка тела пакета Package Body.

5.      На панели инструментов щелкните по значку отношения зависимости.

6.      Щелкните по компоненту, представляющему .adb-файл (тело пакета), и перетащите стрелку на компонент, представляющий .ads-файл (спецификация пакета).

 

Рис. 17.30. Компонентное представление — Component View

 

 

Рис. 17.31. Компонентное представление

 

После создания компонентов им должны быть назначены классы модели (рис. 17.32).

1.      Выполним двукратный щелчок по значку компонента CourseOffering, представляющего .ads-файл (спецификацию пакета), в окне браузера или компонентной диаграмме. В результате станет видимым окно спецификации компонента.

2.      Выберите страницу (вкладку) Realizes. Вы увидете список классов модели.

3.      Щелкните правой кнопкой по классу CourseOffering. В результате станет видимым контекстное меню.

4.      Выберите команду Assign.

5.      Закройте окно спецификации, нажав кнопку ОК.

6.      Выполните аналогичные действия для тела пакета, представляющего .adb-файл.

 

Рис. 17.32. Назначение классов компоненту

 

Генерация программного кода

 

Команды для генерации кода на языке Ada 95 содержит пункт Toots главного меню (рис. 17.33).

1. На компонентной диаграмме выделите оба компонента CourseOffering.

2. Выберите команду Tools:Ada95: Code Generation из главного меню.

Итоги генерации кода отображаются в окне Code Generation Status (рис. 17.34).

Все ошибки заносятся в log-окно.

3. Для завершения процесса генерации кода нажмите кнопку Close.

 

Рис. 17.33. Меню Tools: генерация кода на языке Ada 95

 

 

Рис. 17.34. Статус генерации кода

 

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

Просмотрим код, сгенерированный средой Rational Rose.

Фрагмент содержания .ads-файла, отражающего спецификацию класса CourseOffering, представлен на рис. 17.35. Отметим, что в программный текст добавлено то описание, которое было внесено в модель через окно документации. Более того, система Rational Rose подготавливает код к многократной итеративной модификации, защите выполняемых изменений. Стандартный раздел программного кода имеет вид

--##begin module.privateDeclarations preserve=yes

--##end module.privateDeclarations

 

Рис. 17.35. Код спецификации класса, сгенерированный средой Rational Rose

 

Запись module.privateDeclarations обозначает имя раздела. Элемент preserve=(yes/no) говорит системе, можно ли при повторной генерации кода этот раздел изменять или нельзя. После генерации кода программный текст добавляется между операторами ##begin и ##end.

Полный листинг сгенерированного кода спецификации выглядит так:

--##begin module.cp preserve=no

--##end module.cp

-- Specification CourseOffering (Package Specification)

-- Dir : C:\Program Files\Rational\Rose\ada95\source

-- File: courseoffering.ads

--##begin module.withs preserve=yes

--##end module.withs

package CourseOffering is

--##begin module.declarations preserve=no

--##end module.declarations

-- Class CourseOffering

-- Documentation:

-- CourseOffering is

-- Concurrency: Sequential

-- Persistence: Transient

-- Cardinality: n

type Object is tagged private;

type Handle is access Object'Class;

-- Array declarations

type Array_0f_0bject is

array (Positive range <>) of Object;

type Access_Array_Of_Object is

access Array_0f_0bject;

-- Standard Operations

function Create return Object;

function Copy (From : in Object) return Object;

procedure Free (This : in out Object);

function Create return Handle;

function Copy (From : in Handle) return Handle;

procedure Free (This : in out Handle);

-- Accessor Operations for Associations

function Get_The_Course (This : in Object)

return Course.Handle;

pragma Inline (Get_The_Course);

-- Association Operations

procedure Associate (This_Handle : in Handle;

This_Handle : in Handle);

procedure Associate (This_Handle : in Handle;

This_Array_Of_Handle : in Array_Of_Handle);

procedure Dissociate (This : in Handle);

procedure Dissociate (This : in Handle);

procedure Dissociate (This : in Array_Of_Handle);

-- Other Operations

function offeringOpen (This : in Object) return Integer;

--##begin module.additionalDeclarations preserve=yes

--##end module.additiona1Declarations

private

--##begin module.privateDeclarations preserve=yes

--##end module.privateDeclarations

type Object is tagged

record

-- Data Members for Class Attributes

numberStudents : Integer;

-- Data Members for Associations

The_Course : Course.Handle;

--##begin CourseOffering.private preserve=no

--##end CourseOffering.private

end record:

--##begin module.additionalPrivateDeclarations preserve=yes

--##end module.additionalPrivateDeclarations

end CourseOffering;

Соответственно, фрагмент содержания .adb-файла, отображающего тело класса CourseOffering, представлен на рис. 17.36.

 

Рис. 17.36. Код тела класса, сгенерированный Rational Rose

 

Полный листинг сгенерированного кода для тела класса выглядит следующим образом:

--##begin module.cp preserve=no

--##end module.cp

-- Body CourseOffering (Package Body)

-- Dir : C:\Program Files\Rationa1\Rose\ada95\source

-- File: courseoffering.adb

with Unchecked_Deallocation;

--##begin module.withs preserve=yes

--##end module.withs

package body CourseOffering is

--##begin module.declarations preserve=no

--##end module.declarations

-- Standard Operations

--##begin CourseOffering.CreatefcObject.documentation preserve=yes

--##end CourseOffering.Create%Object.documentation

function Create return Object is

--##begin CourseOffering.Create%Object.declarations preserve=no

--##end CourseOfferi ng.Create%Object.declarations

begin

--##begin CourseOffering.Create%Object.statements preserve=no

[statement]

--##end CourseOffering.Create%Object.statements

end Create;

--##begin CourseOffering.Copy%Object.documentation preserve=yes

--##end CourseOfferi ng.Copy%Object.documentation

function Copy (From : in Object) return Object is

--##begin CourseOffering.Copy%Object.declarations preserve=no

--##end CourseOffering.Copy%Object.declarations

begin

--##begin CourseOffering.Copy%Object.statements preserve=no

[statement]

--##end CourseOfferi ng.Copy%Object.statements

end Copy;

--##begin CourseOffering.Free%Object.documentation preserve=yes

--##end CourseOfferi ng.Free%Object.documentation

procedure Free (This : in out Object) is

--##begin CourseOffering.Free%Object.declarations preserve=yes

--##end CourseOfferi ng.Free%Object.decl arati ons

begin

--##begin CourseOffering.Free%Object.statements preserve=no

[statement]

--##end CourseOffering.Free%Object.statements

end Free;

--##beginCourseOffering. Create%Handle. documentati on preservers=yes

--##end CourseOffering.Create%Handle.documentation

function Create return Handle is

--##begin CourseOffering.Create%Handle.declarations preserve=no

--##end CourseOffering.Create%Handle.declarati ons

begin

--##begin CourseOffering.Create%Handle.statements preserve=no

[statement]

--##end CourseOfferi ng.Create%Handle.statements

end Create;

--##begi n CourseOffering.Copy%Handle.documentation preserve=yes

--##end CourseOffering.Copy%Handle.documentation

function Copy (From : in Handle) return Handle is

--##begin CourseOffering.Copy%Handle.declarations preserve=no

--##end CourseOfferi ng.Copy%Handle.declarations

begin

--##begin CourseOffering.Copy%Handle.statements preserve=no

[statement]

--##end CourseOfferi ng.Copy%Handle.statements

end Copy;

--##begin CourseOffering.Free%Handle.documentation preserve=yes

--##end CourseOfferi ng.Free%Handle.documentation

procedure Free (This : in out Handle) is

--##begin CourseOffering.Free%Handle.declarations preserve=yes

--##end CourseOffering. Free%Handle. declarations

begin

--##begin CourseOf feri ng.Free%Handle. statements preserve=no

[statement]

--##end CourseOffering.Free%Handle.statements

end Free:

-- Other Operations

--##begin CourseOffenng.offeringOpen%Object.940157546.documentati on preserve=yes

--##end CourseOfferi ng.offeringOpen%Object.940157546.documentation

function offeringOpen (This : in Object) return Integer is

--##begin CourseOfferi ng.offeri ngOpen%Object.940157546.declarations preserve=yes

--##end CourseOfferi ng.offeri ngOpen%Object.940157546.declarations

begin

--##begin CourseOfferi ng.offeri ngOpen%Object.940157546.statements preserve=yes

[statement]

--##end CourseOf feri ng. of f eri ngOpen%Object. 940157546. statements

end offeringOpen;

-- Accessor Operations for Associations

--##begin CourseOffering.Get_The_Course%Object.documentati on preserve=yes

--##end CourseOfferi ng. Get_The_Course%Object. documentati on

function Get_The_Course (This : in Object) return Course.Handle is

--##begi n CourseOfferi ng.Get_The_Course%Object.declarati ons preserve=no

--##end CourseOffering. Get__The_Course%Object. declarations

begin

--##begi n CourseOfferi ng.Get_The_Course%Object.statements preserve=no

return This.The_Course;

--##end CourseOfferi ng.Get_The_Course%Object.statements

end Get_Jhe_Course;

-- Association Operations

--##begin module.associations preserve=no

generic

type Role_Type is tagged private;

type Access_Role_Type is access Role_Type'Class;

type Index is range <>;

type Array _Of_Access_Role_Type is

array (Index range <>) of Access_Role_Type;

type Access_Array_Of_Access Role_Type is

access Array_Of_Access_Ro1e_Type;

package Generic_Tagged_Association is

procedure Set (This_Access_Array : in out

Access_Array_Of_Access_Role_Type;

This_Array : Array_Of_Access_Role_Type;

Limit : Positive := Positive

((Index'Last - Index'First) + 1));

function Is_Unique (This_Array : Array_Of_Access_Role_Type;

This : Access_Ro1e_Type) return Boolean;

function Unique (This_Array : in Array_Of_Access_Role_Type)

return Array_Of_Access_Role_Type;

pragma Inline (Set, Is_Unique, Unique);

end Generic_Tagged_Association;

package body Generic_Tagged_Association is

procedure Free is new Unchecked_Deallocation

( Array_Of_Access_Role_Type,

Access_Array_Of_Access_Role_Type);

procedure Set (This_Access_Array : in out

Access_Array_Of_Access_Role_Type;

This_Array : Array_Of_Access_Role_Type;

Limit : Positive := Positive

((Index'Last - Index'First) + 1)) is

Valid : Boolean;

Pos : Index := This_Array'First;

Last : constant Index := This_Array'Last;

Temp_Access_Array : Access_Array_Of_Access_Role_Type;

begin

if Positive(This_Array'Length) > Limit then

raise Constraint_Error;

end if;

if This_Access_Array - null then

-- Allocate entries.

This_Access_Array := new

Array_Of_Access_Role_Type'(This_Array);

return;

end if;

for I in This_Array'Range loop

Valid := Is_Unique (Th1s_Access_Array.all. This_Array (I));

pragma Assert (Valid);

end loop;

-- Reuse any empty slots.

for I in This_Access_Array'Range loop

if This_Access_Array (I) = null then

This_Access_Array (I) := This_Array (Pos);

Pos := Pos + 1;

if Pos > Last then return;

end if;

end if;

end loop;

if Positive(This_Access_Array'Length + (Last - Pos + 1)) > Limit then raise

Constraint_Error;

end if;

-- For any remaining entries, combine by reallocating.

Temp_Access_Array := new Array_Of_Access_Role_Type'

(This_Access_Array.all & This_Array (Pos .. Last));

Free (This_Access_Array);

This_Access_Array := Temp_Access_Array;

end Set;

function Is_Unique (This_Array : Array_Of_Access_Role_Type;

This : Access_Role_Type) return Boolean is

begin

if This = null then return False;

end if;

for I in This_Array'Range loop

if This_Array (I) = This then return False;

end if;

end loop;

return True;

end Is_Unique;

function Unique (This_Array : in Array_Of_Access_Role_Type)

return Array_Of_Access_Ro1e_Type is

First : constant Index := This_Array'First;

Count : Index := First;

Temp_Array : Array_Of_Access_Role_Type (This_Array'Range);

begin

for I in This_Array'Range loop

if Is_Unique (Temp_Array (First .. Count - 1).

This_Array (I)) then

Temp_Array (Count) := This_Array (I);

Count :- Count + 1;

end if;

end loop;

return Temp_Array (First .. Count - 1);

end Unique:

end Generic_Tagged_Association;

package Role_0bject is new Generic_Tagged_Association

(Object,

Handle,

Positive,

Array_Of_Handle,

Access_Array_Of_Hand1e);

--##end module.associations

procedure Associate (This_Handle : in Handle: This_Handle

: in Handle) is

--#begin Associate%38099E7D0190.declarations preserve=no

use Role_0bject;

--##end Associate%38099E7D0190.declarations

begin

--##begin Associate%38Q99E70Q190.statements preserve=no

pragma Assert (This_Hand1e /= null);

pragma Assert (This_Handle /= null);

pragma Assert (This_Handle.The_Course = null or

else This_Handle.The_Course = This_Handle);

This_Handle.The_Course := This_Handle;

Set (This_Handle.The_CourseOffering. Array_Of_Handle'(1 =>

This_Handle));

--##end Associate3%38099E7D0l90. statements

end Associate;

procedure Associate (This_Handle : in Handle;

This_Array_Of_Handle : in Array_Of_Handle) is

--##begin Associate%(1,N)38099E7D0190.declarations preserve=no

use Role_0bject;

Temp_Array_Of_Handle : constant Array_Of_Handle := Unique

(This_Array_Of_Handle):

--«end Associate%(l,N)38099E7D0190.declarations

begin

--##begin Associate%(l.N)38099E7D0190.statements preserve=no

pragma Assert (This_Handle /= null);

pragma Assert (Temp_Array_Of_Handle'Length > 0);

for I in Temp_Array_Of_Handle'Range loop

pragma Assert (Temp_Array_Of_Handle (I).The_Course = null or else

Temp_Array_Of_Handle (I).The_Course - This_Handle);

Temp_Array_Of_Handle (I).The_Course := This_Handle;

end loop;

Set (This_Handle.The_CourseOffering. Temp_Array_Of_Handle);

--##end Associate%(1,N)38099E7D0190.statements

end Associate;

procedure Dissociate (This : in Handle) is

--##begin Dissociate«38099E7D0190.declarations preserve=yes

--##end Dissociate«38099E7D0190.declarations

begin

--##begin Dissociate%38099E7D0190.statements preserve=no

pragma Assert (This /= null);

for I in This.The_CourseOffering'Range loop

if This.The_CourseOffering (I) /= null then

if This.The_CourseOffering (I).The_Course = This then

This.The_CourseOffering (I).The_Course := null;

This.The_CourseOffering (I) := null;

end if;

end if;

end loop;

--##end Dissociate%38099E7D0190.statements

end Dissociate;

procedure Dissociate (This := in Handle) is

--##begin Dissociate%38099E7D0190.declarations preserve=yes

--##end Dissociate%38099E7D0190.declarations

begin

--##begin Dissociate%38099E7D0190.statements preserve=no

pragma Assert (This /= null):

for I in This.The_Course.The_CourseOffering'Range loop

if This.The_Course.The_CourseOffering (I) = This then

This.The_Course.The_CourseOffering (I) :=null;

This.The_Course ;=null;

exit:

end if;

end loop;

--##end Dissociat%38099E7D0190.statements

end Dissociate;

procedure Dissociate (This : in Array_Of_Handle) is

--##begin Dissociate%(M)38099E7D0190.declarations preserve=yes

--##end Dissociate%(M)38099E7D0190.declarations

begin

--##begin Dissociate%(M)38099E7D0190.statements preserve=no

for I in This'Range loop

if This (I) /= null then

Dissociate (This (I));

end if;

end loop;

--##end Dissociate%(M)38099E7D0190.statements

end Dissociate;

--##begin module.additionalDeclarations preserve=yes

--##end module.additionalDec!arations

begin

--##begin module.statements preserve=no

null;

--##end module.statements

end CourseOffering;

 

Отметим, что в теле есть стандартные методы, которые не задавались в модели, — например, методы constructor, destructor и get/set. Их автоматическая генерация была задана настройкой среды — свойствами генерации. Система обеспечивает настройку параметров генерации для уровней класса, роли, свойства (атрибута) и проекта в целом. Более подробную информацию о свойствах генерации кода можно получить из help-файла.


Заключение

 

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

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

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

Впрочем, были времена, когда и другие «кланы» людей относились к «программе-рам» с большим подозрением: им мало платили, унижая материально, не находили для них ласковых слов (а употребляли большей частью ругательные). Где эти люди? И где их прежнее отношение?

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

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

Базис современной программной инженерии образуют следующие составляющие:

q       процессы конструирования ПО;

q       метрический аппарат, обеспечивающий измерения процессов и продуктов;

q       аппарат формирования исходных требований к разработкам;

q       аппарат анализа и проектирования ПО;

q       аппарат визуального моделирования ПО;

q       аппарат тестирования программных продуктов.

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

В заключение «родился теплый лирический тост» — за программистов всех стран! А если серьезно, друзья, вы — строители целого виртуального мира, я верю в вас, я горжусь вами. Дерзайте, творите, разочаровывайтесь и очаровывайтесь! Я уверен, вы построите достойное информационное обеспечение человеческого общества!

Приложение А.

Факторы затрат постархитектурной модели СОСОМО II

 

Значительную часть времени при использовании модели СОСОМО II занимает работа с факторами затрат. Это приложение содержит описание таблиц Боэма, обеспечивающих оценку факторов затрат.

 

Факторы продукта

Таблица А.1. Требуемая надежность ПО (Required Software Reliability) RELY

Фактор

Очень

низкий

Низкий

Номинальный

Высокий

Очень высокий

Сверхвысокий

RELY

Легкое беспокойство

Низкая, легко восстанавливаемые потери

Умеренная, легко восстанавливаемые потери

Высокая, финансовые потери

Риск для человеческой жизни

 

Таблица А.2. Размер базы данных (Data Base Size) DATA

Фактор

Очень низкий

Низкий

Номинальный

Высокий

Очень высокий

Сверхвысокий

DATA

 

Байты БД/ LOCnporp. < 10

10D/P<100

100D/P<1000

D/P1000

 

ПРИМЕЧАНИЕ

Фактор DATA определяется делением размера БД (D) на длину кода программы (Р). Длина программы представляется в LOC-оценках.

Сложность продукта (Product Complexity) CPLX

 

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

Таблица А.З. Сложность модуля в зависимости от области применения

CPLX

Операции управления

Вычислительные операции

Операции с приборами

Очень низкий

Последовательный код

Вычисление простых

Простые операторы

 

с небольшим

выражений,

чтения и записи,

 

количеством

например,

использующие простые

 

структурированных

A=B+C*(D-E)

форматы

 

операторов: DO, CASE,

 

 

 

IF-THEN-ELSE.Простая

 

 

 

композиция модулей

 

 

 

с помощью вызовов

 

 

 

процедур и простых

 

 

 

сценариев

 

 

Низкий

Несложная вложенность

Вычисление выражений

Не требуется знание

 

структурированных

средней сложности,

характеристик

 

операторов. В основном

например

конкретного процессора

 

простые предикаты

D=SQRT(B**2-4*A*C)

или устройства ввода-

 

 

 

вывода. Ввод-вывод выполняется на уровне GET/PUT

Номинальный

В основном простая

Использование

Обработка ввода-

 

вложенность.

стандартных

вывода, включающая

 

Некоторое

математических

выбор устройства,

 

межмодульное

и статистических

проверку состояния

 

управление. Таблицы

подпрограмм.

и обработку ошибок

 

решений. Простые

Базовые матричные /

 

 

обратные вызовы

векторные операции

 

 

(callbacks) или

 

 

 

передачи сообщений,

 

 

 

включение

 

 

 

среднего уровня —

 

 

 

поддержка

 

 

 

распределенной

 

 

 

обработки

 

 

Высокий

Высокая вложенность

Базовый численный

Операции ввода-вывода

 

операторов

анализ:

физического уровня

 

с составными

мультивариантная

(определение адресов

 

предикатами.

интерполяция, обычные

физической памяти;

 

Управление

дифференциальные

поиски, чтения и т. д.).

 

очередями и стеками.

уравнения. Базисное

Оптимизированный

 

Однородная

усечение, учет потерь

совмещенный

 

распределенная

точности

ввод-вывод

 

обработка. Управление

 

 

 

ПО реального времени

 

 

 

на единственном

 

 

 

процессоре

 

 

Очень высокий

Реентерабельное

Сложный, но

Процедуры для

 

и рекурсивное

структурированный

диагностики

 

программирование.

численный анализ:

по прерыванию,

 

Обработка прерываний

уравнения с плохо

обслуживание

 

с фиксированными

обусловленными

и маскирование

.

приоритетами

матрицами, уравнения

прерываний.

 

Синхронизация задач,

в частных производных.

Обслуживание линий

 

сложные обратные

Простой параллелизм

связи.

 

вызовы, гетерогенная

 

Высокопроизводитель-

 

распределенная

 

ные встроенные

 

обработка. Управление

 

системы

 

однопроцессорной

 

 

 

системой в реальном

 

 

 

времени

 

 

Сверхвысокий

Планирование

Сложный

Программирование

 

множественных

и неструктурированный

с учетом временных

 

ресурсов с динамически

численный анализ:

характеристик

 

изменяющимися

высокоточный анализ

приборов,

 

приоритетами.

стохастических данных

микропрограммные

 

Управление на уровне

с большим количеством

операции. Критические

 

микропрограмм.

шумов. Сложный

к производительности

 

Управление

параллелизм

встроенные системы

 

распределенной

 

 

 

аппаратурой в реальном

 

 

 

времени

 

 

Таблица А.4. Сложность модуля в зависимости от области применения

CPLX

Операции управления данными

Операции управления пользовательским интерфейсом

Очень низкий

Простые массивы в оперативной памяти. Простые запросы к БД, обновления

Простые входные формы, генераторы отчетов

Низкий

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

Использование билдеров для простых графических интерфейсов

Номинальный

Ввод из нескольких файлов и вывод в один файл. Простые структурные изменения, простое редактирование. Сложные запросы БД, обновления

Простое использование набора графических объектов (widgets)

Высокий

Простые триггеры, активизируемые содержимым потока данных. Сложное изменение структуры данных

Разработка набора графических объектов, его расширение. Простой голосовой ввод-вывод, мультимедиа

Очень высокий

Координация распределенных БД. Сложные триггеры. Оптимизация поиска

Умеренно сложная 2D/3D-графика, динамическая графика, мультимедиа

Сверхвысокий

Динамические реляционные и объектные структуры с высоким сцеплением. Управление данными с помощью естественного языка

Сложные мультимедиа, виртуальная реальность

Таблица А.5. Требуемая повторная используемость (Required Reusability) RUSE

Фактор

Очень низкий

Низкий

Номинальный

Высокий

Очень высокий

Сверхвысокий

RUSE

 

Нет

На уровне проекта

На уровне программы

На уровне семейства продуктов

На уровне нескольких семейств продуктов

Таблица А.6. Документирование требований жизненного цикла (Documentation match to life-cycle needs) DOCU

Фактор

Очень низкий

Низкий

Номинальный

Высокий

Очень высокий

Сверхвысокий

DOCU

Многие требования жизненного цикла не учтены

Некоторые требования жизненного цикла не учтены

Оптимизированы к требованиям жизненного цикла

Избыточны по отношению к требованиям жизненного цикла

Очень избыточны по отношению к ребованиям жизненного цикла

 

 

Факторы платформы (виртуальной машины)

Таблица А.7. Ограничения времени выполнения (Execution Time Constraint) TIME

Фактор

Очень низкий

Низкий

Номинальный

Высокий

Очень высокий

Сверхвысокий

TIME

 

 

Используется ? 50% возможного времени выполнения

70%

85%

95%

Таблица А.8. Ограничения оперативной памяти (Main Storage Constraint) STOR

Фактор

Очень низкий

Низкий

Номинальный

Высокий

Очень высокий

Сверхвысокий

STOR

 

 

Используется ? 50% доступной памяти

70%

85%

95%

Таблица А.9. Изменчивость платформы (Platform Volatility) PVOL

Фактор

Очень низкий

Низкий

Номинальный

Высокий

Очень высокий

Сверхвысокий

PVOL

 

Значительные изменения — каждые 12мес.; незначительные — каждый месяц

Значительные изменения — каждые 6 мес.; незначительные — каждые 2 недели

Значительные изменения — 2 мес.; незначительные — 1 неделя

Значительные изменения — 2нед.; незначительные — 2 дня

 

 

Факторы персонала

Таблица А. 10. Возможности аналитика (Analyst Capability) ACAP

Фактор

Очень низкий

Низкий

Номинальный

Высокий

Очень высокий

Сверхвысокий

АСАР

15%

35%

55%

75%

90%

 

Таблица А.11. Возможности программиста (Programmer Capability) PCAP

Фактор

Очень низкий

Низкий

Номинальный

Высокий

Очень высокий

Сверхвысокий

РСАР

15%

35%

55%

75%

90%

 

Таблица А. 12. Опыт работы с приложением (Applications Experience) AEXP

Фактор

Очень низкий

Низкий

Номинальный

Высокий

Очень высокий

Сверхвысокий

АЕХР

2 месяца

6 месяцев

1 год

3 года

6 лет

 

Таблица А. 13. Опыт работы с платформой (Platform Experience) PEXP

Фактор

Очень низкий

Низкий

Номинальный

Высокий

Очень высокий

Сверхвысокий

РЕХР

2 месяца

6 месяцев

1 год

3 года

6 лет

 

Таблица А. 14. Опыт работы с языком и утилитами (Language and Tool Experience) LTEX

Фактор

Очень низкий

Низкий

Номинальный

Высокий

Очень высокий

Сверхвысокий

LTEX

2 месяца

6 месяцев

1 год

Згода

6 лет

 

Таблица А. 15. Непрерывность персонала (Personnel Continuity) PCON

Фактор

Очень низкий

Низкий

Номинальный

Высокий

Очень высокий

Сверхвысокий

PCON

48%/год

24%/год

12%/год

6%/год

3%/год

 

ПРИМЕЧАНИЕ

С помощью фактора PCON учитывается процент смены персонала.

 

Факторы проекта

Таблица А. 16. Использование программных утилит (Use of Software Tools) TOOL

Фактор

Очень низкий

Низкий

Номинальный

Высокий

Очень высокий

Сверхвысокий

TOOL

Редактирование, кодирование, отладка

Простая входная, выходная CASE-утилита, малая интеграция

Базовые утилиты жизненного цикла, умеренная интеграция

Развитые утилиты жизненного цикла, умеренная интеграция

Развитые утилиты жизненного цикла, хорошо интегрированные с процессами, методами, повторным использованием

 

Таблица А. 17. Мультисетевая разработка (Multisite Development) SITE

Фактор

Очень низкий

Низкий

Номинальный

Высокий

Очень высокий

Сверхвысокий

SITE: комму-никации

Один телефон, почта

Индивидуаль-ные телефоны, FAX

Узкополосный e- mail

Широкопо-лосные электронные коммуника-ции

Широкополо-сные электронные коммуникации, видеоконференции от случая к случаю

Интерактивные мультимедиа

Таблица А. 18. Требуемый график разработки (Required Development Schedule) SCED

Фактор

Очень низкий

Низкий

Номинальный

Высокий

Очень высокий

Сверхвысокий

SCED

75% от номинального срока

85%

100%

130%

160%

 

Таблица А. 19. Числовые значения множителей затрат

Фактор

Очень низкий

Низкий

Номинальный

Высокий

Очень высокий

Сверхвысокий

RELY

Легкое беспокойство 0,75

Низкая, легко восстанавливаемые потери 0,88

Умеренная, легко восстанавливаемые потери 1,00

Высокая, финансовые потери 1,15

Риск для человеческой жизни 1,39

 

DATA

 

байты БД/LOС прогр. <10 0,93

10D/P<100 1,00

100D/P< <1000 1,09

D/P 1000 1,19

 

CPLX

0,75

0,88

1,00

1,15

1,30

1,66

RUSE

 

Нет

На уровне

На уровне

На уровне

На уровне

 

0,91

проекта

программы

семейства

нескольких

 

 

1,00

1,14

продуктов

семейств

 

 

 

 

1,29

продуктов

 

 

 

 

 

1,49

DOCU

Многие

Некоторые

Оптимизирова-

Избыточны

Очень

 

требования

требования

ны

по

избыточны

 

жизненного

жизненного

к требованиям

отношению к

по отношению

 

цикла

цикла

жизненного

требованиям

к требованиям

 

не учтены

не учтены

цикла

жизненного

жизненного

 

0,89

0,95

1,00

цикла

цикла

 

 

 

 

1,06

1,13

 

TIME

 

 

Используется

70%

85%

95%

 

 

< 50%

1,11

1,31

1,67

 

 

возможного

 

 

 

 

 

времени

 

 

 

 

 

выполнения

 

 

 

 

 

1,00

 

 

 

STOR

 

 

Используется

70%

85%

95%

 

 

< 50%

1,06

1,21

1,57

 

 

доступной

 

 

 

 

 

памяти

 

 

 

 

 

1,00

 

 

 

PVOL

 

Значительные

Значительные—

Значительные

Значтельные —

 

 

изменения —

через 6 мес.;

—через 2

через 2 нед.

 

 

через 1 год;

незначительные

мес.; незначи-

незначительные

 

 

незначитель

— через

тельные —

— через 2 дня

 

 

ные —

2 недели

через 1

1,30

 

 

через 1 мес.

1,00

неделю

 

 

 

0,87

 

1,15

 

 

ACAP

15%

35%

55%

75%

90%

 

1,50

1,22

1,00

0,83

0,67

 

PCAP

15%

35%

55%

75%

90%

 

1,37

1,16

1,00

0,87

0,74

 

PCON

48%/год

24%/год

12%/год

6%/год

3%/год

 

1,24

1,10

1,00

0,92

0,84

 

AEXP

?2 месяцев

6 месяцев

1 год

3 года

6 лет

 

1,22

1,10

1,00

0,89

0,81

 

PEXP

? 2 месяцев

6 месяцев

1 год

Згода

6 лет

 

1,25

1,12

1,00

0,88

0,81

 

LTEX

?2 месяцев

6 месяцев

1 год

3 года

6 лет

 

 

1,22

1,10

1,00

0,91

0,84

 

TOOL

Редактирование,

Простая

Базовые

Развитые

Развитые

 

кодирование,

входная,

утилиты

утилиты

утилиты

 

отладка

выходная

жизненного

жизненного

жизненного

 

1,24

CASE

цикла,

цикла,

цикла, хорошо

 

 

утилита, малая

умеренная

умеренная

интегрированы

 

 

интеграция

интеграция

интеграция

с процессами,

 

 

1,12

1,00

0,86

методами,

 

 

 

 

 

повторным

 

 

 

 

 

использованием

 

 

 

 

 

0,72

 

SITE

Один телефон,

Индивиду-

Узкополосный

Широкополое

Широко-

Интерактивные

комму-

почта

альные

e-mail

коммуника-

полосные

мультимедиа

ника-

1,25

телефоны,

1,00

ции

коммуникации,

0,78

ции

 

FAX

 

0,92

иногда

 

 

1,10

 

 

видеокон-

 

 

 

 

 

ференции

 

 

 

 

 

0,84

 

SCED

75%

85%

100%

130%

160%

 

от номин.

1,10

1,00

1,00

1,00

 

1,29

 

 

 

 

 

Таблица А. 19 обеспечивает перевод оценок факторов затрат в числовые значения множителей затрат. Порядок использования таблицы чрезвычайно прост. Имя фактора определяет строку таблицы, оценка фактора — столбец. На пересечении строки и столбца находим числовое значение множителя. Например, фактору TIME с оценкой Высокий соответствует множитель со значением 1,11.