Только вот если…
Мне не верится, что я до сих пор пишу такие вещи, но что поделать. Разработка и реализация кода, управляемого тестами (TDD), хороша ровно настолько, насколько хороши решения по проектированию и имплементации, принятые в этом коде. Точно так же, как и код, который разрабатывается не через тестирование.
Вот один ложный аргумент, который я хочу опровергнуть: используя TDD, вы никогда не сможете обобщить код.
Спасибо сегодняшнему спонсору Unblocked.
Когда я был начинающим программистом, нас ругали за того, что мы не пишем больше документации. Но когда эта документация наконец была необходима, она всегда оказывалась бесполезной. Именно это послужило мне толчком к написанию книг о коммуникативном коде (Smalltalk Best Practice Patterns & Implementation Patterns).
Unblocked удовлетворяет ту же самую потребность в тот же самый момент — мне нужно изменить этот код, но что происходит? Unblocked отвечает на этот вопрос с меньшими усилиями со стороны первоначального программиста, и ответы всегда актуальны.
Мы начинаем с этого:
assert factorial(1) == 1
Реализация проста:
function factorial(n)
return 1
Теперь мы добавляем:
assert factorial(2) == 2
Если мы хотим получить зеленый тест как можно быстрее, то у нас есть несколько вариантов. Выберем вот этот:
function factorial(n)
if n == 1 return 1
return 2
При таком наивном применении TDD, как в примере выше, мы бы продолжили делать это, добавляя строку за строкой в функцию факториала. Но мы так не делаем.
Практически никто так не поступает (мне есть что рассказать…). Вместо этого большинство в итоге осознает, что в коде заложена определенная структура. Ведь «2» в функции factorial() это не просто целое число, а произведение двух членов:
function factorial(n)
if n == 1 return 1
return 2 * 1
«2» в «2 * 1» на самом деле не константа, а параметр n. Если мы обобщим, такие же тесты будут проходить (что называется «наблюдаемая эквивалентность», но это два заумных словечка).
function factorial(n)
if n == 1 return 1
return n * 1
“1” в “n * 1” тоже не константа, а результат рекурсивного вызова.
function factorial(n)
if n == 1 return 1
return n * factorial(n — 1)
И теперь у нас есть идеально универсальная функция факториала, созданная через TDD и обобщенная крошечными шагами.
Если бы все обобщения были такими простыми, программирование было бы лёгким. Но это не так — значит, и обобщения не всегда даются так просто. На практике я сталкиваюсь со следующими сложностями:
«Гольфинг» — это сокращение кода до минимального количества токенов (результат обычно нечитаем, но это хороший навык в малых дозах). В TDD существует аналогичная практика: какой минимальный набор тестов и самое раннее обобщение дадут код с нужным поведением? Может, нам стоит устраивать соревнования по TDD на скорость?
Наивная реализация, приведённая выше, обладает сильной связностью (в терминах Empirical Software Design) с тестами. Каждое новое утверждение в тесте требует изменения функции. Обобщение устраняет эту связь между тестами и реализацией. Теперь мы можем добавлять тесты (если необходимо), не меняя код, как и модифицировать код, не изменяя тесты. (Задание для самостоятельной работы: постепенно замените рекурсию на перебор)
Мне не верится, что я до сих пор пишу такие вещи, но что поделать. Разработка и реализация кода, управляемого тестами (TDD), хороша ровно настолько, насколько хороши решения по проектированию и имплементации, принятые в этом коде. Точно так же, как и код, который разрабатывается не через тестирование.
Вот один ложный аргумент, который я хочу опровергнуть: используя TDD, вы никогда не сможете обобщить код.
Спасибо сегодняшнему спонсору Unblocked.
Когда я был начинающим программистом, нас ругали за того, что мы не пишем больше документации. Но когда эта документация наконец была необходима, она всегда оказывалась бесполезной. Именно это послужило мне толчком к написанию книг о коммуникативном коде (Smalltalk Best Practice Patterns & Implementation Patterns).
Unblocked удовлетворяет ту же самую потребность в тот же самый момент — мне нужно изменить этот код, но что происходит? Unblocked отвечает на этот вопрос с меньшими усилиями со стороны первоначального программиста, и ответы всегда актуальны.

Пример: Факториал
Мы начинаем с этого:
assert factorial(1) == 1
Реализация проста:
function factorial(n)
return 1
Теперь мы добавляем:
assert factorial(2) == 2
Если мы хотим получить зеленый тест как можно быстрее, то у нас есть несколько вариантов. Выберем вот этот:
function factorial(n)
if n == 1 return 1
return 2
При таком наивном применении TDD, как в примере выше, мы бы продолжили делать это, добавляя строку за строкой в функцию факториала. Но мы так не делаем.

Обобщай
Практически никто так не поступает (мне есть что рассказать…). Вместо этого большинство в итоге осознает, что в коде заложена определенная структура. Ведь «2» в функции factorial() это не просто целое число, а произведение двух членов:
function factorial(n)
if n == 1 return 1
return 2 * 1
«2» в «2 * 1» на самом деле не константа, а параметр n. Если мы обобщим, такие же тесты будут проходить (что называется «наблюдаемая эквивалентность», но это два заумных словечка).
function factorial(n)
if n == 1 return 1
return n * 1
“1” в “n * 1” тоже не константа, а результат рекурсивного вызова.
function factorial(n)
if n == 1 return 1
return n * factorial(n — 1)
И теперь у нас есть идеально универсальная функция факториала, созданная через TDD и обобщенная крошечными шагами.
Сложности
Если бы все обобщения были такими простыми, программирование было бы лёгким. Но это не так — значит, и обобщения не всегда даются так просто. На практике я сталкиваюсь со следующими сложностями:
- Тестов недостаточно, чтобы ограничить код только «правильными» состояниями. Наивные упрощающие допущения могут оставаться в коде довольно долго, пока не найдётся случай, когда они не сработают.
- Я могу не знать, как сделать обобщение. В моём коде могут годами сохраняться 2, 3, 4 или даже 13 частных случая — пока я не пойму, как их унифицировать. И это нормально, если код корректно обрабатывает нужные нам сценарии.
«Гольфинг» — это сокращение кода до минимального количества токенов (результат обычно нечитаем, но это хороший навык в малых дозах). В TDD существует аналогичная практика: какой минимальный набор тестов и самое раннее обобщение дадут код с нужным поведением? Может, нам стоит устраивать соревнования по TDD на скорость?
Послесловие: Связность
Наивная реализация, приведённая выше, обладает сильной связностью (в терминах Empirical Software Design) с тестами. Каждое новое утверждение в тесте требует изменения функции. Обобщение устраняет эту связь между тестами и реализацией. Теперь мы можем добавлять тесты (если необходимо), не меняя код, как и модифицировать код, не изменяя тесты. (Задание для самостоятельной работы: постепенно замените рекурсию на перебор)
Комментарии (3)
agoncharov
30.05.2025 13:26Почему-то TDD всегда иллюстрируется на примерах, которые не имеют никакого отношения к реальной разработке
nin-jin
Уходящая в бесконечную рекурсию при
n=0
, браво!Zulu0
Хм, одним нулем озадачил. Это хороший граничный сценарий когда n < 1.