2.3.1. Паттерн «Адаптер

advertisement
Министерство образования и науки, молодежи и спорта Украины
Севастопольский национальный технический
университет
МЕТОДИЧЕСКИЕ УКАЗАНИЯ
к лабораторным работам № 1 – 5
по дисциплине
«Технология создания программных продуктов»,
для студентов дневной формы обучения
направления 6.050101 – «Компьютерные науки»
Севастополь
2013
УДК 004.415.53
Методические указания к лабораторным работам по дисциплине «Технология
создания программных продуктов» для студентов дневной формы обучения
направления 6.050101 – «Компьютерные науки»/Сост. В. А. Строганов,
А. Ю. Дрозин – Севастополь: Изд-во СевНТУ, 2013. – 66 с.
Методические указания призваны обеспечить возможность выполнения студентами
лабораторных работ по дисциплине «Технология создания программных продуктов».
Методические указания составлены в соответствии с требованиями программы дисциплины
«Технология создания программных продуктов» для студентов направления 6.050101 и
утверждены на заседании кафедры Информационных систем, протокол №
от « »
2013 г.
Допущено учебно-методическим центром СевНТУ в качестве методических
указаний.
3
Содержание
Общие положения......................................................................................................4
Лабораторная работа №1...........................................................................................5
Лабораторная работа №2..........................................................................................17
Лабораторная работа №3..........................................................................................25
Лабораторная работа №4..........................................................................................40
Лабораторная работа №5..........................................................................................46
Лабораторная работа №6..........................................................................................51
Лабораторная работа №7..........................................................................................58
Библиографический список......................................................................................66
4
Общие положения
Целью лабораторных работ является получения практических навыков
модульного и интеграционного тестирования программного обеспечения, а
также профилирования программного кода.
Данный раздел лабораторного практикума представляет собой цикл из
шести лабораторных работ. В лабораторных работах № 4 и № 5
рассматриваются общие принципы модульного и интеграционного
тестирования программного обеспечения. Лабораторная работа № 6 позволяет
получить практические навыки модульного тестирования с использованием
среды NUnit. В лабораторной работе № 7 рассматриваются основные принципы
профилирования программного обеспечения на примере профилировщика
EQATECProfiler. В лабораторной работе № 3 рассматриваются основные
принципы работы с распределенными системами контроля версий, а также
рассматриваются практические аспекты использования данных систем на
примере Mercurial. Лабораторные работы № 1 и 2 посвящены использованию
паттернов при объектно-ориентированном проектировании программных
продуктов.
В качестве лабораторной установки используется персональный
компьютер и программное обеспечение: среда разработки Microsoft Visual
Studio, среда тестирования NUnit, профилировщик EQATECProfiler, а также
распределенная система контроля версий Mercurial. Порядок работы со средой
тестирования, профилировщиком и системой контроля версий подробно
рассмотрен в разделах 2.2 работ № 3, № 6 и №7.
Время на выполнение лабораторных работ распределяется следующим
образом: лабораторная работа № 1 – 2 часа, лабораторная работа № 2 – 2 часа,
лабораторная работа № 3 – 2 часа, лабораторная работа № 4 – 3 часа,
лабораторная работа № 5 – 4 часа, лабораторная работа № 6 – 2 часа,
лабораторная работа № 7 – 2 часа. Варианты
заданий выдаются
преподавателем студенту лично.
Объем работы распределяется следующим образом: в ходе домашней
самостоятельной подготовки студенты изучают необходимый теоретический
материал. В ходе аудиторных занятий выполняется построение диаграмм
классов (лабораторные работы №1 и №2), написание необходимых
программных модулей, тестирование (работы № 4 – 6) и профилирование
(работа № 7) программ, а также выполнение необходимых операций над
репозиторием (работа № 3).
Результаты лабораторных работ оформляются студентом в виде отчета,
включающего название работы, цель работы, постановку задачи, результаты
работы в виде программного кода, графиков, диаграмм и словесного описания,
а также выводы по результатам работы.
5
Лабораторная работа №1
Исследование способов применения структурных паттернов
проектирования
1. Цель работы
Исследовать возможность использования структурных паттернов
проектирования. Получить практические навыки применения структурных
паттернов при объектно-ориентированном проектировании.
2. Основные положения
2.1. Паттерны проектирования
Паттерны (шаблоны) проектирования [3] представляют собой инструмент,
который
позволяет
документировать
опыт
разработки
объектноориентированных программ. В основе использования паттернов лежит
следующая идея: при проектировании каждый проект не разрабатывается с
нуля, а используется опыт предыдущих проектов. То есть паттерны
проектирования упрощают повторное использование удачных проектных и
архитектурных решений. Представление прошедших проверку временем
методик в виде паттернов проектирования облегчает доступ к ним со стороны
разработчиков новых систем.
Во
многих
объектно-ориентированных
системах
встречаются
повторяющиеся паттерны, состоящие из классов и взаимодействующих
объектов. С их помощью решаются конкретные задачи проектирования, в
результате чего объектно-ориентированный дизайн становится более гибким,
элегантным, и им можно воспользоваться повторно. Проектировщик, знакомый
с паттернами, может сразу же применять их к решению новой задачи, не
пытаясь каждый раз изобретать велосипед.
Описание каждого паттерна принято разбивать на следующие разделы:
- Название и классификация паттерна. Название паттерна должно четко
отражать его назначение. Классификация паттернов проводится в соответствии
со схемой, которая будет рассмотрена ниже.
- Назначение. Лаконичный ответ на следующие вопросы: каковы функции
паттерна, его обоснование и назначение, какую конкретную задачу
проектирования можно решить с его помощью.
- Известен также под именем. Другие распространенные названия
паттерна, если таковые имеются.
- Мотивация. Сценарий, иллюстрирующий задачу проектирования и то,
как она решается данной структурой класса или объекта. Благодаря мотивации
можно лучше понять последующее, более абстрактное описание паттерна.
- Применимость. Описание ситуаций, в которых можно применять данный
паттерн. Примеры проектирования, которые можно улучшить с его помощью.
Распознавание таких ситуаций.
6
- Структура. Графическое представление классов в паттерне с
использованием нотации, основанной на методике Object Modeling Technique
(OMT). Могут использоваться также диаграммы взаимодействий для
иллюстрации последовательностей запросов и отношений между объектами.
- Участники. Классы или объекты, задействованные в данном паттерне
проектирования, и их функции.
- Отношения. Взаимодействие участников для выполнения своих
функций.
- Результаты.
Насколько
паттерн
удовлетворяет
поставленным
требованиям? Результаты применения, компромиссы, на которые приходится
идти. Какие аспекты поведения системы можно независимо изменять,
используя данный паттерн?
- Реализация. Сложности и так называемые подводные камни при
реализации паттерна. Советы и рекомендуемые приемы. Есть ли у данного
паттерна зависимость от языка программирования?
- Пример
кода
программы.
Фрагмент
программного
кода,
иллюстрирующий вероятную реализацию на языках C++ или Smalltalk.
- Известные применения. Возможности применения паттерна в реальных
системах. Даются, по меньшей мере, два примера из различных областей.
- Родственные паттерны. Связь других паттернов проектирования с
данным. Важные различия. Использование данного паттерна в сочетании с
другими.
2.2. Порядок использования паттернов проектирования
1. Прочитать описание паттерна (см. ниже), чтобы получить о нем общее
представление. Особое внимание обратить на разделы «Применимость» и
«Результаты». Убедиться, что выбранный паттерн действительно подходит для
решения данной задачи.
2. Изучить разделы описания паттерна «Структура», «Участники» и
«Отношения». Детально проанализировать назначение упоминаемых в паттерне
классов и объектов и то, как они взаимодействуют друг с другом.
3. Посмотреть на раздел «Пример кода», где приведен конкретный пример
использования паттерна в программе. Изучение программного кода поможет
понять, как нужно реализовывать паттерн.
4. Выбрать для участников паттерна подходящие имена. Имена участников
паттерна обычно слишком абстрактны, чтобы употреблять их непосредственно
в коде. Тем не менее, бывает полезно включить имя участника как имя в
программе. Это помогает сделать паттерн более очевидным при реализации.
Например, при использовании паттерна Стратегия в алгоритме размещения
текста, классы могли бы называться SimpleLayoutStrategy или
TeXLayoutStrategy.
5. Определить классы. Объявить их интерфейсы, установить отношения
наследования и определить переменные экземпляра, которыми будут
представлены данные объекты и ссылки на другие объекты. Выявить
7
имеющиеся в вашем приложении классы, на которые паттерн оказывает
влияние, и соответствующим образом модифицировать их.
6. Определить имена операций, встречающихся в паттерне. Здесь, как и в
предыдущем случае, имена обычно зависят от приложения. При этом следует
руководствоваться теми функциями и взаимодействиями, которые
ассоциированы с каждой операцией. Кроме того, нужно быть
последовательным при выборе имен. Например, для обозначения фабричного
метода можно было бы всюду использовать префикс Create-.
7. Реализовать операции, которые выполняют обязанности и отвечают за
отношения, определенные в паттерне. Советы о том, как это лучше сделать,
можно найти в разделе «Реализация». Поможет и «Пример кода».
2.3. Структурные паттерны
В структурных паттернах рассматривается вопрос о том, как из классов и
объектов образуются более крупные структуры. Структурные паттерны уровня
класса используют наследование для составления композиций из интерфейсов и
реализаций. Простой пример – использование множественного наследования
для объединения нескольких классов в один. В результате получается класс,
обладающий свойствами всех своих родителей. Особенно полезен этот паттерн,
когда нужно организовать совместную работу нескольких независимо
разработанных библиотек.
Другой пример паттерна уровня класса – Адаптер. В общем случае
Адаптер делает интерфейс одного класса (адаптируемого) совместимым с
интерфейсом другого, обеспечивая тем самым унифицированную абстракцию
разнородных интерфейсов. Это достигается за счет закрытого наследования
адаптируемому классу. После этого адаптер выражает свой интерфейс в
терминах операций адаптируемого класса.
Вместо композиции интерфейсов или реализаций структурные паттерны
уровня объекта компонуют объекты для получения новой функциональности.
Дополнительная гибкость в этом случае связана с возможностью изменить
композицию объектов во время выполнения, что недопустимо для статической
композиции классов.
Примером структурного паттерна уровня объектов является Компоновщик.
Он описывает построение иерархии классов для двух видов объектов:
примитивных и составных. Последние позволяют создавать произвольно
сложные структуры из примитивных и других составных объектов.
В паттерне Заместитель объект берет на себя функции другого объекта. У
Заместителя есть много применений. Он может действовать как локальный
представитель объекта, находящегося в удаленном адресном пространстве. Или
представлять большой объект, загружаемый по требованию. Или ограничивать
доступ к критически важному объекту. Заместитель вводит дополнительный
косвенный уровень доступа к отдельным свойствам объекта. Поэтому он может
ограничивать, расширять или изменять эти свойства.
Паттерн Приспособленец определяет структуру для совместного
8
использования объектов. Владельцы разделяют объекты, по меньшей мере, по
двум причинам: для достижения эффективности и непротиворечивости.
Приспособленец акцентирует внимание на эффективности использования
памяти. В приложениях, в которых участвует очень много объектов, должны
снижаться накладные расходы на хранение. Значительной экономии можно
добиться за счет разделения объектов вместо их дублирования. Но объект
может быть разделяемым, только если его состояние не зависит от контекста. У
объектов-приспособленцев такой зависимости нет. Любая дополнительная
информация передается им по мере необходимости. В отсутствие контекстных
зависимостей объекты-приспособленцы могут легко разделяться.
Если паттерн Приспособленец дает способ работы с большим числом
мелких объектов, то Фасад показывает, как один объект может представлять
целую подсистему. Фасад представляет набор объектов и выполняет свои
функции, перенаправляя сообщения объектам, которых он представляет.
Паттерн Мост отделяет абстракцию объекта от его реализации, так что их
можно изменять независимо.
Паттерн Декоратор описывает динамическое добавление объектам новых
обязанностей. Это структурный паттерн, который рекурсивно компонует
объекты с целью реализации заранее неизвестного числа дополнительных
функций. Например, объект-декоратор, содержащий некоторый элемент
пользовательского интерфейса, может добавить к нему оформление в виде
рамки или тени либо новую функциональность, например возможность
прокрутки или изменения масштаба. Два разных оформления прибавляются
путем простого вкладывания одного декоратора в другой. Для достижения этой
цели каждый объект-декоратор должен соблюдать интерфейс своего компонента
и перенаправлять ему сообщения. Свои функции (скажем, рисование рамки
вокруг компонента) декоратор может выполнять как до, так и после
перенаправления сообщения.
2.3.1. Паттерн «Адаптер»
Название и классификация паттерна
Адаптер – паттерн, структурирующий классы и объекты.
Назначение
Преобразует интерфейс одного класса в интерфейс другого, который
ожидают клиенты. Адаптер обеспечивает совместную работу классов с
несовместимыми интерфейсами, которая без него была бы невозможна.
Известен также под именем
Wrapper (обертка).
Мотивация
Иногда класс из инструментальной библиотеки, спроектированный для
повторного использования, не удается использовать только потому, что его
9
интерфейс не соответствует тому, который нужен конкретному приложению.
Рассмотрим, например, графический редактор, благодаря которому
пользователи могут рисовать на экране графические элементы (линии,
многоугольники, текст и т.д.) и организовывать их в виде картинок и диаграмм.
Основной абстракцией графического редактора является графический объект,
который имеет изменяемую форму и изображает сам себя. Интерфейс
графических объектов определен абстрактным классом Shape. Редактор
определяет подкласс класса Shape для каждого вида графических объектов:
LineShape для прямых, PolygonShape для многоугольников и т.д.
Классы для элементарных геометрических фигур, например LineShape и
PolygonShape, реализовать сравнительно просто, поскольку заложенные в них
возможности рисования и редактирования крайне ограничены. Но подкласс
TextShape, умеющий отображать и редактировать текст, уже значительно
сложнее, поскольку даже для простейших операций редактирования текста
нужно нетривиальным образом обновлять экран и управлять буферами. В то же
время, возможно, существует уже готовая библиотека для разработки
пользовательских интерфейсов, которая предоставляет развитый класс
TextView, позволяющий отображать и редактировать текст. В идеале мы хотели
бы повторно использовать TextView для реализации TextShape, но библиотека
разрабатывалась без учета классов Shape, поэтому заставить объекты TextView
и Shape работать совместно не удается.
Так каким же образом существующие и независимо разработанные классы
вроде TextView могут работать в приложении, которое спроектировано под
другой, несовместимый интерфейс? Можно было бы так изменить интерфейс
класса TextView, чтобы он соответствовал интерфейсу Shape, только для этого
нужен исходный код. Но даже если он доступен, то вряд ли разумно изменять
TextView; библиотека не должна приспосабливаться к интерфейсам каждого
конкретного приложения.
Вместо этого мы могли бы определить класс TextShape так, что он будет
адаптировать интерфейс TextView к интерфейсу Shape. Это допустимо сделать
двумя способами:
- наследуя интерфейс от Shape, а реализацию от TextView;
- включив экземпляр TextView в TextShape и реализовав TextShape в
терминах интерфейса TextView. Два данных подхода соответствуют вариантам
паттерна Адаптер в его классовой и объектной ипостасях. Класс TextShape мы
будем называть адаптером.
10
Рисунок 2.1 – Пример использования паттерна Адаптер
На рисунке 2.1 показан адаптер объекта. Видно, как запрос BoundingBox,
объявленный в классе Shape, преобразуется в запрос GetExtent, определенный
в классе TextView. Поскольку класс TextShape адаптирует TextView к
интерфейсу Shape, графический редактор может воспользоваться классом
TextView, хотя тот и имеет несовместимый интерфейс.
Часто адаптер отвечает за функциональность, которую не может
предоставить адаптируемый класс. На диаграмме показано, как адаптер
выполняет такого рода функции. У пользователя должна быть возможность
перемещать любой объект класса Shape в другое место, но в классе TextView
такая операция не предусмотрена. TextShape может добавить недостающую
функциональность, самостоятельно реализовав операцию CreateManipulator
класса Shape, которая возвращает экземпляр подходящего подкласса
Manipulator.
Manipulator – это абстрактный класс объектов, которым известно, как
анимировать Shape в ответ на такие действия пользователя, как перетаскивание
фигуры в другое место. У класса Manipulator имеются подклассы для
различных фигур. Например, TextManipulator – подкласс для TextShape.
Возвращая экземпляр TextManipulator, объект класса TextShape добавляет
новую функциональность, которой в классе TextView нет, а классу Shape
требуется.
Применимость
Паттерн Адаптер следует применяит, когда:
- необходмио использовать существующий класс, но его интерфейс не
соответствует вашим потребностям;
- нужно создать повторно используемый класс, который должен
взаимодействовать с заранее неизвестными или не связанными с ним классами,
имеющими несовместимые интерфейсы;
- (только для адаптера объектов!) нужно использовать несколько
существующих подклассов, но непрактично адаптировать их интерфейсы путем
порождения новых подклассов от каждого. В этом случае адаптер объектов
11
может приспосабливать интерфейс их общего родительского класса.
Структура
Адаптер класса использует множественное наследование для адаптации
одного интерфейса к другому. Структура адаптера класса показана на рисунке
2.2.
Рисунок 2.2 – Структура адаптера класса
Адаптер объекта применяет композицию объектов. Структура адаптера
уровня объектов показана на рисунке 2.3.
Рисунок 2.3 – Структура адаптера объекта
Участники
- Target (Shape) – целевой. Определяет зависящий от предметной области
интерфейс, которым пользуется Client;
- Client (DrawingEditor) – клиент: вступает во взаимоотношения с
объектами, удовлетворяющими интерфейсу Target;
- Adaptee (Textview) – адаптируемый: определяет существующий
интерфейс, который нуждается в адаптации;
- Adapter (Text Shape) – адаптер: адаптирует интерфейс Adaptee к
интерфейсу Target.
Отношения
Клиенты вызывают операции экземпляра адаптера Adapter. В свою
очередь адаптер вызывает операции адаптируемого объекта или класса
Adaptee, который и выполняет запрос.
12
Результаты
Результаты применения адаптеров объектов и классов различны. Адаптер
класса:
- адаптирует Adaptee к Target, перепоручая действия конкретному классу
Adaptee. Поэтому данный паттерн не будет работать, если мы захотим
одновременно адаптировать класс и его подклассы;
- позволяет
адаптеру
Adapter
заместить
некоторые
операции
адаптируемого класса Adaptee, так как Adapter есть не что иное, как подкласс
Adaptee;
- вводит только один новый объект. Чтобы добраться до адаптируемого
класса, не нужно никакого дополнительного обращения по указателю.
Адаптер объектов:
- позволяет одному адаптеру Adapter работать со многим адаптируемыми
объектами Adaptee, то есть с самим Adaptee и его подклассами (если таковые
имеются). Адаптер может добавить новую функциональность сразу всем
адаптируемым объектам;
- затрудняет замещение операций класса Adaptee. Для этого потребуется
породить от Adaptee подкласс и заставить Adapter ссылаться на этот подкласс,
а не на сам Adaptee.
Пример кода программы
Ниже приводится краткий обзор реализации адаптеров класса и объекта
для примера, рассмотренного в разделе «Мотивация».
class Shape {
public:
Shape();
virtual void BoundingBox(Points bottomLeft, Point& topRight)
const;
virtual Manipulator* CreateManipulator() const;
};
class TextView {
public:
TextView();
void GetOrigin(Coord& x, Coords y) const;
void GetExtent(Coord& width, Coords height) const;
virtual bool IsEmpty() const;
};
В классе Shape предполагается, что ограничивающий фигуру
прямоугольник определяется двумя противоположными углами. Напротив, в
классе TextView он характеризуется начальной точкой, высотой и шириной. В
классе Shape определена также операция CreateManipulator() для создания
объекта-манипулятора класса Manipulator, который знает, как анимировать
фигуру в ответ на действия пользователя. В TextView эквивалентной операции
13
нет. Класс TextShape является адаптером между двумя этими интерфейсами.
Для адаптации интерфейса адаптер класса использует множественное
наследование. Принцип адаптера класса состоит в наследовании интерфейса по
одной ветви и реализации – по другой. В C++ интерфейс обычно наследуется
открыто, а реализация – закрыто. Мы будем придерживаться этого соглашения
при определении адаптера TextShape:
class TextShape : public Shape, private TextView {
public:
TextShape();
virtual void BoundingBox(Point& bottomLeft, Points topRight)
const;
virtual bool IsEmptyO const;
virtual Manipulator* CreateManipulator() const;
};
Операция BoundingBox преобразует интерфейс TextView к интерфейсу
Shape:
void TextShape::BoundingBox (Points bottomLeft, Point& topRight)
const
{
Coord bottom, left, width, height;
GetOrigin(bottom, left);
GetExtent(width, height);
bottomLeft = Point(bottom, left);
topRight = Point(bottom + height, left + width);
}
На примере операции IsEmpty демонстрируется прямая переадресация
запросов, общих для обоих классов:
bool TextShape::IsEmpty () const
{
return TextView::IsEmpty();
}
Наконец, определим операцию CreateManipulator (отсутствующую в
классе TextView) с нуля. Предположим, класс TextManipulator, который
поддерживает манипуляции с TextShape, уже реализован:
Manipulator* TextShape::CreateManipulator () const
{
return new TextManipulator(this);
}
Адаптер объектов применяет композицию объектов для объединения
классов с разными интерфейсами. При таком подходе адаптер TextShape
14
содержит указатель на TextView:
class TextShape : public Shape {
public:
TextShape(TextView*);
virtual void BoundingBox(Point& bottomLeft, Points topRight)
const;
virtual bool IsEmptyO const;
virtual Manipulator* CreateManipulator() const;
private:
TextView* _text;
};
Объект TextShape должен инициализировать указатель на экземпляр
TextView. Делается это в конструкторе. Кроме того, он должен вызывать
операции объекта TextView всякий раз, как вызываются его собственные
операции.
В этом примере мы предположим, что клиент создает объект TextView и
передает его конструктору класса TextShape:
TextShape::TextShape (TextView* t)
{
_text = t;
}
void TextShape::BoundingBox (Points bottomLeft, Point& topRight)
const
{
Coord bottom, left, width, height;
_text->GetOrigin(bottom, left);
_text->GetExtent(width, height);
bottomLeft = Point(bottom, left);
topRight = Point(bottom + height, left + width);
}
bool TextShape::IsEmpty () const
{
return _text->IsEmpty();
}
Реализация CreateManipulator не зависит от версии адаптера класса,
поскольку реализована с нуля и не использует повторно никакой
функциональности TextView:
Manipulator* TextShape::CreateManipulator () const
{
return new TextManipulator(this);
}
Сравним этот код с кодом адаптера класса. Для написания адаптера объекта
нужно потратить чуть больше усилий, но зато он оказывается более гибким.
15
Например, вариант адаптера объекта TextShape будет прекрасно работать и с
подклассами TextView: клиент просто передает экземпляр подкласса TextView
конструктору TextShape.
3. Варианты заданий
В соответствии с вариантом задания, полученным от преподавателя,
выбирается предметную область из перечисленных ниже:
- обслуживание клиента в банке;
- обучение студента в университете;
- строительство дома;
- учет успеваемости студентов;
- отдел кадров предприятия;
- фирма по продаже бытовой техники;
- фирма по продаже компьютерной техники;
- библиотека ВУЗа;
- офис юридической компании;
- редакция журнала или газеты;
- работа страховой компании;
- работа банкомата.
4. Порядок выполнения работы
4.1. Ознакомиться
с
основными
преимуществами
объектноориентированного проектирования на основе паттернов, изучить порядок
проектирования с использованием паттернов. Изучить назначение и структуру
паттерна Адаптер (выполнить в ходе самостоятельной подготовки).
4.2. Применительно к заданной по варианту предметной области
проанализировать возможность использования паттерна Адаптер. Для этого
выполнить анализ заданной предметной области, построить диаграмму классов,
на диаграмме классов найти класс-клиент и адаптируемый класс,
функциональностью которого должен воспользоваться клиент.
4.3. Выполнить перепроектирование диаграммы классов использовав
паттерн Адаптер.
4.4. Сравнить полученные диаграммы классов, сделать выводы и
целесообразности использования паттерна Адаптер для данной системы.
4.5. На основе UML-диаграммы выполнить синтез программного кода,
скомпилировать программу и продемонстрировать ее работоспособность.
5. Содержание отчета
5.1. Цель работы.
5.2. Постановка задачи с указанием моделируемой предметной области.
5.3. Словесное описание мотивации применения паттерна Адаптер при
проектировании данной системы.
16
5.4. UML-диаграммы классов (исходная
использованием паттерна) с комментариями.
5.5. Текст программы.
5.6. Выводы по работе.
и
модифицированная
с
6. Контрольные вопросы
6.1. Что
понимается
под
паттерном
объектно-ориентированного
проектирования?
6.2. Из каких разделов состоит описание паттерна?
6.3. Каков общий порядок применения паттернов проектирования?
6.4. Для чего предназначены структурные паттерны проектирования?
6.5. Какие задачи решает паттерн «Адаптер»?
6.6. Какие классы входят в состав паттерна «Адаптер», каковы их
обязанности?
17
Лабораторная работа №2
Исследование способов применения поведенческих паттернов
проектирования
1. Цель работы
Исследовать возможность использования поведенческих паттернов
проектирования. Получить практические навыки применения паттернов
поведения при объектно-ориентированном проектировании.
2. Основные положения
2.1. Паттерны поведения
Паттерны поведения связаны с алгоритмами и распределением
обязанностей между объектами. Речь в них идет не только о самих объектах и
классах, но и о типичных способах взаимодействия. Паттерны поведения
характеризуют сложный поток управления, который трудно проследить во
время выполнения программы. Внимание акцентировано не на потоке
управления как таковом, а на связях между объектами.
В паттернах поведения уровня класса используется наследование – чтобы
распределить поведение между разными классами. Из них более простым и
широко распространенным является шаблонный метод, который представляет
собой абстрактное определение алгоритма. Алгоритм здесь определяется
пошагово. На каждом шаге вызывается либо примитивная, либо абстрактная
операция. Алгоритм усложняется за счет подклассов, где определены
абстрактные операции.
Другой паттерн поведения уровня класса – интерпретатор, который
представляет грамматику языка в виде иерархии классов и реализует
интерпретатор как последовательность операций над экземплярами этих
классов.
В паттернах поведения уровня объектов используется не наследование, а
композиция. Некоторые из них описывают, как с помощью кооперации
множество равноправных объектов справляется с задачей, которая ни одному из
них не под силу. Важно здесь то, как объекты получают информацию о
существовании друг друга. Объекты-коллеги могут хранить ссылки друг на
друга, но это увеличит степень связанности системы. При максимальной
степени связанности каждому объекту пришлось бы иметь информацию обо
всех остальных. Эту проблему решает паттерн посредник. Посредник,
находящийся между объектами-коллегами, обеспечивает косвенность ссылок,
необходимую для разрывания лишних связей.
Паттерн цепочка обязанностей позволяет и дальше уменьшать степень
связанности. Он дает возможность посылать запросы объекту не напрямую, а
по цепочке «объектов-кандидатов». Запрос может выполнить любой
«кандидат», если это допустимо в текущем состоянии выполнения программы.
18
Число кандидатов заранее не определено, а подбирать участников можно во
время выполнения.
Паттерн наблюдатель определяет и отвечает за зависимости между
объектами. Классический пример наблюдателя встречается в схеме
модель/вид/контроллер языка Smalltalk, где все виды модели уведомляются о
любых изменениях ее состояния.
Прочие паттерны поведения связаны с инкапсуляцией поведения в объекте
и делегированием ему запросов. Паттерн стратегия инкапсулирует алгоритм
объекта, упрощая его спецификацию и замену. Паттерн команда инкапсулирует
запрос в виде объекта, который можно передавать как параметр, хранить в
списке истории или использовать как-то иначе. Паттерн состояние
инкапсулирует состояние объекта таким образом, что при изменении состояния
объект может изменять поведение. Паттерн посетитель инкапсулирует
поведение, которое в противном случае пришлось бы распределять между
классами, а паттерн итератор абстрагирует способ доступа и обхода объектов
из некоторого агрегата.
2.2. Паттерн «Цепочка обязанностей»
Назначение
Позволяет избежать привязки отправителя запроса к его получателю, давая
шанс обработать запрос нескольким объектам. Связывает объекты-получатели в
цепочку и передает запрос вдоль этой цепочки, пока его не обработают.
Мотивация
Рассмотрим контекстно-зависимую оперативную справку в графическом
интерфейсе пользователя, который может получить дополнительную
информацию по любой части интерфейса, просто щелкнув на ней мышью.
Содержание справки зависит от того, какая часть интерфейса и в каком
контексте выбрана. Например, справка по кнопке в диалоговом окне может
отличаться от справки по аналогичной кнопке в главном окне приложения. Если
для некоторой части интерфейса справки нет, то система должна показать
информацию о ближайшем контексте, в котором она находится, например о
диалоговом окне в целом.
Поэтому естественно было бы организовать справочную информацию от
более конкретных разделов к более общим. Кроме того, ясно, что запрос на
получение справки обрабатывается одним из нескольких объектов
пользовательского интерфейса, каким именно – зависит от контекста и
имеющейся в наличии информации.
Проблема в том, что объект, инициирующий запрос (например, кнопка), не
располагает информацией о том, какой объект в конечном итоге предоставит
справку. Необходим какой-то способ отделить кнопку-инициатор запроса от
объектов, владеющих справочной информацией. Как этого добиться,
показывает паттерн цепочка обязанностей.
Идея заключается в том, чтобы разорвать связь между отправителями и
19
получателями, дав возможность обработать запрос нескольким объектам.
Запрос перемещается по цепочке объектов, пока один из них не обработает его.
Первый объект в цепочке получает запрос и либо обрабатывает его сам, либо
направляет следующему кандидату в цепочке, который ведет себя точно так же.
У объекта, отправившего запрос, отсутствует информация об обработчике.
Говорят, что у запроса есть анонимный получатель (implicit receiver).
Предположим, что пользователь запрашивает справку по кнопке Print
(печать). Она находится в диалоговом окне PrintDialog, содержащем
информацию об объекте приложения, которому принадлежит (диаграмма
объектов показана на рисунке 2.1).
Рисунок 2.1 – Диаграмма объектов
В данном случае ни кнопка aPrintButton, ни окно aPrintDialog не
обрабатывают запрос, он достигает объекта anApplication, который может его
обработать или игнорировать. У клиента, инициировавшего запрос, нет прямой
ссылки на объект, который его в конце концов обработает.
Диаграмма классов показана на рисунке 2.2. Чтобы отправить запрос по
цепочке и гарантировать анонимность получателя, все объекты в цепочке имеют
единый интерфейс для обработки запросов и для доступа к своему преемнику
(следующему объекту в цепочке). Например, в системе оперативной справки
можно было бы определить класс HelpHandler (предок классов всех объектовкандидатов или подмешиваемый класс (mixin class)) с операцией HandleHelp.
Тогда классы, которые будут обрабатывать запрос, смогут его передать своему
родителю.
Для обработки запросов на получение справки классы Button, Dialog и
Application пользуются операциями HelpHandler. По умолчанию операция
HandleHelp просто перенаправляет запрос своему преемнику. В подклассах эта
операция замещается, так что при благоприятных обстоятельствах может
выдаваться справочная информация. В противном случае запрос отправляется
дальше посредством реализации по умолчанию.
20
Рисунок 2.2 – Диаграмма классов
Применимость
Паттерн «Цепочка обязанностей» целесообразно применять, когда:
- есть более одного объекта, способного обработать запрос, причем
настоящий обработчик заранее неизвестен и должен быть найден
автоматически;
- нужно отправить запрос одному из нескольких объектов, не указывая
явно, какому именно;
- набор объектов, способных обработать запрос, должен задаваться
динамически.
Структура
Рисунок 2.3 – Диаграмма классов паттерна
«Цепочка обязанностей»
Рисунок 2.4 – Типичная структура объектов
Участники
Handler (HelpHandler) – обработчик:
21
- определяет интерфейс для обработки запросов;
- реализует связь с преемником (необязательно);
ConcreteHandler (PrintButton, PrintDialog) – конкретный обработчик:
- обрабатывает запрос, за который отвечает;
- имеет доступ к своему преемнику;
- если ConcreteHandler способен обработать запрос, то так и делает, если
не может, то направляет его своему преемнику;
Client – клиент:
- отправляет запрос некоторому объекту ConcreteHandler в цепочке.
Отношения
Когда клиент инициирует запрос, он продвигается по цепочке, пока
некоторый объект ConcreteHandler не возьмет на себя ответственность за его
обработку.
Результаты
Паттерн цепочка обязанностей имеет следующие достоинства и
недостатки:
- ослабление связанности. Этот паттерн освобождает объект от
необходимости «знать», кто конкретно обработает его запрос. Отправителю и
получателю ничего неизвестно друг о друге, а включенному в цепочку объекту
– о структуре цепочки.
Таким образом, цепочка обязанностей помогает упростить взаимосвязи
между объектами. Вместо того чтобы хранить ссылки на все объекты, которые
могут стать получателями запроса, объект должен располагать информацией
лишь о своем ближайшем преемнике;
- дополнительная гибкость при распределении обязанностей между
объектами. Цепочка обязанностей позволяет повысить гибкость распределения
обязанностей между объектами. Добавить или изменить обязанности по
обработке запроса можно, включив в цепочку новых участников или изменив ее
каким-то другим образом. Этот подход можно сочетать со статическим
порождением подклассов для создания специализированных обработчиков;
- получение запроса не гарантировано. Поскольку у запроса нет явного
получателя, то нет и гарантий, что он вообще будет обработан: он может
достичь конца цепочки и пропасть. Необработанным запрос может оказаться и в
случае неправильной конфигурации цепочки.
Реализация
При рассмотрении цепочки обязанностей следует обратить внимание на
следующие моменты:
1) реализация цепочки преемников. Есть два способа реализовать такую
цепочку:
- определить новые связи (обычно это делается в классе Handler, но можно
и в ConcreteHandler);
- использовать существующие связи.
22
2) соединение преемников. Если готовых ссылок, пригодных для
определения цепочки, нет, то их придется ввести. В таком случае класс Handler
не только определяет интерфейс запросов, но еще и хранит ссылку на
преемника.
Следовательно у обработчика появляется возможность определить
реализацию операции HandleRequest по умолчанию – перенаправление
запроса преемнику (если таковой существует). Если подкласс ConcreteHandler
не заинтересован в запросе, то ему и не надо замещать эту операцию, поскольку
по умолчанию запрос как раз и отправляется дальше.
Определение базового класса HelpHandler, в котором хранится указатель
на преемника, имеет вид:
class HelpHandler {
public:
HelpHandler(HelpHandler* s) : _successor(s) { }
virtual void HandleHelp();
private:
HelpHandler* _successor;
};
void HelpHandler::HandleHelp () {
if (_successor) {
_successor->HandleHelp();
}
}
3) представление запросов. Представлять запросы можно по-разному. В
простейшей форме, например в случае класса HandleHelp, запрос жестко
кодируется как вызов некоторой операции. Это удобно и безопасно, но
переадресовывать тогда можно только фиксированный набор запросов,
определенных в классе Handler.
Альтернатива – использовать одну функцию-обработчик, которой
передается код запроса (скажем, целое число или строка). Так можно
поддержать заранее неизвестное число запросов. Единственное требование
состоит в том, что отправитель и получатель должны договориться о способе
кодирования запроса.
Это более гибкий подход, но при реализации нужно использовать условные
операторы для раздачи запросов по их коду. Кроме того, не существует
безопасного с точки зрения типов способа передачи параметров, поэтому
упаковывать и распаковывать их приходится вручную. Очевидно, что это не так
безопасно, как прямой вызов операции.
Чтобы решить проблему передачи параметров, допустимо использовать
отдельные объекты-запросы, в которых инкапсулированы параметры запроса.
Класс Request может представлять некоторые запросы явно, а их новые
типы описываются в подклассах. Подкласс может определить другие
параметры. Обработчик должен иметь информацию о типе запроса (какой
именно подкласс Request используется), чтобы разобрать эти параметры.
Для идентификации запроса в классе Request можно определить функцию
доступа, которая возвращает идентификатор класса. Вместо этого получатель
23
мог бы воспользоваться информацией о типе, доступной во время выполнения,
если язык программирования поддерживает такую возможность.
Приведем пример функции диспетчеризации, в которой используются
объекты для идентификации запросов. Операция GetKind(), указанная в
базовом классе Request, определяет вид запроса:
void Handler::HandleRequest (Request* theRequest) {
switch (theRequest->GetKind()) {
case Help:
// привести аргумент к походящему типу
HandleHelp((HelpRequest*) theRequest);
break;
case Print:
HandlePrint((PrintRequest*) theRequest);
// ...
break;
default:
// ...
break;
}
}
Подклассы могут расширить схему диспетчеризации, переопределив
операцию HandleRequest. Подкласс обрабатывает лишь те запросы, в которых
заинтересован, а остальные отправляет родительскому классу. В этом случае
подкласс именно расширяет, а не замещает операцию HandleRequest.
Подкласс ExtendedHandler расширяет операцию HandleRequest(),
определенную в классе Handler, следующим образом:
class ExtendedHandler : public Handler {
public:
virtual void HandleRequest(Request* theRequest);
// . . .
};
void ExtendedHandler::HandleRequest (Request* theRequest) {
switch (theRequest->GetKind()) {
case Preview:
// обработать запрос Preview
break;
default:
// дать классу Handler возможность обработать
// остальные запросы
Handler::HandleRequest(theRequest);
}
}
3. Порядок выполнения работы
3.1. Изучить назначение и структуру паттерна Цепочка обязанностей
(выполнить в ходе самостоятельной подготовки).
3.2. Применительно к предметной области, заданной в лабораторной работе
24
№1, проанализировать возможность использования паттерна Цепочка
обязанностей. Для этого на диаграмме классов, разработанной в ходе работы
№ 1, найти класс-клиент, запрос от которого необходимо передавать по цепочке
объектов, и классы-получатели запросов, объекты которых целесообразно
объединять в цепочку.
3.3. Выполнить перепроектирование диаграммы классов, разработанной в
лабораторной работе №1, использовав паттерн Цепочка обязанностей.
3.4. Сравнить полученные диаграммы классов, сделать выводы и
целесообразности использования паттернов проектирования для данной
системы.
3.5. На основе UML-диаграммы выполнить синтез программного кода,
скомпилировать программу и продемонстрировать ее работоспособность.
4. Содержание отчета
4.1. Цель работы.
4.2. Постановка задачи с указанием моделируемой предметной области.
4.3. Словесное описание мотивации применения паттерна Цепочка
обязанностей при проектировании данной системы.
4.4. UML-диаграммы классов с комментариями.
4.5. Текст программы.
4.6. Выводы по работе.
5. Контрольные вопросы
5.1. Для чего предназначены поведенческие паттерны проектирования?
5.5. Какие задачи решает паттерн «Цепочка обязанностей»?
5.6. Какие классы входят в состав паттерна «Цепочка обязанностей»,
каковы их обязанности?
25
Лабораторная работа №3
Исследование распределенных систем контроля версий при коллективной
разработке программных продуктов
1. Цель работы
Исследовать основные подходы к организации взаимодействия команды
разработчиков с использованием распределенной системы контроля версий
(DVCS). Приобрести практические навыки установки и настойки DVCS
Mercurial, организации ветвей разработки и осуществление слияния.
2. Основные положения
2.1.
Общие
принципы
организации
распределенных системы контроля версий
централизованных
и
В классических централизованных системах контроля версий (Subversion,
CVS) — есть выделенное специальное хранилище называемое репозиторий, в
котором хранится программный код некоторого проекта и вся история
изменений. И вот к этому хранилищу обращаются попеременно все
работающие над проектом.
Рисунок 2.1 — Модель централизованной системы контроля версий
При использовании данной модели возникает целый ряд проблем,
связанных с тем, что все разработчики вынуждены работать с одним, общим
репозиторием. При этом главная проблема, к которой постепенно приходят все
группы разработчиков — это то что, в больших командах возможно вносить
изменения только большими частями кода, которые покрыты тестами и могут
уже использоваться. Тому много причин, но главное — страх поломать что-то
готовое в репозитирии, что кем-то используется. Где разработчикам хранить
промежуточные изменения не совсем понятно. Механизм так называемых
26
ветвей в SVN реализован достаточно сложно и не может считаться
приемлемым решением проблемы.
Еще одной важной проблемой является чрезмерная перегруженность
сервера, на котором работает централизованный репозиторий. Выход этого
сервера из строя приводит к катастрофическим последствиям.
Для решения указанных выше проблем была предложена более сложная
концепция — распределенные системы контроля версий (DVCS). У каждого
пользователя при этом есть свой локальный репозиторий, причем вовсе не
обязательно один. Централизованный репозиторий отсутствует.
Рисунок 2.2 — Модель распределенной системы контроля версий
За счет локальности достигается большая гранулярность — теперь можно
вносить изменения в репозиторий не опасаясь поломать чужой код, да и весь
проект, при этом вы всегда знаете, что история сохраняется, даже в том случае
если вы не имеете доступа к основному репозиторию, например, в случае
отсутствия доступа в интернет.
Понятие основного репозитория в случае распределенных систем контроля
довольно условное. Он основной, потому что некто его так назвал. Ничто не
мешает вам взять и забрать обновления лично у программиста Х, а ему у вас, да
и отправить свои обновления другому — тоже тривиальная задача. Естественно
если это позволяют настройки прав доступа. Таким образом, получаем, что в
распределенных системах отсутствует строгая иерархичность — все
репозитории равны, и рядом с каждым репозиторием может быть размещена
собственная рабочая копия, хотя и не обязательно.
Смотря на такую структуру, возможность локальных изменений,
возможность синхронизации состояния репозитория с кем угодно создается
ощущение, что исходные тексты проекта превратятся в кашу, и на
определенном этапе, причем совсем недалеком от начала, уже невозможно
будет как-то получить адекватное их состояние. На самом деле все не так
страшно. Мощнейшей вещью распределенных систем контроля версий —
является ветвление. В DVCS, ну по крайней мере в Mercurial, ветвление — это
повседневная операция, это в принципе основа контроля версий в данном
27
случае. Реализована она абсолютно логично и понятно, и действительно проста
в использовании.
Однако, как считают некоторые эксперты, в распределенных системах
контроля версий их распределенность не является самой интересной
особенностью. Наиболее интересным является изменение модели —
распределенные системы контроля версий работают с изменениями (changes), а
не с версиями. Если централизованная система контроля версий «думает»: у
меня есть версия 1, после этого будет версия 2, после этого версия 3 и так
далее. В распределенной системе все по другому: сначала не было ничего,
потом добавлены эти изменения, потом добавлены те, и т.д. Изменение
программной модели должно изменить модель пользователя. Теперь
разработчикам тоже необходимо мыслить в терминах изменений. Если раньше
было: «Я хочу получить версию номер Х», или «Я хочу последнюю версию», то
теперь: «Хочу получить набор изменений порграммиста Х.».
Только когда разработчики начнут мыслить в терминах «изменении», и
выбросите из головы «версии» все встанет на свои места. Именно изменение
модели работы системы контроля версий привело к существенному упрощению
слияния (merge) кода. И соответственно к более активному использованию
ветвления, использованию его там, где оно необходимо. Теперь нет
необходимости думать о сложностях последующего слияния создавать
долгоживущие ветви для команд тестирования и поддержки и создавать
короткоживущие ветви для экспериментов.
2.2. Основы работы в Mercurial
Центральным понятием Mercurial является ревизия, которая здесь
называется changeset. В связи со спецификой распределенных систем контроля
версий невозможно выдать каждой ревизии её номер, поскольку не получится
гарантировать его уникальность среди всех существующих репозиториев.
Однако каждая ревизия все-таки имеет уникальный идентификатор, в случае
Mercurial это 40-значный sha1-хеш, который учитывает все параметры ревизии.
Таким образом, у каждой новой ревизии в любом удаленном репозитории будет
свой уникальный идентификатор. Использование подобной нумерации ревизий
немного пугает начинающих пользователей, однако ничего страшного в них
нет, и использование тех или иных идентификаторов это просто дело
привычки.
Вся работа с системой контроля версий Mercurial происходит с помощью
команды hg, и во всех разделах далее будут приводиться именно консольные
команды, и консольные способы работы.
Работа с этой системой контроля версий, как, впрочем, и со всеми
остальными, начинается с создания репозитория в пустом каталоге файловой
системы. Для этого следует перейти в выбранный каталог, например,
~/repos/hgproject, и выполним команду:
> hg init
28
По команде hg init Mercurial создает репозиторий в текущем каталоге.
Если посмотреть на результат работы, то можно увидеть каталог .hg, в
которой собственно и хранится вся история работы над проектом.
Далее следует создать некое подобие обычной структуры работы над
проектом. Для этого надо создать каталог, в котором будет располагаться
проект и перейти в него, пусть это будет ~/projects.
Теперь нужно получить данные для начала работы над проектом. В общем
случае это будет все содержимое некоторого репозитория расположенного гдето на сервере. Для этого перейдем в ~/projects и выполним команду:
> hg clone ~/repos/hgproject
По команде hg
clone Mercurial «клонирует» репозиторий
расположенный по указанному адресу в текущий каталог. При этом к вам
попадает именно репозиторий, то есть хранилище, содержащее всю
существующую историю изменений. Таким образом, уже появляется два
репозитория — то есть локально получена распределенная система контроля
версий. Взаимодействие может происходить с любым имеющимся
репозиторием, так как они все равноценны, однако будем называть
репозиторий в каталоге ~/repos/hgproject "центральным", то есть введем
конвенцию на взаимодействие с системой.
С помощью текстового редактора необходимо создать новый файл в
каталоге с проектом, пусть для примера это будет readme.txt, и напишем строку
символов в этот файл. Таким образом, получен файл в проекте, который
необходимо хранить в репозитории. Перед тем, как сохранить новый файл в
репозитории следует убедиться в том, что Mercurial его видит, для этого в
каталоге с новым файлом необходимо выполнить:
>hg status
? readme.txt
Mercurial ответил, что он видит файл readme.txt, при этом этот файл пока
не находится в системе контроля версий (символ «?» слева от имени файла). По
команде status Mercurial выводит состояние рабочей копии в сравнении с
состоянием локального репозитория. Для того, чтобы сказать Mercurial, что его
необходимо версионировать выполним:
> hg add
adding readme.txt
И ещё раз:
> hg status
A readme.txt
Слева от имени файла появился символ «А», который означает что файл
readme.txt будет добавлен в систему контроля версий при следующем
29
выполнении команды commit, которая как бы вносит появившиеся изменения в
репозиторий или так сказать, подтверждает и фиксирует их там.
>hg commit
Mercurial запустит текстовый редактор и попросит ввести описание к
вносимым изменениям. Как только редактор будет закрыт, все изменения в
рабочей копии будут сохранены в локальном репозитории. Убедиться в этом
достаточно просто:
>hg log
changeset: 0:8fae369766e9
tag:
tip
user:
mike@mike-notebook
date:
Fri Nov 27 08:58:01 2009 +0300
summary: Файл readme.txt добавлен в репозиторий
Changeset — это и есть номер ревизии, который состоит из двух частей:
виртуального номера ревизии (записан до «:») и идентификатора (sha1-хеша).
Виртуальный номер ревизии призван облегчить жизнь пользователям, и всетаки ввести в эту систему некоторую нумерацию ревизий. Но, как показывает
практика использовать этот номер для однозначной идентификации нельзя, так
как может привести к путанице в понимании происходящего в репозиториях.
Обычно для однозначной идентификации версии достаточно 4-5
шестнадцатеричных цифр идентификатора. Следующей строкой идёт «tag: tip»,
вообще говоря tip — это обозначение последней ревизии, хотя выбирается это
обозначение в различных случаях по различным принципам, в дальнейшем, при
рассмотрении организаций ветвлений этот момент исследуем более подробно.
Значение следующих строк очевидно, и нет необходимости их как-либо
комментировать.
Выполнение команды commit локально, то есть история изменений
сохранены только в данном локальном репозитории. Для того, чтобы передать
изменения в репозиторий расположенный в ~/repos/hgproject следует
выполнить:
> hg push
pushing to ~/repos/hgproject
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files
После выполнения этой команды все изменения, зафиксированные в
локальном репозитории, были зафиксированы также и в удаленном.
30
Теперь следует склонировать репозиторий ещё раз, и посмотреть как
происходит обмен ревизиями в Mercurial. Для этого необходимо создать новый
каталог ~/projects/hgproj_clone, и склонировать в него наш удаленный
репозиторий:
>hg clone ~/repos/hgproject ~/projects/hgproj_clone
updating working directory
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
И уже во вновь склонированном репозитории создадим файл other.txt с
помощью текстового редактора. И снова повторим операции описанные выше:
> hg status
? other.txt
> hg add
adding other.txt
> hg commit
> hg log
changeset: 1:270e49e72f4b
tag:
tip
user:
mike@mike-notebook
date:
Fri Nov 27 10:39:35 2009 +0300
summary: Записан файл other.txt в другом репозитории
changeset: 0:8fae369766e9
user:
mike@mike-notebook
date:
Fri Nov 27 08:58:01 2009 +0300
summary: Файл readme.txt добавлен в репозиторий
Видим, что в новом репозитории отражены как изменения, сделанные
локально, так и изменения, сделанные в удаленном репозитории, которые мы
ранее отправляли командой push. Теперь необходимо воспользоваться еще
одной командой:
> hg outgoing
comparing with ~/repos/hgproject
searching for changes
changeset: 1:270e49e72f4b
tag:
tip
user:
mike@mike-notebook
date:
Fri Nov 27 10:39:35 2009 +0300
summary: Записан файл other.txt в другом репозитории
По команде hg outgoing Mercurial выводит список ревизий, которые есть в
вашем локальном репозитории, но которых нет в «центральном». Отправить
31
появившиеся ревизии в «центральный» репозиторий можно рассмотренным
ранее способом:
> hg push
pushing to ~/repos/hgproject
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files
Таким образом, в «центральном репозитории две ревизии. Теперь
рассмотрим, как следует забирать обновления из центрального репозитория.
Для этого перейдём в каталог с первым клоном, то есть в ~/projects/hgproject, и
выполним:
> hg incoming
comparing with ~/repos/hgproject
searching for changes
changeset: 1:270e49e72f4b
tag:
tip
user:
mike@mike-notebook
date:
Fri Nov 27 10:39:35 2009 +0300
summary: Записан файл other.txt в другом репозитории
Команда hg incoming выдает список ревизий, которые есть в удаленном
репозитории, но отсутствуют в локальном. А затем можно получить эти
ревизии, для чего надо выполнить:
> hg pull
pulling from ~/repos/hgproject
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files
(run 'hg update' to get a working copy)
Команда hg pull получает ревизии из удаленного репозитория, и добавляет
их в локальный, таким образом, изменения из «центрального» репозитория
были перемещены в локальный репозиторий. Но они остались только в
репозитории, локальная копия осталась нетронутой. Для того, чтобы обновить
локальную копию выполним:
> hg update
32
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
Если посмотреть на состояние рабочей копии, то она соответствует
состоянию рабочей копии в репозитории ~/projects/hgproj_clone, а состояние
хранилища во всех трех репозиториях одинаково.
Рисунок 2.3 — Основные команды работы в Mercurial
2.3Работа с ветвями и слияниями в Mercurial
В этом разделе будут рассмотрены сложные операции с репозиториями, а
именно — создание ветвей и работа с ними, а также сопутствующие вопросы
Первым ветвлением, с которым столкнется команда, работающая с
Mercurial — это ветвление при помещении в центральный репозиторий новых
изменений. Ситуация возникает когда в локальном репозитории имеются
изменения подтвержденные командой commit, но не отправленные в
центральные репозиторий командой, и в тоже время один (а то и несколько)
коллег поместили в "центральный" репозиторий свои изменения. Далее
поясним ситуацию на описанных в предыдущем разделе трех репозиториях.
Итак, в обоих репозиториях сохранено по две ревизии, при этом оба
репозитория были синхронизированы с "центральным". Внесем в репозитории
различные изменения и рассмотрим, что из этого выйдет.
Для начала необходимо создать файл first.txt в первом репозитории,
подтвердим добавление его и отправим изменения в центральный репозиторий:
> echo "new text to first.txt" > first.txt
> hg status
? first.txt
> hg add first.txt
> hg commit
> hg outgoing
comparing with /home/mike/Repositories/newProject
33
searching for changes
changeset: 2:66c5686e355e
tag:
tip
user:
mike@mike-vbox
date:
Thu Jan 07 22:28:39 2010 +0300
summary: Коммит файла first.txt в первом репозитории
> hg push
pushing to /home/mike/Repositories/newProject
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files
А теперь с эмулируем ситуацию, когда коллега также внес изменения,
отличающиеся от рассмотренных выше, и посмотрим как такая ситуация
решается средствами Mercurial, ведь подобная ситуация в случае командной
разработки будет достаточно частой. Для этого переместимся в имеющийся у
нас второй репозиторий, создадим в нем новый файл, и посмотрим что будет:
> echo "file created in second repository" > second.txt
> hg status
? second.txt
> hg add
adding second.txt
> hg commit
> hg log
changeset: 2:6872fa960507
tag:
tip
user:
mike@mike-vbox
date:
Sun Jan 10 19:40:45 2010 +0300
summary: Файл second.txt создан во втором репозитории
changeset: 1:270e49e72f4b
user:
mike@mike-notebook
date:
Fri Nov 27 10:39:35 2009 +0300
summary: Записан файл other.txt в другом репозитории
changeset: 0:8fae369766e9
user:
mike@mike-notebook
date:
Fri Nov 27 08:58:01 2009 +0300
summary: Файл readme.txt добавлен в репозиторий
Итак, уже имеется ситуация, когда в локальном репозитории и в
удаленном отличаются "головы" разработки, то есть существуют две различные
34
ревизии, производные от одной. В терминах любой системы контроля версий
— это ветвление, пусть пока неявное, но скоро станет таковым. Попробуем
отправить имеющиеся ревизии в "центральный" репозиторий:
> hg outgoing
comparing with /home/mike/Repositories/newProject
searching for changes
changeset: 2:6872fa960507
tag:
tip
user:
mike@mike-vbox
date:
Sun Jan 10 19:40:45 2010 +0300
summary: Файл second.txt создан во втором репозитории
> hg push
pushing to /home/mike/Repositories/newProject
searching for changes
abort: push creates new remote heads!
(did you forget to merge? use push -f to force)
Итак, Mercurial нам запрещает помещать изменения в центральные
репозиторий, сообщая, что команда push приведет к созданию новой головы в
удаленном репозитории. И предлагает произвести слияние репозиториев.
Сделаем это:
> hg incoming
comparing with /home/mike/Repositories/newProject
searching for changes
changeset: 2:66c5686e355e
tag:
tip
user:
mike@mike-vbox
date:
Thu Jan 07 22:28:39 2010 +0300
summary: Коммит файла first.txt в первом репозитории
> hg pull
pulling from /home/mike/Repositories/newProject
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files (+1 heads)
(run 'hg heads' to see heads, 'hg merge' to merge)
Вытянув с "центрального" репозитория все имеющиеся изменения,
Mercuial сообщает, что в локальном репозитории теперь две "головы" которые
требуют слияния. Можно даже попросить Mercurial показать некоторую
картинку (используется дополнение graphlog, расширение есть в стандартной
поставке):
35
> hg glog
o changeset: 3:66c5686e355e
| tag:
tip
| parent: 1:270e49e72f4b
| user:
mike@mike-vbox
| date:
Thu Jan 07 22:28:39 2010 +0300
| summary: Коммит файла first.txt в первом репозитории
|
| @ changeset: 2:6872fa960507
|/ user:
mike@mike-vbox
| date:
Sun Jan 10 19:40:45 2010 +0300
| summary: Файл second.txt создан во втором репозитории
|
o changeset: 1:270e49e72f4b
| user:
mike@mike-notebook
| date:
Fri Nov 27 10:39:35 2009 +0300
| summary: Записан файл other.txt в другом репозитории
|
o changeset: 0:8fae369766e9
user:
mike@mike-notebook
date:
Fri Nov 27 08:58:01 2009 +0300
summary: Файл readme.txt добавлен в репозиторий
Поскольку пока не планировалось целенаправленно создавать две ветви
разработки, необходимо выполнить слияние имеющихся ветвей:
> hg merge
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
(branch merge, don't forget to commit)
Итак, Mercurial, после команды hg merge произвел слияние рабочей копии
и репозитория, и напоминает программисту, что эти изменения следовало бы
подтвердить командой commit.
Теперь необходимо отправить изменения в "центральный" репозиторий, и
посмотреть, что же делать теперь с ними первому разработчику.
> hg push
pushing to /home/mike/Repositories/newProject
searching for changes
adding changesets
adding manifests
adding file changes
added 2 changesets with 1 changes to 1 files
Теперь переместимся в каталог первого разработчика, и получим
изменения и из центрального репозитория:
> hg pull
36
pulling from /home/mike/Repositories/newProject
searching for changes
adding changesets
adding manifests
adding file changes
added 2 changesets with 1 changes to 2 files
(run 'hg update' to get a working copy)
> hg update
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
В трех репозиториях получена идентичная ситуация, несмотря на
несколько более сложную исходную.
2.4 Основы организации ветвей
Для начала следует дать определения ветви, чтобы начинать с единого
понимания процесса. Ветвь (branch) — это связанная последовательность
ревизий (changeset) являющаяся отдельным направлением разработки. Таким
образом, ветвь — это в первую очередь логическое понятие, так как в случае с
распределенными системами контроля версий она будет содержать
значительное число "спонтанных" ветвлений-слияний.
Рисунок 2.4 — Исходное состояние репозитория
В этом разделе продолжается работа над примером, описанном ранее. На
рисунке 2.4 показано состояние репозитория после операций совершенных с
ним в предыдущих разделах. И хотя формально в репозитории уже имеется
одно ветвление, Mercurial говорит что ветвь одна:
$ hg branches
default
4:6d6c634e2e20
Команда hg branches выводит список всех именованных ветвей в
репозитории. Как видно основная ветвь разработки называется default. Если
быть точным, так называется ветвь, в которую происходит первое
подтверждение изменений в репозиторий, так сказать название по умолчанию.
37
На рисунке 2.4 приведено текущее состояние репозитория и граф ревизий в нем
находящихся. Красным кружком отмечена "вершину" (tip) репозитория, как
сказано в документации Mecurial, вершина — это самая свежая ревизия в
репозитории. Команда hg branches не выводит анонимные ветви, хотя
разработчики могут их использовать при необходимости, и создавать
самостоятельно.
В Mercurial предусмотрен способ создания ветвей разработки с
некоторыми именами, задаваемыми пользователем. Для организации подобных
ветвлений предназначена команда hg branch. С помощью этой команды версия,
находящаяся в локальной копии помечается ветвью с новым именем, при этом
сама ветвь будет создана только после того, как будет выполнена команда
commit. Далее реализуем именованную ветвь, родительской ревизией для
которой будет ff8f:
> hg branch new_feature
marked working directory as branch new_feature
> hg commit
> hg branches
new_feature
6:4d530267d302
default
5:ff8ffd5270cb
Итак, как видно из результатов приведенных выше, Mercurial уже знает
про две именованные ветви, "вершинами" для которых являются ревизии ff8f и
4d53, хотя на графе ревизий это одна ветвь. На рисунке 2.5 показано, что
именно понимается под именованной ветвью в Mercurial, при этом, фактически,
для каждой ветви есть своя "вершина" (tip), хотя hg log это не показывает.
Рисунок 2.5 — Именованные ветви в репозитории
Убедиться в том, что "вершины" все таки существуют можно с
помощью hg update, то есть переключившись на другую ветвь:
38
> hg update default
0 files updated, 0 files merged, 0 files removed, 0 files unresolved
> hg ident
ff8ffd5270cb
А затем переключится обратно:
$ hg update new_feature
0 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ hg ident
4d530267d302 (new_feature) tip
При этом в репозитории сложилась интересная ситуация. tip ветви default
не совпадает с "головой" (head) этой же ветви. В этом легко убедиться,
попросив Mercurial сказать какие же "головы" есть в репозитории:
$ hg heads
changeset: 6:4d530267d302
branch: new_feature
tag:
tip
user:
mike@mike-vbox
date:
Sun Jan 31 21:31:07 2010 +0300
summary: Создание именованной ветви в репозитории
changeset: 4:6d6c634e2e20
parent: 3:6872fa960507
parent: 2:66c5686e355e
user:
mike@mike-vbox
date:
Sun Jan 10 20:34:21 2010 +0300
summary: Выполнено слияние двух веток
Следует отметить один очень важный факт — ветвление произведено в
локальном репозитории, и разработчик может работать с ним так, как ему
угодно, при этом боясь поломать чужой код своими изменениями, или вызвать
у коллег проблемы своими ветвлениями. Вот именно так концепция
распределенной системы контроля версий позволяет решить стандартные
болячки централизованных систем.
3. Порядок выполнения работы
3.1. Разработать модель командной работы согласно варианту, полученному
у преподавателя.
3.2. Создать необходимое
количество
репозиториев,
разработать
соглашение по предназначению репозиториев.
3.3. Создать изменения в одном локальном репозитории, сохранить их в
39
удаленном.
3.4. Получить набор изменений из удаленного репозитория в репозиторий
отличный от описанного в п.3.3, внести дополнительные изменения и сохранить
их в удаленном репозитории.
3.5. Внести одновременно разные изменения в локальные репозитории
сохранить их все в удаленном, продемонстрировать процесс слияния.
3.6. Продемонстрировать создание именованных веток в локальном
репозитории.
3.7. Проанализировать результаты работы, сделать выводы.
4. Содержание отчета
4.1. Цель работы.
4.2. Постановка задачи, описание реализуемой модели работы команды
разработчиков.
4.3. Команды, реализующие поставленную задачу, и результаты их работы.
4.4. Описание изменений в локальном и удаленном репозиториях на
различных этапах работы.
4.5. Выводы по работе.
5. Контрольные вопросы
5.1. Расскажите о назначении систем контроля версий.
5.2. Опишите основные различия между централизованными и
распределенными системами контроля версий.
5.3. Поясните понятие ревизии.
5.4. Назовите основные команды для работы с локальным репозиторием в
Mercurial.
5.5. Опишите алгоритм и команды его реализующие для безопасного
обмена ревизиями с удаленным репозиторием.
5.6. Объясните понятие ветви в распределенной системе контроля версий
5.7. Какие основные команды для работы с ветвями есть в Mercurial.
40
Лабораторная работа №4
Исследование способов модульного тестирования программного
обеспечения
1. Цель работы
Исследовать основные подходы к модульному тестированию программного
обеспечения. Приобрести практические навыки составления модульных тестов
для объектно-ориентированных программ.
2. Основные положения
2.1. Общий порядок тестирования программного обеспечения
Цель тестирования программных модулей состоит в том, чтобы
удостовериться, что каждый модуль соответствует своей спецификации. В
процедурно-ориентированном
программировании
модулем
называется
процедура или функция, иногда группа процедур. Тестирование модулей
обычно представляет собой некоторое сочетание проверок и прогонов тестовых
случаев. Можно составить план тестирования модуля, в котором учесть
тестовые случаи и построение тестового драйвера.
Тестирование классов аналогично тестированию модулей. Основным
элементом объектно-ориентированной программы является класс. Тестирование
класса предполагает его проверку на точное соответствие своей спецификации.
Существует два основных подхода к тестированию классов: просмотр
(review) программного кода и тестовые прогоны.
Просмотр исходного кода ПО производится с целью обнаружения ошибок
и дефектов, возможно, до того, как это ПО заработает. Просмотр кода
предназначен для выявления таких ошибок, как неспособность выполнять то
или иное требование спецификации или ее неправильное понимание, а также
алгоритмических ошибок в реализации.
Тестовый прогон обеспечивает тестирование ПО в процессе выполнения
программы. Осуществляя прогон программы, тестировщик стремится
определить, способна ли программа вести себя в соответствии со
спецификацией. Тестировщик должен выбрать наборы входных данных,
определить соответствующие им правильные наборы выходных данных и
сопоставить их с реально получаемыми выходными данными.
2.2. Порядок тестирования классов
Рассмотрим тестирование классов в режиме прогона тестовых случаев.
После идентификации тестовых случаев для класса нужно реализовать
тестовый драйвер, обеспечивающий прогон каждого тестового случая, и
запротоколировать результаты каждого прогона. При тестировании классов
41
тестовый драйвер создает один или большее число экземпляров тестируемого
класса и осуществляет прогон тестовых случаев. Тестовый драйвер может быть
реализован как автономный тестирующий класс.
Тестирование классов выполняют, как правило, их разработчики. В этом
случае время на изучение спецификации и реализации сводится к минимуму.
Недостатком подхода является то, что если разработчик неправильно понял
спецификации, то он для своей неправильной реализации разработает и
"ошибочные" тестовые наборы.
В результате тестирования необходимо удостовериться, что программный
код класса в точности отвечает требованиям, сформулированным в его
спецификации, и что он не делает ничего более.
План тестирования или хотя бы тестовые случаи должны разрабатываться
после составления полной спецификации класса. Разработка тестовых случаев
по мере реализации класса помогает разработчику лучше понять
спецификацию. Тестирование класса должно проводиться до того, как
возникнет необходимость использовать этот класс в других компонентах ПО.
Регрессионное тестирование класса должно выполняться всякий раз, когда
меняется реализация класса. Регрессионное тестирование позволяет убедиться
в том, что разработанные и оттестированные функции продолжают
удовлетворять спецификации после выполнения модификации ПО.
В модульном тестировании участвуют компоненты трех типов:
1) Модуль (unit) – наименьший компонент, который можно скомпилировать.
2) Драйверы тестов, которые запускают тестируемый элемент.
3) Программные заглушки – заменяют недостающие компоненты, которые
вызываются элементом и выполняют следующие действия:
- возвращаются к элементу, не выполняя никаких других действий;
- отображают трассировочное сообщение и иногда предлагают
тестировщику продолжить тестирование;
- возвращают постоянное значение или предлагают тестировщику самому
ввести возвращаемое значение;
- осуществляют упрощенную реализацию недостающей компоненты;
- имитируют исключительные или аварийные ситуации.
В большинстве объектно-ориентированных языков члены класса имеют
один из трех уровней доступа:
1) Public. Члены с доступом public доступны из любых классов.
2) Private. Члены с доступом private доступны только внутри самого класса,
то есть из его методов. Они являются частью внутренней реализации класса и
недоступны стороннему разработчику.
3) Protected. Члены с доступом protected доступны из самого класса и из
классов, являющихся его потомками, но недоступны извне. Использование этих
методов возможно только при создании класса-потомка, расширяющего
функциональность базового класса.
Таким образом, необходимость тестирования функциональности класса
зависит от того, предоставляется ли им возможность наследования. Если класс
является законченным (final) и не предполагает наследования, необходимо
42
тестирование его public части (впрочем, классы final не содержат protected
членов). Если же класс рассчитан на расширение за счет наследования,
необходимо тестирование также его protected части.
Кроме того, во многих языках класс может содержать статические (static)
члены, которые принадлежат классу в целом, а не его конкретным экземплярам.
При наличии public static или protected static членов, кроме тестирования
объектов класса, должно отдельно выполняться тестирование статической части
класса.
Тестирование классов обычно выполняется путем разработки тестового
драйвера, который создает экземпляры классов и окружает эти экземпляры
соответствующей средой (тестовым окружением), чтобы стал возможен прогон
соответствующего тестового случая. Драйвер посылает сообщения экземпляру
класса в соответствии со спецификацией тестового случая, а затем проверяет
исход этих сообщений. Тестовый драйвер должен удалять созданные им
экземпляры тестируемого класса. Статические элементы данных класса также
необходимо тестировать.
Существует несколько способов реализации тестового драйвера:
1) Тестовый драйвер реализуется в виде отдельного класса. Методы этого
класса создают объекты тестируемого класса и вызывают их методы, в том
числе статические методы класса. Таким способом можно тестировать public
часть класса.
2) Тестовый драйвер реализуется в виде класса, наследуемого от
тестируемого. В отличие от предыдущего способа, такому тестовому драйверу
доступна не только public, но и protected часть.
3) Тестовый драйвер реализуется непосредственно внутри тестируемого
класса (в класс добавляются диагностические методы). Такой тестовый драйвер
имеет доступ ко всей реализации класса, включая private члены. В этом случае в
методы класса включаются вызовы отладочных функций и агенты,
отслеживающие некоторые события при тестировании.
В качестве примера
следующего вида:
рассмотрим
тестирование
класса
TCommand
public class TCommand {
public int NameCommand;
public string GetFullName();
}
Спецификация тестового случая должна включать следующие пункты:
1) Название тестируемого класса: TCommand.
2) Название тестового случая: TCommandTest1.
3) Описание тестового случая – словесное описание логики теста с
указанием тестовых данных: Тест проверяет правильность работы метода
GetFullName() – получения полного названия команды на основе кода
команды. В тесте подаются следующие значения кодов команд (входные
значения): -1, 1, 2, 4, 6, 20, где -1 – запрещенное значение.
На основе спецификации создадим тестовый драйвер – класс
43
TCommandTester,
Tester.
наследующий
функциональность
абстрактного
класса
public class Log
{
//Создание лог файла
static private StreamWriter log=new StreamWriter("log.log");
static public void Add(string msg) //Добавление сообщения в
лог файл
{
log.WriteLine(msg);
}
static public void Close() //Закрыть лог файл
{
log.Close();
}
}
abstract class Tester
{
protected void LogMessage(string s)
//Добавление сообщения в лог-файл
{
Log.Add(s);
}
}
class TCommandTester:Tester // Тестовый драйвер
{
TCommand OUT;
public TCommandTester()
{
OUT = new TCommand();
Run();
}
private void Run()
{
TCommandTest1();
}
private void TCommandTest1()
{
int[] commands = {-1, 1, 2, 4, 6, 20};
for(int i=0;i<=5;i++)
{
OUT.NameCommand = commands[i];
LogMessage(commands[i].ToString()
+
OUT.GetFullName());
}
}
"
:
"
+
44
static void Main()
{
TCommandTester CommandTester = new TCommandTester();
Log.Close();
}
}
Класс TCommandTester содержит метод TCommandTest1(), в котором
реализована вся функциональность теста. В данном случае для покрытия
спецификации достаточно перебрать следующие значения кодов команд: -1, 1,
2, 4, 6, 20, где -1 - запрещенное значение, и получить соответствующие им
полное название команды с помощью метода GetFullName(). Пары
соответствующих значений заносятся в log-файл для последующей проверки на
соответствие спецификации.
Таким образом, для тестирования любого метода класса необходимо:
- Определить, какая часть функциональности метода должна быть
протестирована, то есть при каких условиях он должен вызываться. Под
условиями здесь понимаются параметры вызова методов, значения полей и
свойств объектов, наличие и содержимое используемых файлов и т. д.
- Создать тестовое окружение, обеспечивающее требуемые условия.
- Запустить тестовое окружение на выполнение.
- Обеспечить сохранение результатов в файл для их последующей
проверки.
- После завершения выполнения сравнить полученные результаты со
спецификацией.
3. Порядок выполнения работы
3.1. Выбрать в качестве тестируемого один из классов, спроектированных в
лабораторных работах №№ 1 – 4.
3.2. Составить спецификацию тестового случая для одного из методов
выбранного класса, как показано в разделе 2.2.
3.3. Реализовать тестируемый класс и необходимое тестовое окружение на
языке С#.
3.4. Выполнить тестирование с выводом результатов на экран и
сохранением в log-файл.
3.5. Проанализировать результаты тестирования, сделать выводы.
4. Содержание отчета
4.1. Цель работы.
4.2. Постановка задачи, включающая описание обязанностей тестируемого
45
класса.
4.3. Спецификация тестового случая.
4.4. Текст программы.
4.5. Выводы по работе.
5. Контрольные вопросы
5.1. Для чего применяется тестирование программного обеспечения?
5.2. Какие существуют разновидности тестирования?
5.3. Что такое модульное тестирование?
5.4. Что понимается под модулем при модельном тестировании?
5.5. Каков порядок мольного тестирования классов?
5.6. Какие разделы включает спецификация тестового случая?
46
Лабораторная работа № 5
Исследование способов интеграционного тестирования программного
обеспечения
1. Цель работы
Исследовать основные принципы интеграционного тестирования
программного обеспечения. Приобрести практические навыки организации
интеграционных тестов для объектно-ориентированных программ.
2. Основные положения
Основное назначение тестирования взаимодействий состоит в том, чтобы
убедиться, что происходит правильный обмен сообщениями между объектами,
классы которых уже прошли тестирование в автономном режиме (на модульном
уровне тестирования).
Тестирование взаимодействия, или интеграционное тестирование,
представляет собой тестирование собранных вместе, взаимодействующих
модулей (объектов). В интеграционном тестировании можно объединять разное
количество объектов – от двух до всех объектов тестируемой системы. При
интеграционном тестировании используется подход «белого ящика». Целью
интеграционного тестирования является только проверка правильности
взаимодействия объектов, а не проверка правильности функционирования
системы в целом.
2.1. Идентификация взаимодействий
Взаимодействие объектов представляет собой просто запрос одного
объекта (отправителя) на выполнение другим объектом (получателем) одной из
операций получателя и всех видов обработки, необходимых для завершения
этого запроса.
В ситуациях, когда в качестве основы тестирования взаимодействий
объектов выбраны только спецификации общедоступных операций,
тестирование намного проще, чем когда такой основой служит реализация. В
данной лабораторной работе мы ограничимся тестированием общедоступного
интерфейса. Такой подход вполне оправдан, поскольку мы полагаем, что классы
уже успешно прошли модульное тестирование. Тем не менее, выбор такого
подхода отнюдь не означает, что не нужно возвращаться к спецификациям
классов, дабы убедиться в том, что тот или иной метод выполнил все
необходимые вычисления. Это обусловливает необходимость проверки
значений атрибутов внутреннего состояния получателя, в том числе любых
агрегированных атрибутов, т.е. атрибутов, которые сами являются объектами.
Основное внимание уделяется отбору тестов на основе спецификации каждой
47
операции из общедоступного интерфейса класса.
Взаимодействия неявно предполагаются в спецификации класса, в которой
установлены ссылки на другие объекты. Выявить такие взаимодействующие
классы можно, используя отношения ассоциации, агрегирования и композиции,
представленные на диаграмме классов. Связи такого рода преобразуются в
интерфейсы класса, а тот или иной класс взаимодействует с другими классами
посредством одного или нескольких способов:
1) Общедоступная операция имеет один или большее число формальных
параметров объектного типа. Сообщение устанавливает ассоциацию между
получателем и параметром, которая позволяет получателю взаимодействовать с
этим параметрическим объектом.
2) Общедоступная операция возвращает значения объектного типа. На
класс может быть возложена задача создания возвращаемого объекта, либо он
может возвращать модифицированный параметр.
3) Метод одного класса создает экземпляр другого класса как часть своей
реализации.
4) Метод одного класса ссылается на глобальный экземпляр некоторого
другого класса. Разумеется, принципы хорошего тона в проектировании
рекомендуют минимальное использование глобальных объектов. Если
реализация какого-либо класса ссылается на некоторый глобальный объект,
рассматривайте его как неявный параметр в методах, которые на него
ссылаются.
2.2. Выбор тестовых случаев
Исчерпывающее тестирование, другими словами, прогон каждого
возможного тестового случая, покрывающего каждое сочетание значений – это,
вне всяких сомнений, надежный подход к тестированию. Однако во многих
ситуациях количество тестовых случаев достигает таких больших значений, что
обычными методами с ними справиться попросту невозможно. Если имеется
принципиальная возможность построения такого большого количества
тестовых случаев, на построение и выполнение которых не хватит никакого
времени, должен быть разработан систематический метод определения, какими
из тестовых случаев следует воспользоваться. Если есть выбор, то мы отдаем
предпочтение таким тестовым случаям, которые позволяют найти ошибки, в
обнаружении которых мы заинтересованы больше всего.
Существуют различные способы определения, какое подмножество из
множества всех возможных тестовых случаев следует выбирать. При любом
подходе мы заинтересованы в том, чтобы систематически повышать уровень
покрытия.
2.3. Подробное описание тестового случая
Продемонстрируем тестирование взаимодействий на примере класса
TСommandQueue:
48
public class TCommandQueue : System.Windows.Forms.ListBox
{
public TCommandQueue()
// Добавляет команду в очередь команд на указанную позицию
public void AddCommand(int NameCommand, // Код команды
int Position)
//
Позиция в очереди
// Удаляет команду из очереди
public void DeleteCommand(int Position)
// Выполняет первую команду в очереди
public void ProcessCommand()
}
Класс реализует очередь FIFO объектов типа TCommand. Наследуется от
System.Windows.Forms.ListBox библиотеки .NET. Количество команд в
очереди не ограничено.
Операции:
- Конструктор TCommandQueue().
- Операция AddCommand(...) создает объект типа TCommand, присваивает
ему переданные параметры и добавляет в очередь команд на указанную
позицию. Значение позиции в очереди = -1 означает, что команда будет
добавлена в конец очереди.
- Операция DeleteCommand(...) удаляет команду из очереди на указанной
позиции.
- Операция ProcessCommand() при наличии команд в очереди посылает
первую команду из очереди на выполнение.
С объектом TCommand осуществляется взаимодействие третьего типа, т. е.
TCommandQueue создает объекты класса TCommand как часть своей внутренней
реализации.
Для тестирования взаимодействия класса TCommandQueue и класса
TСommand, так же, как и при модульном тестировании, разработаем
спецификацию тестового случая:
1) Названия взаимодействующих классов: TСommandQueue, TCommand.
2) Название теста: TCommandQueueTest1.
3) Описание теста: тест проверяет возможность создания объекта типа
TCommand и добавления его в очередь при вызове метода AddCommand().
4) Начальные условия: очередь команд пуста.
5) Ожидаемый результат: в очередь будет добавлена одна команда.
На основе этой спецификации был разработан тестовый драйвер – класс
TCommandQueueTester, который наследуется от класса Tester. Этот класс
содержит:
- Метод Init(), в котором создается объект класса TCommandQueue. Этот
метод необходимо вызывать в начале каждого теста, чтобы тестируемые
объекты создавались вновь:
private void Init()
49
{
CommandQueue=new TCommandQueue();
}
- Методы, реализующие тесты. Каждый тест реализован в отдельном
методе.
- Метод Run(), в котором вызываются методы тестов.
- Метод dump(), который сохраняет в log-файле теста информацию обо
всех командах, находящихся в очереди в формате: «номер позиции в очереди:
полное название команды».
- Точку входа в программу – метод Main(), в котором происходит создание
экземпляра класса TCommandQueueTester и запуск метода Run().
Сначала создадим тест, который проверяет, создается ли объект типа
TСommand, и добавляется ли команда в конец очереди.
private void TCommandQueueTest1()
{
Init();
LogMessage("///////// TCommandQueue Test1 /////////////");
LogMessage("Проверяем, создается ли объект типа TCommand");
// В очереди нет команд
dump();
// Добавляем команду
// параметр = -1 означает, что команда должна быть добавлена
//в конец очереди
CommandQueue.AddCommand(1, -1);
LogMessage("Command added");
// В очереди одна команда
dump();
}
Для выполнения этого теста в методе Run() необходимо вызвать метод
TCommandQuеueTest1() и запустить программу на выполнение:
private void Run()
{
TCommandQueueTest1();
}
После завершения теста следует просмотреть текстовый журнал теста,
чтобы сравнить полученные результаты с ожидаемыми результатами,
заданными в спецификации тестового случая TCommandQueueTest1.
3. Порядок выполнения работы
3.1. Выбрать в качестве тестируемого взаимодействие двух или более
классов, спроектированных в лабораторных работах №№1 – 4.
3.2. Составить спецификацию тестового случая.
50
3.3. Реализовать тестируемые классы и необходимое тестовое окружение на
языке С#.
3.4. Выполнить тестирование с выводом результатов на экран и
сохранением в log-файл.
3.5. Проанализировать результаты тестирования, сделать выводы.
4. Содержание отчета
4.1. Цель работы.
4.2. Постановка
задачи,
включающая
тестируемых классов.
4.3. Спецификация тестового случая.
4.4. Текст программы.
4.5. Выводы по работе.
описание
взаимодействия
5. Контрольные вопросы
5.1. Для чего применяется интеграционное тестирование программного
обеспечения?
5.2. Какие существуют типы взаимодействий объектов?
5.3. Исходя из каких соображений выполняется выбор тестовых случаев
при интеграционном тестировании?
5.4. Какие разделы включает спецификация тестового случая для
интеграционного тестирования?
51
Лабораторная работа №6
Исследование способов модульного тестирования программного
обеспечения в среде NUnit
1. Цель работы
Исследовать эффективность использования методологии TDD при
разработке программного обеспечения. Получить практические навыки
использования фреймворка NUnit для модульного тестирования программного
обеспечения.
2. Общие положения
2.1. Разработка через тестирование
Разработка через тестирование (Test-driven development, TDD) представляет
собой одну из современных «гибких» (agile) методологий разработки
программного обеспечения. Эта методология предполагает использование
модульного тестирования для контроля разрабатываемого программного кода.
Основная идея состоит в том, что модульные тесты разрабатываются до
разработки программного кода, который они будут тестировать. Разработка
таких тестов заставляет программиста подробно разобраться в требованиях к
разрабатываемому модулю до начала разработки, четко определиться с
конечными целями разработки; успешное выполнение тестов является
критерием прекращения разработки. Разработку через тестирование можно
представить в виде последовательности следующих основных этапов:
1) получение программы в непротиворечивом состоянии и набора успешно
выполняемых модульных тестов;
2) разработка нового модульного теста;
3) максимально быстрая разработка минимального программного кода,
позволяющего успешно выполнить весь набор тестов;
4) рефакторинг разработанного программного кода с целью улучшения его
структуры и устранения избыточности;
5) контроль переработанного программного кода с помощью полного
набора тестов.
В результате выполнения этих шагов в программу будет добавлена новая
функциональность, а работоспособность модифицированной программы будет
гарантироваться выполнением полного набора модульных тестов. Такой подход
позволяет избежать ситуации, когда добавление новой функции нарушает
работоспособность ранее разработанного кода.
Такой подход дает следующие преимущества:
- предотвращается возникновение ошибок во вновь разработанном коде;
- появляется возможность вносить изменения в существующий
52
программный код без риска нарушить его работоспособность, т. к.
возникающие ошибки будут сразу же обнаружены модульными тестами;
- модульные тесты могут быть использованы в качестве документации к
программному коду, показывать способы обращения к соответствующим
программным модулям (объектам, методам и т.п.);
- улучшается дизайн кода: тесты заставляют создавать более «легкие» и
независимые компоненты, которые проще поддерживать и модифицировать;
- повышается квалификация разработчиков, т. к. разработка качественных
модульных тестов требует глубоких знаний ООП и паттернов проектирования.
Таким образом, применение методологии TDD позволяет создавать более
качественный программный код, снижает вероятность возникновения ошибок и
ускоряет процесс разработки.
К недостаткам разработки через тестирование можно отнести, во-первых,
высокие требования к квалификации разработчиков. Еще одной проблемой
является тестовое покрытие программного кода. В идеале набор модульных
тестов должен обеспечивать покрытие 100% программного кода. Однако на
практике этого достичь не удается, особенно в тех случаях, когда TDD
применяется для модификации уже существующего программного обеспечения.
Поэтому необходимо обеспечить покрытие тестами наиболее критических
модулей, а также компонентов, которые будут подвергаться рефакторингу.
2.2. Тестирование с помощью NUnit
NUnit представляет собой открытый фреймворк для тестирования
приложений под Microsoft .NET Framework. NUnit включает библиотеки для
написания тестов, а также ПО для выполнения этих тестов.
Рассмотрим порядок создания и выполнения модульных тестов с помощью
NUnit.
Для примера создадим dll-библиотеку, содержащую класс, описывающий
простой калькулятор с возможностью выполнения основных арифметических
операций. Для разработки будем использовать среду Microsoft Visual Studio.
Создадим новый проект: меню File/New Project/ Class Library.
Созданный проект содержит один пустой файл Class1.cs. Переименуем его
в ICalc.cs и опишем в этом файле интерфейс ICalc, который включает
основные арифметические операции:
namespace CalcLib
{
public interface ICalc
{
double Add(double a, double b);
double Subtract(double a, double b);
double Multiply(double a, double b);
double Divide(double a, double b);
}
}
53
Теперь добавим к проекту файл Calc.cs, в котором опишем класс Calc
(пункт меню Project/Add Class или сочетание клавиш Shift + Alt + C):
using System;
namespace CalcLib
{
public class Calc : ICalc
{
public double Add(double a, double b)
{
throw new NotImplementedException();
}
public double Subtract(double a, double b)
{
throw new NotImplementedException();
}
public double Multiply(double a, double b)
{
throw new NotImplementedException();
}
public double Divide(double a, double b)
{
throw new NotImplementedException();
}
}
}
Видно, что класс Calc реализует интерфейс ICalc. Методы класса пока не
реализованы, т. к. в соответствии с методологией TDD сначала должны быть
реализованы тесты, а потом – код, который будет ими тестироваться.
Для того чтобы можно было использовать возможности NUnit при
написании тестов, необходимо добавить в проект ссылку на сборку
nunit.framework (меню Project/Add Reference):
Рисунок 2.1 – Добавление ссылки на сборку
Добавим в проект класс CalcTest, который будет содержать модульные
54
тесты для класса Calc:
using System;
using NUnit.Framework;
namespace CalcLib
{
[TestFixture]
public class CalcTest
{
public void AddTest()
{
ICalc calculator = new Calc();
double actualVal = calculator.Add(2, 2);
double expected = 2 + 2;
Assert.AreEqual(expected, actualVal);
}
}
}
Класс CalcTest имеет атрибут [TestFixture], который указывает среде
выполнения NUnit на то, что данный класс будет содержать модульные тесты.
Он должен быть объявлен с модификатором public и иметь конструктор по
умолчанию.
Каждый отдельный тест должен иметь атрибут [Test]. Метод, который
описывает тест, обязан возвращать void, быть доступным извне и не иметь
входных параметров.
В нашем случае в методе AddTest() для проверки правильности работы
метода Add() класса Calc создается экземпляр калькулятора, выполняется
операция сложения и полученный результат сравнивается с ожидаемым
(результатом выполнения одноименной операции в языке C#). Класс Assert,
использованный в этом примере, содержит набор методов, позволяющих
выполнять различные проверки данных. Если данные неверны, метод
оповещает среду выполнения NUnit, что тест не пройден. Ниже приведены
некоторые проверки, доступные в классе Assert:
1.
Assert.AreEqual – проверяет равенство входных параметров;
2.
Assert.AreNotEqual – проверяет то, что входные параметры
неравны;
3.
Assert.AreSame – проверка на то, что входные параметры
ссылаются на один и тот же объект;
4.
Assert.AreNotSame – входные параметры не ссылаются на один и
тот же объект;
5.
Assert.Contains – метод получает на входе объект и коллекцию и
проверяет, что данный объект содержится в это коллекции;
6.
Assert.IsNull – входной параметр – null;
7.
Assert.IsEmpty – входной параметр – пустая коллекция.
8.
Assert.Fail – прерывает выполнение теста и среде NUnit, что тест
не пройден. Эта функция может быть использована, когда нужно организовать
более сложные типы проверок.
55
Добавим в класс CalcTest модульные тесты для остальных методов класса
Calc:
[Test]
public void SubtractTest()
{
ICalc calculator = new Calc();
double actual = calculator.Subtract(2, 2);
double expected = 2 - 2;
Assert.AreEqual(expected, actual);
}
[Test]
public void MultiplyTest()
{
ICalc calculator = new Calc();
double actual = calculator.Multiply(2, 2);
double expected = 2 * 2;
Assert.AreEqual(expected, actual);
}
[Test]
public void DivideTest()
{
ICalc calculator = new Calc();
double actual = calculator.Divide(2, 2);
double expected = 2 / 2;
Assert.AreEqual(expected, actual);
}
После того как написаны модульные тесты и проект скомпилирован (меню
Build/Build Solution или F6), можно переходить к тестированию.
Для запуска тестов используется среда NUnit. После запуска среды NUnit
необходимо загрузить тестируемый проект (File/Open Project или сочетание
клавиш Ctrl + O). В нашем случае это будет библиотека CalcLib.dll.
В левой части программного окна отображается древовидная структура,
включающая проект (CalcLib), наборы тестов TextFicture (CalcTest) и
собственно отдельные модульные тесты (AddTest, DivideTest, MultiplyTest,
SubtractTest). После выполнения тестов в правой части окна отображаются
результаты тестирования. В том случае, если тесты выполнились неудачно, есть
возможность просмотреть сообщения об ошибке и исходный код неудачного
теста и тестируемого им модуля.
56
Рисунок 2.2 – Выполнение тестов в NUnit
При написании тестов можно использовать еще ряд атрибутов. Атрибут
[SetUp] помечает метод, который будет выполнен перед исполнением каждого
теста из набора. Например, следующий метод позволяет вывести время начала
каждого теста:
[SetUp]
public void TestSetup()
{
Trace.WriteLine("Test started at " + DateTime.Now);
}
(Для использования класса Trace необходимо добавить директиву using
System.Diagnostics;)
Метод, помеченный атрибутом [TestFixtureSetUp], выполняется один
раз, перед запуском всего набора тестов:
[TestFixtureSetUp]
public void TestKitSetup()
{
Trace.WriteLine("Initialized at " + DateTime.Now);
}
Аналогично, атрибуты [TearDown] и [TestFixtureTearDown] помечают
методы, выполняющиеся после каждого теста и после всего набора тестов
соответственно. Например:
[TearDown]
public void TestDispose()
{
57
Trace.WriteLine("Test finished at " + DateTime.Now);
}
[TestFixtureTearDown]
public void TestKitDispose()
{
Trace.WriteLine("Completed at " + DateTime.Now);
}
Если тест помечен атрибутом [Ignore], то он игнорируется.
3. Порядок выполнения работы
3.1. Реализовать на языке C# один из классов, спроектированных в
лабораторной работе № 1. Методы класса при этом не реализовывать.
3.2. Разработать для созданного класса набор модульных тестов,
включающий тесты для каждого метода.
3.3. Запустить набор тестов, проанализировать и сохранить результаты.
3.4. Поочередно реализовать методы класса, выполняя тестирование при
каждом изменении программного кода.
3.5. После того, как весь набор тестов будет выполняться успешно,
реализацию классов можно считать завершенной.
4. Содержание отчета
4.1. Цель работы.
4.2. Постановка задачи.
4.3. Описание обязанностей тестируемого класса.
4.4. Программный код тестируемого класса и тестов.
4.5. Результаты тестирования, полученные в процессе разработки.
4.5. Выводы по работе.
5. Контрольные вопросы
5.1. В чем заключаются основные принципы методологии TDD (Разработка
через тестирование)?
5.2. Какие можно выделить достоинства и недостатки подхода TDD?
5.3. В чем состоит общий порядок модульного тестирования с помощью
NUnit.
5.4. Каково назначение основных атрибутов NUnit?
5.5. Какие типы проверок реализованы в классе Assert фреймворка NUnit?
58
Лабораторная работа № 7
Исследование способов профилирования программного обеспечения
1. Цель работы
Исследовать критические по времени выполнения участки программного
кода и возможности их устранения. Приобрести практические навыки анализа
программ с помощью профайлера EQATECProfiler.
2. Основные положения
2.1. Профилирование программного обеспечения
После разработки программного кода, удовлетворяющего спецификации и
модульным тестам, возникает задача оптимизации программы по таким
параметрам, как производительность, расход памяти и т. п. Процесс анализа
характеристик системы называется профилированием и выполняется с
помощью специальных программных инструментов – так называемых
профилировщиков,
или
профайлеров (profiler).
Программа-профайлер
выполняет запуск программы и анализирует характеристики выполнения.
Процесс профилирования сводится к декомпозиции программы на отдельные
выполняющиеся элементы и измерению времени, затрачиваемого на
выполнение каждого элемента, и расходования памяти. В результате
профилирования может быть построен граф вызовов, показывающий
последовательность вызовов функций, а также определено время выполнения
каждой функции и ее вклад в общее время выполнения программы. Анализ этой
информации позволяет выявить критические участки программного кода (хотспоты, hot-spot), т. е. «узкие места», на которые следует в первую очередь
обратить внимание при оптимизации программы.
2.2. Порядок работы с профайлером EQATECProfiler
EQATECProfiler представляет собой свободно распространяемый
профайлер для приложений под .NET. Данный инструмент позволяет выполнять
анализ производительности программы, строить граф вызовов методов
(method-call graph), анализировать время выполнения программы и
относительный вклад времени выполнения вызываемых функций.
Рассмотрим работу профайлера на следующем примере. Разработаем
программу, вычисляющую площадь и периметр прямоугольника с заданными
сторонами. Для этого воспользуемся библиотекой CalcLib, разработанной в
предыдущей работе.
На базе класса Calc реализуем класс Rect, описывающий прямоугольник
и предоставляющий функциональность для вычисления его площади и
59
периметра. Для этого добавим в проект новый класс (пункт меню Project/Add
Class), назовем файл Rect.cs и поместим в него следующее описание класса:
using System;
using System.Text;
namespace CalcLib
{
class Rect
{
private double rectWidth;
private double rectHeight;
public double Width
{
get { return rectWidth; }
set { rectWidth = value; }
}
public double Height
{
get { return rectHeight; }
set { rectHeight = value; }
}
public Rect(double aWidth, double aHeight)
{
rectWidth = aWidth;
rectHeight = aHeight;
}
public double getArea()
{
ICalc calc = new Calc();
double areaValue = calc.Multiply(rectWidth,
rectHeight);
return areaValue;
}
public double getPerimeter()
{
ICalc calc = new Calc();
double perimeterValue =
calc.Add(calc.Multiply(rectWidth, 2), calc.Multiply(rectHeight,
2));
return perimeterValue;
}
}
}
Рассмотрим текст класса подробней. Класс содержит два приватных поля
rectWidth и rectHeight. Принцип инкапсуляции предполагает, что к
60
внутренним переменным объекта нельзя получить доступ непосредственно.
Единственный способ изменить состояние объекта – это использовать его
интерфейс. Поэтому для изменения и получения значений полей класса созданы
свойства (property):
public double Width
{
get { return rectWidth; }
set { rectWidth = value; }
}
public double Height
{
get { return rectHeight; }
set { rectHeight = value; }
}
Свойства позволяют ужесточить контроль доступа к внутреннему состоянию
объекта, организовать проверку корректности присваиваемых данных, отложить
вычисление значений полей до тех пор, пока они не будут запрошены клиентом
и т. п. Для клиента же свойство выглядит как обычное поле со свободным
доступом.
После того, как реализованы необходимые классы, перейдем к реализации
приложения. Создадим класс MainClass, который будет содержать статический
метод Main(). Этот метод будем использовать в качестве входной точки (entry
point) приложения, т.е. функции, которая запускается первой при запуске
программы (аналогично функции main() в языке С++):
using System;
using System.Collections.Generic;
using System.Text;
namespace CalcLib
{
class MainClass
{
public static void Main()
{
Rect rect = new Rect(5, 8);
double rectArea = rect.getArea();
double rectPerimeter = rect.getPerimeter();
Console.WriteLine("Rectangle " + rect.Width + " x " +
rect.Height + " has area " + rectArea + " and perimeter " +
rectPerimeter);
}
}
}
Преобразуем библиотеку CalcLib в запускаемое приложение. Для этого
61
изменим свойства проекта (пункт меню Project/ CalcLib Properties): свойство
Outout Type установим в «Console Application» и в качествe Startup Object
укажем CalcLib.MainClass.
Рисунок 2.1 – Задание параметров проекта
После сохранения настроек приложение можно скомпилировать (клавиша F6) и
запустить (клавиша F5).
Для наглядности профилирования изменим методы класса Calc,
искусственно добавив в них задержку:
using System;
using System.Threading;
namespace CalcLib
{
public class Calc : ICalc
{
public double Add(double a, double b)
{
Thread.Sleep(50);
return a + b;
}
public double Subtract(double a, double b)
{
Thread.Sleep(50);
return a - b;
}
public double Multiply(double a, double b)
{
Thread.Sleep(100);
return a * b;
}
public double Divide(double a, double b)
{
Thread.Sleep(100);
return a / b;
62
}
}
}
Перейдем к собственно профилированию разработанной программы. Во
вкладке Build программы EQATECProfiler необходимо выбрать папку, в которой
расположена скомпилированная программа (.exe-файл). После загрузки следует
выбрать те модули, которые будем профилировать.
Рисунок 2.2 – Выбор программы для профилирования
Затем нажимаем кнопку «Build». В результате профайлер добавит в нашу
программу дополнительные служебные инструкции. После этого нажатие
кнопки «Run app» запускает процесс профилирования. По его окончании
создается отчет. Список отчетов показан во вкладке «Run». Выбрав нужный
(последний по порядку) отчет, переходим на вкладку «View». Здесь в таблице
представлены параметры выполнения функций программы. Ниже построен
граф вызовов для выбранной функции (на рисунке 2.3 – для метода
MainClass.Main()). В вершинах графа, соответствующих каждой из
вызываемых функций, показано время выполнения в миллисекундах, а также
относительный вклад (в процентах) этой функции в общее время выполнения
вызывающей функции. Анализ этих данных позволяет выявить «узкие места»
программы. Например, в нашем случае видно, что наибольшее время
выполнения имеет метод Rect.getPerimeter(), а в этом методе, в свою
очередь, узким местом является метод Calc.Multiply().
63
Рисунок 2.3 – Просмотр результатов профилирования
С целью оптимизации времени выполнения программы перепишем метод
Rect.getPerimeter(), заменив в нем операции умножения операциями
сложения:
public double getPerimeter()
{
ICalc calc = new Calc();
double perimeterValue = calc.Add(calc.Add(rectWidth,
rectWidth), calc.Add(rectHeight, rectHeight));
return perimeterValue;
}
Скомпилировав приложение, повторив в EQATECProfiler операции «Build»,
«Run app» и просматривая результаты, видим, что такая модификация кода
привела к сокращению общего времени выполнения программы. При этом
уменьшился вклад функции Rect.getPerimeter() в общее время выполнения.
Для сравнения профилей программы до и после оптимизации выбираем на
вкладке Run два отчета и нажимаем кнопку «Compare». В результате строится
таблица, в которой приводятся как абсолютные показатели старого и нового
варианта программы (время выполнения Old Avg и New Avg, количество
вызовов Old Calls и New Calls), так и изменения нового варианта по сравнения
со старым (относительное изменение времени выполнения функций Speedup,
64
абсолютное изменение времени выполнения Diff Avg, изменение количества
вызовов функций Diff Calls).
Рисунок 2.4 – Сравнение профилей программы
3. Порядок выполнения работы
3.1. Разработать программу на основе библиотеки классов, реализованной
и протестированной в предыдущей работе. Программа должна как можно более
полно использовать функциональность класса. При необходимости для
наглядности профилирования в методы класса следует искусственно внести
задержку выполнения.
3.2. Выполнить профилирование разработанной программы, выявить
функции, на выполнение которых тратится наибольшее время.
3.3. Модифицировать программу с целью оптимизации времени
выполнения.
3.4. Выполнить повторное профилирование программы, сравнить новые
результаты и полученные ранее, сделать выводы.
4. Содержание отчета
4.1. Цель работы.
4.2. Постановка задачи.
4.3. Текст первоначального варианта программы.
4.4. Результаты профилирования первоначального варианта программы с
подробным анализом «узких мест».
4.5. Текст модифицированного варианта программы со словесным
65
описанием и обоснованием внесенных изменений.
4.6. Результаты профилирования модифицированного варианта программы,
их сравнение с результатами профилирования первоначального варианта
программы.
4.7. Выводы по работе.
5. Контрольные вопросы
5.1. В чем заключается процесс профилирования программного
обеспечения?
5.2. Какие разновидности «узких мест» программы определяются с
помощью профилирования?
5.3. Что такое граф вызовов программы, для чего он строится?
5.4. В чем заключается общий порядок профилирования программ с
помощью EQATECProfiler?
66
Библиографический список
1. Котляров В. П. Основы тестирования программного обеспечения:
Учебное пособие / В. П. Котляров, Т. В. Коликова.– М.: Интернет-Университет
Информационных технологий; БИНОМ. Лаборатория знаний, 2006.– 285 с.
2. Рамбо Дж.UML 2.0. Объектно-ориентированное моделирование и
разработка / Дж. Рамбо, М. Блаха.– М. и др. : «Питер», 2007.– 544 с.
3. Гамма Э. Приемы
объектно-ориентированного
проектирования.
Паттерны проектирования / Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес.–
СПб.: «Питер», 2009.– 366 с.
Заказ №______ от « » _________ 2013 г. Тираж ________ экз.
Изд-во СевНТУ
Download