- LXC: Kонтейнерные утилиты Linux
- Эволюционирующая архитектура и непредсказуемое проектирование: Исследование архитектуры и дизайна
- Эволюционирующая архитектура и непредсказуемое проектирование: Проектирование через тестирование, Часть 1
Деньги же я получил только за одну статью, которая в списке под номером 1.
Своему коллеге я склонен доверять и в то, что он мог их присвоить я не верю.
Каюсь, сам не без изъяна, так как задержал перевод примерно на месяц.
Но ведь можно было как-то отреагировать... мда...
Ну да, ладно, то что деньги за проделанную работу до меня не дошли уже неважно, но зато я теперь обладаю моральным правом опубликовать свои старания. Может кому-то и пригодиться, читайте на здоровье.
Хотел опубликовать на Хабре, но там меня заминусовали ))
Эволюционирующая архитектура и непредсказуемое проектирование: Проектирование через тестирование, Часть 1
Позвольте тестам порулить и улучшить ваш дизайн
Резюме: Большинство разработчиков думает что тесты - это самое выгодное в использовании разработки через тестирование (TDD). Однако, если все делать правильно, то TDD полностью изменяет дизайн кода в лучшую сторону. Эта часть из серии статей об Эволюционирующей архитектуре и непредсказуемом проектировании проведет по обширном примеру, показывающему как дизайн может проявляться из-за разных, всплывающих при тестировании, штуковин. Тестирование лишь побочный эффект TDD; важная часть того, как код меняется к лучшему.
Дата оригинала: 24 Февраля 2009
Уровень: средний
PDF: A4 and Letter (146KB | 14 pages)Get Adobe® Reader®
Уровень: средний
PDF: A4 and Letter (146KB | 14 pages)Get Adobe® Reader®
TDD - одна из общеизвестных методологий быстрой разработки ПО. TDD - это стиль написания программ, использующий тесты, чтобы понять последний шаг фазы требований. Тесты пишутся до написания кода, улучшая понимание, что же код должен делать.
Большинство разработчиков думают, что основное преимущество, получаемое от TDD - это комплексное покрытие кода юнит-тестами. Однако, если все делать правильно, то TDD может полностью изменить дизайн вашего кода в лучшую сторону, поскольку откладывает принятие решений пока не наступит ответственный момент. Так как дизайн не спланирован заранее, то это дает выбирать наилучшие альтернативы или проводить рефакторинг, приводящий к наилучшему дизайну. Это статья проведет по примеру, иллюстрирующему мощь в позволении дизайну проявляться из решений, исходящих от юнит-тестов.
Важным словом в определении разработка через тестирование является через, показывающее, что именно тестирование управляет процессом разработки. Рисунок 1 показывает рабочий процесс TDD:
Рисунок 1. Рабочий процесс TDD
Рабочий процесс на рисунке 1 это:
- Написание проваливающего теста.
- Написание кода, который его проходит.
- Повторение шагов 1 и 2.
- Продвижение вперед активным рефакторингом.
- Когда больше нет идей насчет тестов, значит готово.
Разработка через тестирование требует, чтобы тесты появились первыми. Только после того как тесты написаны (и провалены), пишется код под этот тест. Многие разработчики используют разновидность тестирования называемую поздними тестами (пост-тестами, TAD), когда сначала пишется код и потом покрывается юнит-тестами. В этом случае, не смотря на то, что тесты имеются, но аспекты непредсказуемого проектирования из TDD отсутствуют. Ничто не мешает написать совершенно отвратительный код и потом ломать голову, как его протестировать. Разрабатывая код первым, в него внедряются идеи о том как он должен работать, и затем тестируется. TDD требует идти от обратного: сначала написать тест, и позволить ему сообщать, когда код его проходит. Чтобы проиллюстрировать это важное отличие, рассмотрим распространённый пример.
Чтобы показать преимущества дизайна TDD, необходима проблема для решения. В своей книгеРазработка через тестирование (см. Источники), Кент Бэк (Kent Beck) использует для примера деньги — достаточно хорошая иллюстрация TDD, но слегка упрощенная. По-настоящему сложная задача - это найти пример, который не такой комплексный, чтобы потеряться в проблемной области, но достаточно полный, чтобы показать действительную ценность.
В конце концов, были выбраны совершенные числа. Для тех кто не силен в математике, концепция восходит к Эвклиду, который вывел одно из самых ранних доказательств существования совершенных чисел. Совершенное число равно сумме всех своих собственных делителей. Например, 6 - совершенное число, потому что делители 6-ти (исключая саму 6) - это 1, 2, и 3, а 1 + 2 + 3 = 6. Более алгоритмическое определение совершенного числа - это число, которое является суммой делителей (исключая само число), равной этому числу. В примере выше, вычисления такие: 1 + 2 + 3 + 6 - 6 = 6.
Решением для данной проблемной области будет: создать определитель совершенного числа. Реализуем решение двумя различными путями. Для начала, отключим ту часть мозга, которая хочет делать через TDD, и сначала напишем решение, а потом тесты для него. Когда будем развивать TDD версию решения, то сможем сравнить и противопоставить оба подхода.
Для примера, реализуем определитель совершенного числа на языке Java (версии 5 или новее, потому что используются аннотации), JUnit 4.x (самой новой версии), и сопоставители Hamcrest из Google code (см. Источники). Сопоставители Hamcrest представляют синтаксический сахар с человечным интерфейсом к стандартным сопоставителям JUnit. Например, вместо
assertEquals(expected, actual)
, можно написать assertEquals(actual, is(expected))
, что выглядит более осмысленно. Сопоставители Hamcrest поставляются вместе с JUnit 4.x (в виде статического импорта); если все еще используется JUnit 3.x, то можно скачать совместимую версию для него.Листинг 1 показывает первую версию
PerfectNumberFinder
:Листинг 1. Пост-тест
PerfectNumberFinder
public class PerfectNumberFinder1 { public static boolean isPerfect(int number) { // получим делители List |
Конечно, это не особо эффектный код, но свою работу выполняет. Сначала создается динамический список всех делителей (
ArrayList
). Куда добавляется 1 и конечное число (придерживаясь формулы выше, и список всех делителей, включая 1 и само число). Теперь пройдем в цикле по другим возможным коэффициентам до числа, включительно, проверяя, что это делитель. И добавляем в список, если это так. Далее, суммируем все коэффициенты и пишем Java-версию формулы для определения совершенного числа.Теперь, мне необходим юнит-тест, чтобы выяснить, работает оно или нет. Нужны как минимум два теста: один, чтобы видеть, что совершенные числа определяются верно и другой, чтобы предотвратить ложные срабатывания. Юнит-тесты в Листинге 2:
Листинг 2. Юнит-тесты для
PerfectNumberFinder
public class PerfectNumberFinderTest { private static Integer[] PERFECT_NUMS = {6, 28, 496, 8128, 33550336}; @Test public void test_perfection() { for (int i : PERFECT_NUMS) assertTrue(PerfectNumberFinder1.isPerfect(i)); } @Test public void test_non_perfection() { List |
Этот код выводит правильные совершенные числа, но, так как проверяет много чисел, то работает очень медленно на негативных тестах. Вопросы быстродействия, всплывшие из юнит-тестов, возвращают меня в код, чтобы проверить, что можно подкрутить. Сейчас, выбирая делители, цикл в любом случае идет до целевого числа включительно. Но нужно ли так далеко заходить? Нет, если делители можно выбирать парами. Все делители состоят в парах (например, если целевое число 28, то когда найден делитель 2, то можно взять и 14). Поэтому, если делители можно выбирать парами, то шагов необходимо сделать равное квадратному корню из числа. Подводя итог, улучшаем алгоритм и рефакторим код до Листинга 3:
Листинг 3. Улучшенная версия алгоритма
public class PerfectNumberFinder2 { public static boolean isPerfect(int number) { // получим делители List |
Этот код работает за ожидаемое время, но провалилась пара тестовых правил. Это случилось, когда, выбирая парами числа, случайно, при достижение квадратного корня числа, взяли их дважды. Например, для числа 16 квадратным корнем будет 4, которое неумышленно добавлено в список еще раз. Это легко чиниться, защитным условием, как показано на Листинге 4:
Листинг 4. Исправленный улучшенный алгоритм
for (int i = 2; i <= sqrt(number); i++) if (number % i == 0) { factors.add(i); if (number / i != i) factors.add(number / i); } |
Теперь у меня есть версия пост-теста для определителя совершенных чисел. Это работает, но некоторые проблемы дизайна высовывают свои мерзкие головки. Сначала, используя комментарии, разобьем код на секции. Неизменный запах дурного кода - это крик помощи о рефакторинге на отдельные методы. Новый материал, который только что добавили, возможно, требует комментарий, разъясняющий назначение маленького защитного условия, но пока он останется в одиночестве. Самая большая проблема кроется в размере. Мое эмпирическое правило, для проектов на Java, гласит, что не должно быть методов длиннее десяти строк кода. Если метод превысил это число, то, как правило, метод делает более одной вещи, чего следует избегать. Этот метод открыто игнорирует эвристический подход, потому я сделаю следующую попытку, на этот раз используя TDD.
Для кодирования через TDD, заучите мантру: "Какая самая простая вещь, для которой можно написать тест?". В нашем случае: "это число совершенно или нет?". Ответ "Нет" — будет слишком расплывчатым. Необходимо разбить проблему и подумать, что значит "совершенное число". Я легко могу за несколько шагов определить совершенное число:
- Мне нужны делители числа из запроса.
- Необходимо определять, что число - это делитель.
- Необходимо суммировать делители.
Возвращаясь к идее простейшей вещи, какой из элементов списка выглядит самым простым? Полагаю, это определение, что одно число является делителем другого, поэтому вот мой первый тест в Листинге 5:
Листинг 5. Тест "Это число - делитель?"
public class Classifier1Test { @Test public void is_1_a_factor_of_10() { assertTrue(Classifier1.isFactor(1, 10)); } } |
С точки зрения той глупости, которая нужна мне, это простой незначительный тест. Чтобы скомпилировать этот код, у вас должен быть класс
Classifier1
с методом isFactor()
Тогда, необходим скелет структуры класса, прежде чем смогу получить красную полоску. Написание безумно простых тестов, позволит содержать структуру в порядке, до того как начнете существенно размышлять над проблемной областью. Я хочу думать только об одной вещи одновременно, и это поможет работать над скелетной структурой, без нужды волноваться о нюансах решаемой проблемы. Скомпилировав это и получив красную полоску, я готов написать код из Листинга 6:Листинг 6. Первый проход метода "Делитель?"
public class Classifier1 { public static boolean isFactor(int factor, int number) { return number % factor == 0; } } |
Итак, он прост, элегантен, и работает. Теперь я могу приступать к следующей простейшей задаче: получение списка делителей числа. Тесты представлены в Листинге 7:
Листинг 7. Следующий тест: делители числа
@Test public void factors_for() { int[] expected = new int[] {1}; assertThat(Classifier1.factorsFor(1), is(expected)); } |
На листинге 7 самый простой пример, который я смог подобрать для данных делителей, и потому теперь могу написать простейший код, проходящий тест (и чуть позже усложню его). Следующий метод представлен в Листинге 8:
Листинг 8. Простой метод
factorsFor()
public static int[] factorsFor(int number) { return new int[] {number}; } |
Хотя этот метод работает, но намертво тормозит мое продвижение. Хорошей идеей выглядит сделать метод
isFactor()
статическим, т.к. он всего лишь возвращает что-то на основе входных данных. Однако, теперь придется сделать статическим еще и метод factorsFor()
, а значит необходимо передавать обоим методам параметр number
Как побочный эффект завышенной статичности, код становится слишком процедурным. Чтобы починить, займусь рефакторингом двух имеющихся методов, что просто, потому что в них до сих пор мало кода. Отредактированный классClassifier
представлен в Листинге 9:Листинг 9. Улучшенный класс
Classifier
public class Classifier2 { private int _number; public Classifier2(int number) { _number = number; } public boolean isFactor(int factor) { return _number % factor == 0; } } |
Я сделал number переменной экземпляра внутри класса
Classifier2
, что дает избежать ее передачу как параметра куче статических методов.Следующая задача в списке декомпозиции говорит, что нужно найти делители для числа. Таким образом, следующий тест (показан в Листинге 10) должен проверить это:
Листинг 10. Следующий тест: делители числа
@Test public void factors_for_6() { int[] expected = new int[] {1, 2, 3, 6}; Classifier2 c = new Classifier2(6); assertThat(c.getFactors(), is(expected)); } |
А теперь, в Листинге 11, попытаюсь реализовать метод, возвращающий массив делителей для заданного параметра:
Листинг 11. Первый проход метода
getFactors()
public int[] getFactors() { List |
Этот код проходит тест, но, если подумать, он ужасен! Такое случается, когда рассматриваете способы воплощения в коде, используя тесты. Так что же в нем страшного? Во-первых, он слишком большой, запутанный и страдает от проблемы "больше, чем одна вещь". Инстинкт подсказывает мне вернуться к
int[]
, но это добавит много сложности в фундамент, и ничего мне не принесет. Если думать о будущих методах, которые могут вызывать существующий, то это значит заведомо вставать на скользкую дорожку. Чтобы добавить нечто комплексное в сустав скелета, должны быть сильные причины, и таких обоснований сейчас нет. Глядя на код, можно предположить, что произойдет еслиfactors
существует как внутреннее состояние класса, и что позволяет разбить функциональность метода.Одна из полезных характеристик этих поверхностных тестов, что метод по настоящему цельный. Кент Бек (Kent Beck) написал об этом в авторитетной книге Smalltalk Best Practice Patterns (см. Источники). В книги, Кент вводит шаблон метод композиций. Шаблон метода композиций определяет три ключевых утверждения:
- Разделяйте программу на методы, выполняющие одну определимую задачу.
- Держите на одном уровне абстракции все операции метода.
- Программы естественным образом превратятся в множество мелких методов размером в несколько строк.
Метод композиций - это одна из полезных характеристик дизайна на которую провоцирует TDD, и который откровенно нарушен в методе
getFactors()
из Листинга 11. Это возможно починить, проделав следующие шаги:- Перевести
factors
во внутреннее состояние. - Переместить инициализацию
factors
в конструктор. - Давайте уйдем от золочённого преобразования к коду
int[]
и рассмотрим его позже, когда оно станет выгодным. - Добавим другой тест для
addFactors()
.
Четвертый, совершенно неуловимый, но важный шаг. Написание дефектной версии кода, выявило, что первый шаг декомпозиции не завершен. Строка кода
addFactors()
зарытая в середине длинного метода - это тестируемое свойство. Это настолько элементарно, что не обратил внимания при первом рассмотрении проблемы, но теперь все очевидно. Как это обычно и бывает. Один из тестов может привести к последующей декомпозиции проблемы на все более мелкие и мелкие части, и каждая из них тестируемая.До поры до времени, отложу широкую проблему
getFactors()
и всерьез займусь более мелкой новой. Таким образом, вот следующий тест addFactors()
(показан в Листинге 12):Листинг 12. Тест для
addFactors()
@Test public void add_factors() { Classifier3 c = new Classifier3(6); c.addFactor(2); c.addFactor(3); assertThat(c.getFactors(), is(Arrays.asList(1, 2, 3, 6))); } |
Код теста, представленный в Листинге 13, сама простота:
Листинг 13. Простой код для добавления делителей
public void addFactor(int factor) { _factors.add(factor); } |
Запускаю юнит-тест, полный уверенности увидеть зеленую полоску, но он проваливается! Как такой простой тест может провалиться? Основная причина показана на Рисунке 2:
Рисунок 2. Основная причина проваленного юнит-теста
Предполагаемый список должен иметь значения
1, 2, 3, 6
, а в действительности результат 1, 6, 2, 3
. А, так это потому что изменил код для добавления 1 и самого числа в конструкторе. Одним из решений этой проблемы будет постоянно принимать на страхование мои ожидаемые предположения, что 1 и число всегда будут первыми. Но правильное ли это решение? Нет. Проблема намного более глубокая. Делители - это список из чисел? Нет, это набор из чисел. Мое первое (неверное) предположение привело к использованию списка из целых чисел для делителей, но это скверное абстрагирование. Перестроив код на использование наборов вместо списков, не только починили проблему, но используя более правильное абстрагирование, сделали решение лучше в целом.Это именно тот вид дефектного суждения что тесты могут разоблачать, если тесты пишутся до кода, чтобы сбить с толку. Теперь, так как это простой тест, общий дизайн кода стал лучше, потому что найдено более подходящее абстрагирование.
До сих пор, я обсуждал непредсказуемое проектирование в контексте проблемной области совершенных чисел. В особенности замечу, что в первой версии решения (с пост-тестами) сделано некорректное допущение о типах данных. "Пост-тест" проверяет крупномодульную функциональность кода, а не индивидуальные части. TDD проверяет строительные блоки, из которых возникает крупномодульная функциональность, раскрывая дополнительные сведения в процессе работы.
В следующем выпуске, на проблеме совершенных чисел, я продолжу дальнейшее разъяснение примеров дизайна, которые могут проявится, если отказаться от вашего способа тестирования. Когда версия TDD будет завершена, я сравню некоторые показатели обоих наборов исходных коодов. А так же обращу внимание на некоторые другие каверзные вопросы TDD-дизайна, такие как, когда и где тестировать частные методы.
Изучите
- Hamcrest matchers: Библиотека сопоставителей объектов, позволяющая декларативно задавать "совпало" при использовании в других фреймворках.
- Test-Driven Development (Kent Beck, Addison-Wesley, 2003): Бек, создатель экстремального программирования, использует примеры на деньгах, чтобы разъяснить TDD.
- Smalltalk Best Practice Patterns (Kent Beck, Prentice Hall, 1996): Узнайте больше о шаблоне composed method.
- The Productive Programmer (Neal Ford, O'Reilly Media, 2008): Более развернутая версия примера из это статьи рассмотрена в главе "Test Driven Development" новой книги Нила Форда.
- "Emergent Optimization in Test Driven Design" (Michael Feathers): Как тестирование помогает избегать необдуманной оптимизации.
- Поищите в техническом книжном магазине книги по этой и другим техническим тематикам.
- Раздел Java-технологий на developerWorks: Найдите сотни статей о каждом аспекте программирования на Java.
Получите продукты и технологии
- JUnit: Скачайте JUnit.
Обсуждение
- Ознакомьтесь с блогами developerWorks и участвуйте в сообществе developerWorks
Нил Форд (Neal Ford) Архитектор ПО и Идейный вдохновитель (Meme Wrangler) в глобальной консалтинговой IT-компании ThoughtWorks. Он проектирует и разрабатывает приложения, образовательные материалы, статьи для журналов, обучающие системы, видеопрезентации, а так же является автором или редактором книг широкого круга технологий, включая новейшую The Productive Programmer. Специализируется на проектировании и разработке крупномасштабных корпоративных приложениях. Является международно признанным докладчиком на конференциях разработчиков по всему миру. Посмотрите его Веб-сайт.
Комментариев нет:
Отправить комментарий