Тернии вокруг золота

в 15:18, , рубрики: .net, tdd, Проектирование и рефакторинг, метки:

Примечание автора: это перевод статьи Боба Мартина.
На написание этой статьи меня вдохновила статья Марка Симана (@ploeh). Статья Марка кратко и хорошо изложена. Пожалуйста, прочитайте сначала её, прежде чем продолжать читать данную.
Ловушка, о которой рассказывает Марк, это частный случай более общей ловушки, которую я называю воровством золота. Я могу продемонстрировать эту ловушку, возвращаясь обратно к статье Марка.

Заметьте, что первый тест, который написал Марк выглядел следующим образом:

[InlineData("Seven Lions Polarized"  , "LIONS POLARIZED SEVEN"  )]
[InlineData("seven lions polarized"  , "LIONS POLARIZED SEVEN"  )]
[InlineData("Polarized seven lions"  , "LIONS POLARIZED SEVEN"  )]
[InlineData("Au5 Crystal Mathematics", "AU5 CRYSTAL MATHEMATICS")]
[InlineData("crystal mathematics au5", "AU5 CRYSTAL MATHEMATICS")]

Он уже попал в ловушку. Почему? Потому что он уже украл золото.

Золото и тернии

Основная функциональность, которую Марк пытается описать — это упорядочение слов по алфавиту. Естественно, его тесты и отражают эту функциональность. Основная функциональность — золото и он его своровал.
Проблема в том, что золото защищено невидимой тернистой изгородью, которая опутает любого, ничего о ней не подозревающего программиста, который, будучи ослеплённым золотом, попытается его украсть. Что за тернистая изгородь? В случае Марка это null и пустая строка в качестве входных данных.
Я следую дисциплине TDD вот уже как пятнадцать лет. Я выучил многое об этой невидимой тернистой изгороди. Я выучил урок о том, что она всегда где-то рядом. Я понял, что если вы попытаетесь своровать золото слишком рано, то невидимая изгородь воспрепятствует вашему прогрессу и разорвёт ваши усилия на куски [1]. Так что, стратегия которой я научился следовать состоит в том, чтобы отвести глаза от золота на время, пока я прощупываю изгородь и расчищаю от неё путь.

Разведка и расчистка

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

Поведения-исключения

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

Дегенеративные поведения

Здесь речь идёт о входных данных, которые заставляют основную функциональность делать «ничего». Я поставил «ничего» в кавычки, потому что иногда «ничего» может быть относительно сложным.
В случае Марка, пустые строки и строки, состоящие из пробелов являются дегенеративными входными данными. В конечном счёте он решил проблему таких строк сложным набором условий и операций, которые возвращали пустую строку в случае с одним пробельным символом или пустой строкой в качестве входных данных, в остальных случаях все пробельные символы удалялись [2].
В целом, дегенеративные условия, это штуки вроде пробелов, пустых строк, пустых коллекций, массивов нулевой длины и т.д. В более сложных приложениях, дегенеративный случай может быть достаточно сложным и требовать сложной обработки. Рассмотрите, например, Java-компилятор, который обрабатывает исходные файлы, которые содержат тысячи строк, которые состоят из точек с запятыми и комментариев. Каким должен быть результат обработки?

Вспомогательные поведения

Эти случаи иногда найти труднее всего. Вспомогательные поведения это те, которые окружают и поддерживают основную функциональность, но не являются его частью. Например, функция getSize() класса Stack. Ответ на запрос размера не связан с основной функциональностью, реализующей LIFO.
Дело в том, что вспомогательные поведения часто оказываются полезными для основной функциональности по очевидным причинам. Например, получается так, что размер стека это индекс массива, который используется для операций проталкивания и извлечения в стеках фиксированного размера. Я обычно сталкиваюсь с тем, что, после реализации всех вспомогательных поведений, основную функциональность гораздо проще реализовать.

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

[1] Действительно, всего лишь позавчера я потратил четыре часа, буду опутанным терниями, которые я упустил и не расчистил как следует. В конце концов git reset — hard оказался моим единственным выходом.

[2] Покрыл ли он все возможные условия? Что насчёт табуляции, переводов строк, бэкспэйсов, непечатаемых символов?

Автор: EngineerSpock

Источник

Поделиться новостью

* - обязательные к заполнению поля