Экстремальное программирование. Разработка через тестирование — страница 40 из 41


Рис. П1.4. Обратная связь


постоянно увеличивается. Чтобы обнаружить положительную обратную связь, достаточно посчитать количество отрицательных соединений в цикле. Если в цикле четное количество отрицательных соединений, значит, этот цикл является циклом положительной обратной связи. На рис. П1.4 изображен цикл положительной обратной связи: попав в этот цикл, вы продолжаете набирать вес, пока в составе цикла не появится какое-либо дополнительное действие.

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

Вот три ключа хорошего системного дизайна:

• создание благоприятных циклов, в которых положительная обратная связь приводит к увеличению интенсивности полезных действий;

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

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

Контроль над системой

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

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


Рис. П1.5. Недостаток времени для тестирования приводит к общему недостатку времени


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

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

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

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

Приложение IIФибоначчи

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

Первый тест показывает, что fib(0) = 0. Реализация возвращает константу.


public void testFibonacci() {

assertEquals(0, fib(0));

}

int fib(int n) {

return 0;

}


(Я использую класс TestCase как вместилище кода, так как мы разрабатываем всего одну функцию.)


Второй тест показывает, что fib(1) = 1.


public void testFibonacci() {

assertEquals(0, fib(0));

assertEquals(1, fib(1));

}


Я просто добавил еще один оператор assert() в тот же самый тестовый метод, так как не вижу особого смысла создавать новый метод с именем testFibonacciOfOneIsOne.

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


int fib(int n) {

if (n == 0) return 0;

return 1;

}


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


public void testFibonacci() {

int cases[][] = {{0,0},{1,1}};

for (int i = 0; i < cases.length; i++)

assertEquals(cases[i][1], fib(cases[i][0]));

}


Теперь добавление нового теста требует всего шесть нажатий на клавиши и никаких дополнительных строк:

public void testFibonacci() {


int cases[][] = {{0,0},{1,1},{2,1}};

for (int i = 0; i < cases.length; i++)

assertEquals(cases[i][1], fib(cases[i][0]));

}


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


public void testFibonacci() {

int cases[][] = {{0,0},{1,1},{2,1},{3,2}};

for (int i = 0; i < cases.length; i++)

assertEquals(cases[i][1], fib(cases[i][0]));

}


Ура! Наконец-то тест не сработал. Воспользуемся прежней стратегией (рассматриваем меньшие входные значения как специальные случаи):


int fib(int n) {

if (n == 0) return 0;

if (n <= 2) return 1;

return 2;

}


Теперь мы готовы обобщить код. Мы написали 2, однако на самом деле мы имели в виду 1 + 1.


int fib(int n) {

if (n == 0) return 0;

if (n <= 2) return 1;

return 1 + 1;

}


Первая единица в сумме – на самом деле fib(n-1):


int fib(int n) {

if (n == 0) return 0;

if (n <= 2) return 1;

return fib(n-1) + 1;

}


Вторая единица в сумме – на самом деле fib(n-2):

int fib(int n) {

if (n == 0) return 0;

if (n <= 2) return 1;

return fib(n-1) + fib(n-2);

}


Теперь окончательно чистим код. Та же самая структура должна работать для fib(2), поэтому мы можем преобразовать второй условный оператор:


int fib(int n) {

if (n == 0) return 0;

if (n == 1) return 1;

return fib(n-1) + fib(n-2);

}


Это и есть функция вычисления последовательности Фибоначчи, целиком и полностью разработанная в рамках методики TDD.

Послесловие

Мартин Фаулер (Martin Fowler)


Когда рассказываешь о разработке, основанной на тестировании, сложнее всего передать то психическое состояние, в котором находишься, работая в стиле TDD. Я помню, как в ходе проекта C3 мы с Ральфом Битти (Ralph Beattie) работали над реализацией сложного набора условий выплаты. Ральф сформулировал набор соответствующих тестов, после чего мы приступили к реализации этих тестов одного за другим. Процесс был равномерным и неторопливым, из-за этого казалось, что мы работаем медленно. Однако, взглянув назад на проделанную работу, можно было понять, что, несмотря на кажущуюся неторопливость, мы работали очень даже быстро.

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

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

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

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

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