Только вот если…

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

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

Спасибо сегодняшнему спонсору Unblocked.

Когда я был начинающим программистом, нас ругали за того, что мы не пишем больше документации. Но когда эта документация наконец была необходима, она всегда оказывалась бесполезной. Именно это послужило мне толчком к написанию книг о коммуникативном коде (Smalltalk Best Practice Patterns & Implementation Patterns).

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

image

Пример: Факториал


Мы начинаем с этого:

assert factorial(1) == 1

Реализация проста:

function factorial(n)
return 1

Теперь мы добавляем:

assert factorial(2) == 2

Если мы хотим получить зеленый тест как можно быстрее, то у нас есть несколько вариантов. Выберем вот этот:

function factorial(n)
if n == 1 return 1
return 2

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

image

Обобщай


Практически никто так не поступает (мне есть что рассказать…). Вместо этого большинство в итоге осознает, что в коде заложена определенная структура. Ведь «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 частных случая — пока я не пойму, как их унифицировать. И это нормально, если код корректно обрабатывает нужные нам сценарии.
Единственное, что со мной никогда не случается — это бесконечное копипастирование. Полагаю, это одно из преимуществ СДВГ (или «чувствительности к скуке», как я это предпочитаю называть). Когда я дохожу до четвёртого 'return 4' — будьте уверены, я точно займусь обобщением…

«Гольфинг» — это сокращение кода до минимального количества токенов (результат обычно нечитаем, но это хороший навык в малых дозах). В TDD существует аналогичная практика: какой минимальный набор тестов и самое раннее обобщение дадут код с нужным поведением? Может, нам стоит устраивать соревнования по TDD на скорость?

Послесловие: Связность


Наивная реализация, приведённая выше, обладает сильной связностью (в терминах Empirical Software Design) с тестами. Каждое новое утверждение в тесте требует изменения функции. Обобщение устраняет эту связь между тестами и реализацией. Теперь мы можем добавлять тесты (если необходимо), не меняя код, как и модифицировать код, не изменяя тесты. (Задание для самостоятельной работы: постепенно замените рекурсию на перебор)

Комментарии (3)


  1. nin-jin
    30.05.2025 13:26

    И теперь у нас есть идеально универсальная функция факториала, созданная через TDD и обобщенная крошечными шагами.

    Уходящая в бесконечную рекурсию при n=0, браво!


    1. Zulu0
      30.05.2025 13:26

      Хм, одним нулем озадачил. Это хороший граничный сценарий когда n < 1.


  1. agoncharov
    30.05.2025 13:26

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