В процессе этого они пользуются поддержкой друг друга, а также программистов, которые входят в состав команды уже давно.
Если мы признаем, что риск является основной проблемой, которую требуется решить, то в каком месте следует искать решение этой проблемы? Перед нами стоит задача сформировать стиль разработки программного обеспечения, который позволил бы существенно снизить все перечисленные риски. Кроме того, мы должны как можно понятнее объяснить эту дисциплину программистам, менеджерам и заказчикам. Также мы должны сформировать набор рекомендаций, позволяющих адаптировать данную дисциплину для конкретных локальных условий (другими словами – определить, что в этой дисциплине является незыблемым, а что можно варьировать в зависимости от конкретных обстоятельств).
Именно об этом пойдет речь в первой и второй частях книги. Шаг за шагом мы тщательно изучим каждый из аспектов проблемы, сформируем круг вопросов, на которые нам предстоит найти ответ, а затем мы решим проблему. Мы начнем с рассмотрения базовых предположений, а затем получим решение, которое будет диктовать, каким именно образом должна осуществляться разнообразная деятельность (планирование, тестирование, разработка, дизайн и внедрение), связанная с разработкой программного продукта.
Глава 2.
Эпизод из программистской практики
День за днем программист выполняет одну и ту же последовательность действий, которую можно назвать программным проектом в миниатюре: он изучает задачу, четко связанную с возможностью продукта, в которой нуждается заказчик, затем он формирует набор необходимых тестов, реализует поставленную задачу, вылизывает код, а потом интегрирует его в систему. В каждом таком эпизоде содержатся все этапы, которые обычно составляют процесс разработки крупного программного продукта.
Для начала небольшой экскурс вперед, то есть туда, куда мы с вами движемся. Данная глава описывает живой процесс экстремального программирования так, как это происходит на практике. Речь пойдет об эпизоде разработки. Эпизод разработки – это фрагмент деятельности программиста, когда он реализует некоторую инженерную задачу (наименьший квант планирования) и интегрирует ее с остальной системой.
Я смотрю на мой набор карточек с задачами. На самой верхней из них написано: Экспорт значения квартальных издержек на момент обращения к системе. Я вспоминаю, что на сегодняшнем утреннем совещании ты сообщил присутствующим, что завершил блок вычисления значения квартальных издержек. Я обращаюсь к тебе (моему гипотетическому соратнику) с вопросом, не найдется ли у тебя времени помочь мне с разработкой блока экспорта. Ты отвечаешь: Конечно! Это одно из основных правил ХР: если к тебе обращаются с просьбой о помощи, ты обязан ответить да. Таким образом мы становимся партнерами по программированию в паре.
В течение пары минут мы обсуждаем работу, которую ты выполнил вчера. Ты рассказываешь мне о добавленном тобою в систему коде, о бинарных значениях и о том, как выглядят соответствующие тесты. Мы отодвигаем мой монитор чуть назад, так как при этом эффективность парного программирования несколько увеличивается.
Ты спрашиваешь у меня: Как выглядят тестовые случаи для этой задачи?
Я отвечаю: После выполнения экспортирующего кода значения в экспортированной записи должны соответствовать бинарным значениям.
Ты спрашиваешь: Какие поля требуется заполнить?
Я не знаю, надо спросить у Эдди.
Мы отрываем Эдди от работы на тридцать секунд, и он рассказывает нам о пяти полях, которые связаны с квартальными издержками.
Мы обращаемся к изучению структуры некоторых существующих тестовых случаев, связанных с экспортом. Обнаруживается, что один из тестовых случаев почти полностью нам подходит. Если, используя обнаруженный нами код, создать абстрактный суперкласс, на его основе мы могли бы с легкостью реализовать наш тестовый случай. Так мы выполняем переработку кода (refactoring). Потом мы запускаем существующие тесты, и все они успешно выполняются.
После этого мы обращаем внимание, что несколько других тестовых случаев можно переработать так, чтобы они были основаны на только что созданном нами абстрактном суперклассе. Однако нам необходимо решить стоящую перед нами конкретную задачу, поэтому мы просто берем пустую карточку, записываем на ней выполнить переработку тестовых случаев с использованием класса AbstractExportTest и размещаем эту карточку в наборе задач, которые нам предстоит выполнить.
После этого мы разрабатываем наш тестовый случай. Благодаря тому, что перед этим мы разработали суперкласс, создание нового тестового случая не представляет труда. Мы выполняем эту работу в течение нескольких минут. Где-то в середине работы я говорю: Я уже представляю себе, как мы можем это реализовать. Мы можем...
Однако ты прерываешь меня: Давай сначала завершим работу над тестовым случаем. Пока мы пишем тестовый случай, в голову приходят идеи для трех вариаций. Ты записываешь их на очередной карточке.
Мы завершаем работу над тестовым случаем и запускаем его. Он не срабатывает. Естественно, мы ведь еще ничего не реализовали. Подожди минутку, – говоришь ты, – вчера, когда мы с Ральфом работали над блоком вычислений, мы написали пять тестовых случаев, о которых мы думали, что они не сработали. Все, за исключением одного, сработали с первого раза.
Мы запускаем отладчик и приступаем к изучению работы тестового случая. Мы анализируем объекты, с которыми имеет дело наш тестовый случай.
Я пишу код (или это делаешь ты – все зависит от того, у кого из нас возникнет более удачная идея). Пока мы работаем над реализацией, мы замечаем еще пару дополнительных тестовых случаев, которые неплохо было бы написать. Мы заносим эту информацию на карточку. После того как код реализован, ранее разработанный тестовый случай срабатывает.
Мы переходим ко второму тестовому случаю, затем к третьему. Я реализую их. Ты обращаешь внимание, что код можно упростить. Мне с трудом удается одновременно слушать твои объяснения и продолжать писать код, поэтому я просто передаю тебе клавиатуру. Ты перерабатываешь код, после этого ты запускаешь тестовые случаи и они срабатывают. Ты приступаешь к реализации следующей пары тестовых случаев.
Спустя некоторое время мы смотрим на набор карточек с заданиями, которые необходимо доделать, и обнаруживаем, что нам осталось переделать тестовые случаи, которые были разработаны ранее. Все идет хорошо, и мы переделываем их, а затем убеждаемся в том, что все они срабатывают.
Теперь список дел, которые нам необходимо выполнить, пуст. Мы обращаем внимание на то, что компьютер, на котором выполняется интеграция, свободен. Мы загружаем последнюю версию системы, затем мы загружаем наши последние изменения. Затем мы запускаем все тестовые случаи, потом запускаем новые тестовые случаи, только что разработанные нами, а затем абсолютно все тестовые случаи, которые кто-либо когда-либо разрабатывал. Один из них не срабатывает. Это очень странно, – произносишь ты, – прошло не меньше месяца с тех пор, когда я в последний раз сталкивался со сбоем тестового случая в процессе интеграции. Однако особых проблем в этом нет. Мы отлаживаем тестовый случай и исправляем код. После этого мы вновь запускаем весь полный набор тестов. На этот раз все они срабатывают. Мы включаем наш код в состав системы.
Вот так выглядит полный цикл разработки кода в рамках ХР. Следует обратить внимание на следующие обстоятельства.
• Два программиста работают над решением задачи вместе в одной паре.
• Разработка базируется на тестах. Сначала вы пишете тесты, затем – код. Вы не можете считать работу доделанной до тех пор, пока все тесты не сработают. Если все тесты сработали и вы не в состоянии придумать еще каких-либо других тестов, которые могут не сработать, вы можете считать, что добавление функциональности завершено.