КулЛиб электронная библиотека 

Идеальная работа [Роберт Мартин] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


Настройки текста:



ИДЕАЛЬНАЯ РАБОТА
ПРОГРАММИРОВАНИЕ БЕЗ ПРИКРАС

Роберт Мартин

2022

ББК 32.973.2-018
УДК 004.3
М29

Мартин Роберт
М29 Идеальная работа. Программирование без прикрас. — СПб.: Питер, 2022. —
384 с.: ил. — (Серия «Библиотека программиста»).
ISBN 978-5-4461-1910-3
В книге «Идеальная работа. Программирование без прикрас» легендарный Роберт Мартин
(Дядюшка Боб) создал исчерпывающее руководство по хорошей работе для каждого программиста.
Роберт Мартин объединяет дисциплины, стандарты и вопросы этики, необходимые для быстрой
и продуктивной разработки надежного, эффективного кода, позволяющего испытывать гордость
за программное обеспечение, которое вы создаете каждый день.
Роберт Мартин, автор бестселлера «Чистый код», начинает с прагматического руководства по
пяти основополагающим дисциплинам создания программного обеспечения: разработка через
тестирование, рефакторинг, простой дизайн, совместное программирование и тесты. Затем он
переходит к стандартам — обрисовывая ожидания «мира» от разработчиков программного обес­
печения, рассказывая, как часто различаются эти подходы, и помогает вам устранить несоответ­
ствия. Наконец он обращается к этике программиста, давая десять фундаментальных постулатов,
которым должны следовать все разработчики программного обеспечения.

16+ (В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-ФЗ.)
ББК 32.973.2-018
УДК 004.3
Права на издание получены по соглашению с Pearson Education Inc. Все права защищены. Никакая часть
данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения
владельцев авторских прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как
надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не
может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за
возможные ошибки, связанные с использованием книги. Издательство не несет ответственности за доступность материалов, ссылки на которые вы можете найти в этой книге. На момент подготовки книги к изданию
все ссылки на интернет-ресурсы были действующими.

ISBN 978-0136915713 англ.
ISBN 978-5-4461-1910-3

© 2022 Pearson Education, Inc.
© Перевод на русский язык ООО «Прогресс книга», 2022
© Издание на русском языке, оформление ООО «Прогресс книга»,
2022
© Серия «Библиотека программиста», 2022

КРАТКОЕ СОДЕРЖАНИЕ

Предисловие . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Вступление . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Благодарности . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Об авторе . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
От издательства . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Глава 1. Мастерство . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27

ЧАСТЬ I
ПРИНЯТЫЕ ПРАКТИКИ
Глава 2. Разработка через тестирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
Глава 3. Дополнительные возможности TDD . . . . . . . . . . . . . . . . . . . . . . . . 102
Глава 4. Разработка тестов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
Глава 5. Рефакторинг . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208
Глава 6. Простой дизайн . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232
Глава 7. Совместное программирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250
Глава 8. Приемочное тестирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254

5

Краткое содержание

ЧАСТЬ II
СТАНДАРТЫ
Глава 9. Производительность . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 261
Глава 10. Качество . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269
Глава 11. Смелость . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279

ЧАСТЬ III
ЭТИКА
Глава 12. Вред . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 303
Глава 13. Верность своим принципам . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 334
Глава 14. Работа в команде . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362

ОГЛАВЛЕНИЕ

Предисловие . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Вступление . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
О термине «мастерство» . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Единственный правильный путь . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Введение в книгу . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Для себя . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Для общества . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Структура книги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Примечание для руководителей . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Благодарности . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Об авторе . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
От издательства . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Глава 1. Мастерство . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27

ЧАСТЬ I
ПРИНЯТЫЕ ПРАКТИКИ
Экстремальное программирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Жизненный цикл . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40

7

Оглавление

Разработка через тестирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
Рефакторинг . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Простота проектирования . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
Совместное программирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
Пользовательское тестирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
Глава 2. Разработка через тестирование . . . . . . . . . . . . . . . . . . . . . . . . 45
Общие сведения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
Программное обеспечение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
Три закона TDD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Четвертый закон . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
Основы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
Простые примеры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
Стек . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
Простые множители . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
Игра в боулинг . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
Резюме . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
Глава 3. Дополнительные возможности TDD . . . . . . . . . . . . . . . . . . . 102
Сортировка 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
Сортировка 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
Мертвая точка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
Настрой, действуй, проверь . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
Введение в BDD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
Конечные автоматы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
И снова про BDD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
Тестовые двойники . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
Пустышка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
Заглушка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
Шпион . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135

8

Оглавление

Подставной объект . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
Имитация . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
Принцип неопределенности TDD . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
Лондон против Чикаго . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
Выбор между гибкостью и определенностью . . . . . . . . . . . . . . . . . 155
Лондонская школа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
Классическая школа, или Школа Чикаго . . . . . . . . . . . . . . . . . . . . . 156
Синтез . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
Архитектура . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
Резюме . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
Глава 4. Разработка тестов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
Тестирование баз данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
Тестирование графических интерфейсов . . . . . . . . . . . . . . . . . . . . . . 164
Графический ввод . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
Шаблоны тестирования . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
Связанный с тестом подкласс . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
Самошунтирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
Скромный объект . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
Проектирование тестов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
Проблема хрупких тестов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
Однозначное соответствие . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
Разрыв соответствия . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
Магазин видеопроката . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
Конкретика против общности . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
Определение очередности преобразований . . . . . . . . . . . . . . . . . . . . 196
{} → ничто . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198
Ничто → константа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198
Константа → переменная . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
Отсутствие условий → выбор . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200

9

Оглавление

Значение → список . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
Оператор → рекурсия . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
Выбор → итерация . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
Значение → измененное значение . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
Пример: числа Фибоначчи . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
Определение очередности преобразований . . . . . . . . . . . . . . . . . . . 206
Резюме . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
Глава 5. Рефакторинг . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208
Что такое рефакторинг . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
Основной инструментарий . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
Переименование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
Выделение методов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
Выделение переменной . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
Выделение поля . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
Кубик Рубика . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226
Практики . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
Тесты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
Быстрые тесты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
Устранение взаимно однозначных соответствий . . . . . . . . . . . . . . 228
Непрерывный рефакторинг . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228
Безжалостный рефакторинг . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229
Поддержка проходимости тестов! . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229
Оставляйте себе выход . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230
Резюме . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230
Глава 6. Простой дизайн . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232
YAGNI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236
Тестовое покрытие . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238
Степень покрытия . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239

10

Оглавление

Асимптотическая цель . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241
Дизайн? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241
Но это еще не все . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242
Максимальное раскрытие предназначения . . . . . . . . . . . . . . . . . . . . 242
Базовая абстракция . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244
Тесты: вторая половина проблемы . . . . . . . . . . . . . . . . . . . . . . . . . . . 245
Минимизация дублирования . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246
Непреднамеренное дублирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247
Минимизация размера . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248
Простой дизайн . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
Глава 7. Совместное программирование . . . . . . . . . . . . . . . . . . . . . . . 250
Глава 8. Приемочное тестирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
Порядок действий . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
Непрерывная сборка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258

ЧАСТЬ II
СТАНДАРТЫ
Ваш новый технический директор . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 260
Глава 9. Производительность . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 261
Мы никогда не будем делать дрянь . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262
Легкая адаптивность . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264
Постоянная готовность . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
Стабильная производительность . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Глава 10. Качество . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269
Постоянное улучшение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270
Бесстрашная компетентность . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271

11

Оглавление

Исключительное качество . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
Мы не будем заваливать работой отдел контроля качества . . . . . 273
Болезнь отдела тестирования . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274
Отдел контроля качества ничего не найдет . . . . . . . . . . . . . . . . . . . . 274
Автоматизация тестирования . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
Автоматизированное тестирование и пользовательские
интерфейсы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
Тестирование пользовательского интерфейса . . . . . . . . . . . . . . . . . . 278
Глава 11. Смелость . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Прикрываем друг другу спину . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
Честная оценка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281
Умение говорить «нет» . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
Непрерывное интенсивное обучение . . . . . . . . . . . . . . . . . . . . . . . . . . 284
Наставничество . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285

ЧАСТЬ III
ЭТИКА
Самый первый программист . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288
75 лет . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289
Ботаники и Спасители . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 294
Образцы для подражания и злодеи . . . . . . . . . . . . . . . . . . . . . . . . . . . . 297
Мы правим миром . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298
Катастрофы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299
Клятва . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301
Глава 12. Вред . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 303
Прежде всего — не навреди . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 304
Не навреди обществу . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305
Нарушение функционирования . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307

12

Оглавление

Нарушение структуры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 310
Программное обеспечение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311
Тесты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 313
Лучшая работа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 314
Делаем это правильно . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315
Что такое хорошая структура . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 316
Матрица Эйзенхауэра . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 318
Программисты как заинтересованные лица . . . . . . . . . . . . . . . . . . 320
Делать все возможное . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322
Повторяемое доказательство . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 324
Дейкстра . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 324
Доказательство правильности . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 325
Структурное программирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . 328
Функциональная декомпозиция . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
Разработка через тестирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331
Глава 13. Верность своим принципам . . . . . . . . . . . . . . . . . . . . . . . . . . 334
Малые циклы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 335
История управления исходным кодом . . . . . . . . . . . . . . . . . . . . . . . 335
Git . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 341
Короткие циклы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 342
Непрерывная интеграция . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343
Ветки и переключатели . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 344
Непрерывное развертывание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 346
Непрерывная сборка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 348
Неустанное улучшение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 349
Покрытие тестами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 349
Мутационное тестирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 350
Семантическая стабильность . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351

13

Оглавление

Очистка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
Творения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
Поддержание высокой продуктивности . . . . . . . . . . . . . . . . . . . . . . . 353
Вязкость . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 354
Управление отвлекающими факторами . . . . . . . . . . . . . . . . . . . . . . 357
Управление временем . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 360
Глава 14. Работа в команде . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362
Работать как одна команда . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 363
Открытый/виртуальный офис . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 363
Честная и справедливая оценка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 365
Ложь . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 366
Честность, безошибочность, точность . . . . . . . . . . . . . . . . . . . . . . . . 367
История 1. Проект «Векторизация» . . . . . . . . . . . . . . . . . . . . . . . . . . 368
История 2. pCCU . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 370
Уроки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
Безошибочность . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372
Точность . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
Обобщение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375
Честность . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 376
Уважение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
Никогда не переставай учиться . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379

ПРЕДИСЛОВИЕ

Я помню, как познакомилась с Дядей Бобом весной 2003 года, вскоре
после того, как нашей команде специалистов по информационным
технологиям рассказали о методологии Scrum. Как скептически
настроенный Scrum-мастер, я слушала рассказы Боба о TDD и инструменте FitNesse и думала: «Зачем вообще писать изначально провальные тесты? Разве тестирование не должно идти за написанием
кода?» Я часто уходила в недоумении, как и многие члены моей
команды. Но стремление Боба к профессионализму в написании
кода не могло не поражать. Я помню, как однажды, просматривая
наш журнал ошибок, он в лоб спросил, с какой стати мы принимаем
настолько неверные решения в отношении программных систем, не
являющихся нашей собственностью: «Это активы компании, а не
ваши личные активы». Его энтузиазм вызывал любопытство, и через
полтора года мы провели рефакторинг, обеспечив 80-процентное
автоматизированное покрытие тестами и чистую кодовую базу, что
значительно упростило процедуру изменения бизнес-модели и целей
компании, сделав намного счастливее как клиентов, так и сотрудников. После этого, вооружившись определением «сделано надежно,
как броня», мы молниеносно выстроили защиту от непрерывно
охотящихся на уязвимости черных хакеров; в сущности, мы научились защищаться от самих себя. Со временем мы с теплотой начали
относиться к Дяде Бобу, который стал для нас настоящим дядей —
добросердечным, решительным и смелым человеком, учившим нас
стоять за себя и поступать правильно. В то время как другие дяди
учили своих племянников кататься на велосипеде или ловить рыбу,
наш Дядя Боб учил нас хранить верность своим принципам. Умение
и желание в любой ситуации проявлять смелость и любопытство

15

Предисловие

были лучшим, чему я научилась за все время моей профессиональной деятельности.
Я следовала советам Боба, когда начала работать Agile-коучем, и быстро
заметила, что лучшие команды разработчиков владели умением подбирать под конкретный контекст и конкретных клиентов лучшие из
практик, принятых в отрасли. Я вспоминала уроки Боба, обнаружив,
что лучшие в мире инструменты разработки хороши ровно настолько,
насколько успешно люди умеют найти им оптимальное применение.
Я поняла, что иногда высокий процент покрытия модульными тестами
достигается исключительно для того, чтобы поставить галочку и соответствовать метрике, а на деле большинство этих тестов ненадежно.
Достижение заданных метриками значений не приносило никакой
пользы. При этом лучшим командам не приходилось заботиться о метриках; у них была цель, они были дисциплинированными, гордыми,
ответственными — и показатели в каждом случае говорили сами за
себя. Книга «Идеальная работа» сплетает все эти уроки и принципы
в практические примеры кода и опыт, иллюстрирующие разницу между
работой, преследующей единственную цель — уложиться в срок, и реальным созданием систем, стабильных в долгосрочной перспективе.
Книга напоминает, что никогда не нужно соглашаться на меньшее, что
нужно делать свое дело с бесстрашной компетентностью. Она, как
старый друг, напомнит о том, что имеет значение, что работает, а что
нет, что увеличивает риск, а что снижает его. Это уроки на все времена.
Возможно, в процессе чтения вы обнаружите, что уже практикуете
некоторые из описанных в книге техник. Но готова держать пари: вы
найдете и новое или, по крайней мере, то, от чего когда-то отказались,
например, поскольку не успевали завершить проект в запланированные сроки или по какой-то другой причине. Для новичков в мире
разработки, специализирующихся как в бизнесе, так и в технологиях,
эта книга — шанс поучиться у одного из лучших специалистов. И даже
самые опытные практики найдут здесь способы что-то для себя улучшить. Возможно, эта книга поможет вам снова обрести энтузиазм,
возродить желание совершенствоваться в своей профессии, сколько
бы препятствий ни стояло на вашем пути.
Разработчики программного обеспечения правят миром, и Дядя Боб
в очередной раз хотел бы напомнить людям, обладающим такой вла-

16

Предисловие

стью, о профессиональной дисциплине. Он продолжает повествование
с того места, на котором закончил книгу «Чистый код». Поскольку
разработчики в буквальном смысле слова пишут правила для всех
людей, Дядя Боб напоминает о необходимости соблюдать строгий этический кодекс, напоминает, что именно они отвечают за то, что делает
написанный ими код, за то, как люди его используют, и за то, где он
выходит из строя. Ошибки в программном обеспечении могут лишить
как средств к существованию, так и жизни. ПО влияет на наш образ
мыслей, на принимаемые нами решения, а благодаря искусственному
интеллекту и предсказательной аналитике — еще и на социальное
и стадное поведение. Поэтому разработчики должны чувствовать свою
ответственность и действовать с большой осторожностью и эмпатией,
ведь от их действий зависит здоровье и благополучие людей. Дядя Боб
помогает нам выстоять перед лицом этой ответственности и стать
профессионалами, которые нужны обществу.
Поскольку на момент написания этого предисловия приближается
двадцатая годовщина с момента создания Манифеста гибкой разработки программного обеспечения, данную книгу можно считать прекрасной возможностью вернуться к основам: своевременным и скромным напоминанием о постоянно растущей сложности мира ПО,
а также о том, что перед человечеством и перед собой мы обязаны
практиковать этичную разработку. Не торопитесь быстрее прочитать
«Идеальную работу». Позвольте принципам укорениться внутри вас.
Практикуйте их. Улучшайте их. Учите им других. Держите эту книгу
на своей книжной полке. Пусть она станет вашим верным другом —
вашим Дядей Бобом, вашим проводником, — пока вы с любопытством
и отвагой прокладываете себе путь в этом мире.
Стася Хаймгартнер Вискарди (Stacia Heimgartner Viscardi),
наставник по CST и Agile

ВСТУПЛЕНИЕ

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

О ТЕРМИНЕ «МАСТЕРСТВО»
Начало XXI века отмечено терминологическими спорами. Индустрия программного обеспечения внесла в эти дискуссии свою лепту.
Термин, который часто считают недостаточно полно описывающим
суть, — мастер своего дела (craftsman).
Я довольно много думал над этим вопросом, разговаривал с людьми,
придерживающимися различных мнений, и пришел к выводу, что
лучшего термина для использования в контексте этой книги нет.
Я рассматривал и такие альтернативы, как «специалист», «умелец»,
«ремесленник», но ни одна из них не имела нужной исторической
весомости. А мне было очень важно подчеркнуть ее.
Словосочетание «мастер своего дела» вызывает в памяти человека,
обладающего глубокими знаниями и опытом в профессиональной
деятельности. Человека, который свободно оперирует своими инструментами и применяет свои профессиональные навыки. Который гордится результатами своего труда, и поэтому можно быть уверенным
в том, что он будет вести себя с достоинством и профессионализмом
в соответствии со своим призванием.

18

Вступление

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

ЕДИНСТВЕННЫЙ ПРАВИЛЬНЫЙ ПУТЬ
В процессе чтения этой книги может возникнуть ощущение, что здесь
описан единственный возможный путь к мастерству. Но я всего лишь
описал собственный путь, а для вас он может оказаться совсем другим.
Выбор только за вами.
Нужен ли вообще один правильный путь? Я не знаю. Возможно. Потребность в строгом определении профессии программиста растет.
К цели можно идти разными путями, в зависимости от важности
создаваемого программного обеспечения. Но как вы скоро убедитесь,
отделить критически важное ПО от неважного не так-то просто.
В одном я уверен. Времена «судей»1 прошли. Сейчас уже недостаточно
того, что каждый программист поступает так, как считает правильным.
Появляются определенные практики, стандарты и этика. Предстоит
решить, будем ли мы, программисты, определять их для себя сами или
они будут навязаны нам теми, кто нас не знает.

ВВЕДЕНИЕ В КНИГУ
Эта книга написана для программистов и для их руководителей.
И одновременно для всего нашего общества. Ведь именно мы, программисты, невольно оказались в самом его центре.

Для себя
Программистам с многолетним стажем, вероятно, знакомо чувство удовлетворения, наступающее после развертывания системы.
1

Отсылка к Книге Судей Израилевых.

19

Вступление

Вы ­испытываете определенную гордость за то, что приложили к этому
руку. Вы счастливы от осознания успешно завершенной работы.
Но гордитесь ли вы тем, как вы достигли такого результата? Чем
вызвана ваша гордость? Самим фактом завершения работы или это
гордость за ваше мастерство? Вы гордитесь тем, что система была
развернута? Или тем, как вы ее построили?
Придя домой после тяжелого рабочего дня, вы смотрите в зеркало
и говорите: «Сегодня я отлично поработал»? Или чувствуете желание
принять душ?
Слишком многие из нас в конце дня ощущают себя грязными. Слишком многие считают, что им приходится выполнять некачественную
работу. Слишком многим кажется, что низкое качество — закономерный результат гонки за высокой скоростью. Слишком многие думают,
что производительность обратно пропорциональна качеству.
В данной книге я пытаюсь сломать такие ментальные установки. Это
книга о том, как работать качественно. Она описывает дисциплины
и практики, которые должен знать каждый программист, чтобы работать быстро, продуктивно и каждый день гордиться результатами
своего труда.

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

20

Вступление

ловек в двух авариях 737 Max? Не потеряла бы финансовая фирма
Knight Capital Group 460 миллионов долларов за 45 минут? Не погибли
бы 89 человек из-за внезапного ускорения автомобилей Toyota?
Каждые пять лет число программистов в мире удваивается. При этом
их практически не обучают. Им показывают инструменты, дают разработать несколько несложных проектов, а затем бросают в работу,
чтобы удовлетворить экспоненциально растущий спрос на новое программное обеспечение. С каждым днем шаткая конструкция, которую
мы называем ПО, все глубже проникает в нашу инфраструктуру, наши
институты, наши правительства и нашу жизнь. И с каждым днем растет риск катастрофы.
Какую катастрофу я имею в виду? Это не крах нашей цивилизации
и не внезапное исчезновение всех программных систем одновременно. Готовый обрушиться карточный домик состоит не из самих
программных систем. Скорее под угрозой находится хрупкая основа
общественного доверия.
Слишком много происшествий с самолетами 737 Max, с самопроизвольным ускорением автомобилей Toyota, с претензиями к автомобилям Volkswagen со стороны California EPA или со сбоем голосования,
как это получилось в Айове. Еще немного громких случаев сбоев
программного обеспечения или злоупотреблений — и отсутствие
у разработчиков дисциплины и этики, а также нехватка стандартов
прикуют к себе внимание недоверчивой и разгневанной общественности. И тогда начнется регулирование, нежелательное для любого
из нас. Регулирование, которое лишит нас возможности свободно исследовать и расширять мастерство разработки ПО; которое наложит
серьезные ограничения на рост технологий и экономики.
Эта книга написана не для того, чтобы остановить безудержное
стремление ко все большему внедрению программного обеспечения.
Не ставлю я целью и снижение темпов его создания. Тем более что
это была бы пустая трата сил. Обществу нужно ПО, и оно в любом
случае его получит. Попытка подавить эту потребность не остановит
надвигающуюся катастрофу общественного доверия.
Скорее своей книгой я пытаюсь убедить разработчиков программного
обеспечения и их руководителей в необходимости дисциплины, а так-

21

Вступление

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

Структура книги
Книга состоит из трех частей, описывающих три уровня: практики,
стандарты и этику.
Первая часть, посвященная различным практикам, описывает самый
низкий уровень и носит прагматичный, технический и предписывающий характер. Ее будет полезно прочитать и понять программистам
всех мастей. Я дал несколько ссылок на видеоролики, в реальном
времени демонстрирующие ритм разработки через тестирование
и рефакторинг. В тексте я тоже попытался дать представление об этом
ритме, но ничто не способно сделать это так же хорошо, как видео.
Вторая часть посвящена стандартам. Это средний уровень. Здесь
я знакомлю вас с ожиданиями, которые окружающий мир возлагает на
нашу профессию. Это хороший материал для руководителей, позволяющий им понять, чего ожидать от профессиональных программистов.
Информация высшего уровня — это часть, посвященная этике. Здесь
в форме клятвы или набора обещаний я описываю этический контекст профессии программиста. В этой части вы встретите множество
исторических и философских дискуссий. Ее имеет смысл читать как
программистам, так и их руководителям.

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

БЛАГОДАРНОСТИ

Спасибо моим мужественным рецензентам: Деймону Пулу (Damon
Poole), Эрику Кричлоу (Eric Crichlow), Хизер Кансер (Heather
Kanser), Тиму Оттингеру (Tim Ottinger), Джеффу Лангру (Jeff Langr)
и Стасе Вискарди (Stacia Viscardi). Они спасли меня от множества
неверных шагов.
Кроме того, я очень благодарен Джули Файфер (Julie Phifer), Крису
Зану (Chris Zahn), Менке Мехте (Menka Mehta), Кэрол Лаллье (Carol
Lallier) и всем сотрудникам издательства Pearson, которые неустанно
совершенствуют выпускаемые книги.
Как всегда, огромное спасибо моему творчески одаренному и талантливому иллюстратору Дженнифер Конке (Jennifer Kohnke). Ее
картинки всегда вызывают у меня улыбку.
И конечно же, спасибо моей прекрасной жене и замечательной семье.

ОБ АВТОРЕ

Роберт С. Мартин, также известный как Дядя Боб (Uncle Bob),
написал первую строку кода в возрасте 12 лет в 1964 году. Работает
программистом с 1970 года. Сооснователь компании cleancoders.com,
предлагающей видеоуроки для разработчиков программного обеспечения, и основатель компании Uncle Bob Consulting LLC, оказывающей
консультационные услуги и услуги по обучению персонала крупным
корпорациям. Был ведущим специалистом в консалтинговой фирме
8th Light, Inc. в городе Чикаго.

24

Об авторе

Опубликовал десятки статей в специализированных журналах и регулярно выступает на международных конференциях и выставках. Создатель популярной серии обучающих видео на сайте cleancoders.com.
Мартин написал несколько книг, еще для некоторых он выступил
редактором:
zz Designing Object-Oriented C++ Applications Using the Booch Method;
zz Patterns Languages of Program Design 3;
zz More C++ Gems;
zz Extreme Programming in Practice;
zz Agile Software Development: Principles, Patterns, and Practices1;
zz UML for Java Programmers;
zz Clean Code2;
zz The Clean Coder3;
zz Clean Architecture: A Craftsman’s Guide to Software Structure and

Design4;
zz Clean Agile: Back to Basics5.

Как лидер в сфере разработки программного обеспечения, Мартин три
года был главным редактором журнала C++ Report и первым председателем группы Agile Alliance.

1
2
3

4

5

Мартин Р. С. Быстрая разработка программ: Принципы, примеры, практика.
Мартин Р. С. Чистый код: Создание, анализ и рефакторинг. — СПб.: Питер.
Мартин Р. С. Идеальный программист: Как стать профессионалом разработки
ПО. — СПб.: Питер.
Мартин Р. С. Чистая архитектура: Искусство разработки программного обес­
печения. — СПб.: Питер.
Мартин Р. С. Чистый Agile. Основы гибкости. — СПб.: Питер.

25

ОТ ИЗДАТЕЛЬСТВА

Обучающие видеоролики, ссылки на которые дает автор, — на английском языке. Срок бесплатного доступа к ним ограничен и составляет
десять дней.
Ваши замечания, предложения, вопросы отправляйте по адресу
comp@piter.com (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.

1

МАСТЕРСТВО

Глава 1. Мастерство

Мечта летать, наверное, так же стара, как само человечество. Древнегреческий миф о Дедале и Икаре датируется примерно 1550 годом
до нашей эры. В последующие тысячелетия в погоне за этой мечтой
множество смелых, но безрассудных людей привязывали к себе несуразные приспособления и прыгали со скал и башен навстречу неизбежной смерти.
Ситуация изменилась примерно 500 лет назад. Машины, эскизы которых нарисовал Леонардо да Винчи, не могли летать, но, по крайней
мере, подход к их проектированию был логичным. Именно да Винчи
понял, что полет возможен, поскольку сопротивление воздуха работает в обе стороны. Сопротивление, возникающее, когда на воздух давят
сверху, создает такую же подъемную силу. Этот принцип стал основой
создания всех современных самолетов.
Про идеи да Винчи забыли до середины XVIII века. А затем начались
лихорадочные изыскания возможности летать. XVIII и XIX века
стали временем упорных исследований и экспериментов в сфере
воздухоплавания. Строились, тестировались, отбрасывались и совершенствовались безмоторные прототипы. Начала формироваться
такая наука, как аэронавтика. Появились определения подъемной
силы, сопротивления, тяги и гравитации. Смельчаки стали предпринимать попытки полетов.
Некоторые падали и погибали.
С конца XVIII века в течение почти 50 лет отец современной аэродинамики сэр Джордж Кейли (George Cayley) строил экспериментальные установки, прототипы и полноразмерные модели. Кульминацией
его усилий стал первый пилотируемый полет планера.
А смельчаки продолжали падать и погибать.
Затем наступила эпоха паровых машин, которая принесла с собой
возможность управляемых полетов. Были построены десятки прототипов и проведены множество экспериментов. Летный потенциал
стали исследовать многочисленные ученые и энтузиасты. В 1890 году
Клеман Адер (Clément Ader) на двухмоторной паровой машине пролетел 50 метров.
Но все равно оставались те, кто падал и погибал.

28

Мастерство

Двигатель внутреннего сгорания полностью изменил правила игры.
Вероятнее всего, первый контролируемый полет совершил Густав
Уайтхед (Gustave Whitehead) в 1901 году. Первый же по-настоящему
управляемый полет на оснащенном двигателем аппарате тяжелее воздуха выполнили 17 декабря 1903 года в местечке Килл-Девил-Хиллз
штата Северная Каролина братья Райт (Wright Brothers). Но даже
тогда хватало и тех, кто падал и погибал.
Тем не менее всего за одну ночь мир изменился. Одиннадцать лет спустя, в 1914 году, над Европой шли воздушные бои на бипланах. И хотя
в этих боях многие разбивались и погибали, столько же разбилось
и погибло при попытках научиться летать. Принципы полета были
более-менее понятными, а вот как полет осуществляется технически,
люди почти не понимали.
Спустя еще два десятилетия грозные истребители и бомбардировщики несли смерть и разрушения городам Франции и Германии. Эти самолеты летали очень высоко, были оснащены пулеметами и обладали
огромной разрушительной силой.
За время Второй мировой было потеряно 65 тысяч американских
самолетов. Но из них только 23 тысячи были потеряны в боях. Куда
чаще летчики гибли потому, что толком не умели летать.
Следующее десятилетие ознаменовалось появлением реактивных самолетов, преодолением звукового барьера и взрывным ростом количества коммерческих авиалиний и гражданских авиаперевозок. Начался
век высоких скоростей, и состоятельные люди получили возможность
за считаные часы перемещаться между городами и странами.
Количество авиакатастроф при этом ужасало, так как мы еще многого не понимали в самолетостроении и пилотировании. Тем не менее
к концу 1950-х пассажирские самолеты Боинг 707 уже летали по
всему миру. А к концу 1960-х появился первый широкофюзеляжный
реактивный самолет Боинг 747. Воздушные путешествия стали самым
безопасным1 и эффективным средством передвижения в истории.
Но это потребовало много времени и человеческих жертв.

1

Если не брать в расчет Боинги 737 Max.

29

 Глава 1. Мастерство

Чесли Салленбергер (Chesley Sullenberger) родился в 1951 году в городе Денисон, штат Техас. Настоящее дитя века высоких скоростей.
Он научился летать в шестнадцать и в конце концов начал пилотировать сверхзвуковые истребители F-4 Phantom. В 1980 году перешел на
работу в гражданскую авиацию и стал пилотом US Airways.
Пятнадцатого января 2009 года, сразу после вылета из аэропорта
Ла-Гуардия пилотируемый Салленбергером Airbus A320, на борту
которого находились 155 человек, столкнулся со стаей гусей и потерял
оба реактивных двигателя. Благодаря опыту, приобретенному за более
чем 20 тысяч часов воздушных полетов, Салленбергеру удалось развернуть выведенный из строя лайнер и приводниться на поверхность
реки Гудзон. Сто пятьдесят пять человек были спасены, поскольку
командир воздушного судна Салленбергер был настоящим мастером
своего дела.
Мечта о быстрых и точных вычислениях и управлении данными тоже,
похоже, существует столько же времени, сколько и человечество.
Тысячи лет назад люди использовали для счета пальцы, палочки,
бусины. Более четырех тысяч лет назад появились счеты. Около двух
тысяч лет назад создали механические устройства для предсказания
движения звезд и планет. А около 400 лет назад изобрели логарифмическую линейку.
В начале XIX века Чарлз Бэббидж (Charles Babbage) начал строить механические вычислительные аппараты, которые приводились
в действие специальными рукоятками. Это были настоящие вычислительные комплексы с памятью и арифметической обработкой.
Но их производство затруднял низкий уровень металлообработки.
Бэббидж построил несколько прототипов, но коммерческого успеха
они не имели.
В середине 1800-х годов у него возникла идея гораздо более мощного
программируемого вычислительного устройства, которое в итоге стало прообразом современного цифрового компьютера. Свое творение
Бэббидж назвал аналитической машиной.
Дочь лорда Байрона Ада, графиня Лавлейс, переводя на английский
язык лекцию Бэббиджа, записанную по-французски, пришла к неожиданному выводу, что со временем такая машина не будет ограничена

30

Мастерство

работой с числами, а сможет обрабатывать любые объекты. Поэтому
Аду часто называют первым в мире настоящим программистом.
Из-за отсутствия финансирования и низкого уровня технологий того
времени аналитическая машина Бэббиджа так и не была построена.
На много десятилетий прогресс в области цифровых компьютеров
остановился. Впрочем, то была золотая пора механических аналоговых
счетных машин.
В 1936 году Алан Тьюринг (Alan Turing) показал, что не существует
общего способа доказать решаемость произвольного диофантового
уравнения1. Для этого математик воспользовался моделью в виде
простого, хотя и бесконечного цифрового компьютера, и доказал
существование невычислимых чисел. В процессе работы над этим доказательством были изобретены конечные автоматы, машинный язык,
язык символов, макросы и примитивные подпрограммы. Тьюринг
изобрел то, что сегодня мы бы назвали программным обеспечением.
Почти в то же время Алонзо Черч независимо от Тьюринга сформулировал и доказал эту же задачу, попутно разработав лямбда-исчисление — основную концепцию функционального программирования.
В 1941 году Конрад Цузе (Konrad Zuse) построил первый электромеханический программируемый цифровой компьютер Z3. Он состоял
из более чем 2000 реле и работал с тактовой частотой от 5 до 10 Гц.
Машина использовала двоичную арифметику, длина машинного слова
составляла 22 бита.
Во время Второй мировой войны Тьюринга пригласили помочь экспертам из Блетчли-парка (центра британской разведки), которые бились над расшифровкой кодов немецкой «Энигмы». Она представляла
собой электромеханическую роторную машину, которая случайным
образом меняла символы текстовых сообщений, транслируемых по
радиотелеграфу. Тьюринг помог создать устройство для расшифровки
кодов «Энигмы».
После войны он сыграл важную роль в создании и программировании одного из первых в мире ламповых компьютеров — Automatic
1

Уравнение с целыми коэффициентами.

31

Глава 1. Мастерство

Computing Engine (ACE). Первоначальный прототип содержал
1000 электронных ламп и обрабатывал двоичные числа со скоростью
миллион бит в секунду.
В 1947 году, написав несколько программ для этой машины и изучив
ее возможности, Тьюринг прочитал лекцию, во время которой прозвучали следующие пророческие заявления:
Нам потребуется большое количество способных математиков
для преобразования задач в форму, подходящую для машинной обработки.
Одной из трудностей станет необходимость придерживаться
определенных практик, позволяющих не терять из виду то, что
делаем.
И за одну ночь мир изменился.
За несколько лет была изобретена память на магнитных сердечниках.
Появилась возможность за микросекунды получать доступ к сотням
тысяч, если не к миллионам битов памяти. А массовое производство
электронных ламп привело к появлению более дешевых и надежных
компьютеров. Становилось реальностью мелкосерийное массовое
производство. К 1960 году фирма IBM продала 140 компьютеров
модельного ряда 70x. Это были огромные машины на электронных
лампах стоимостью в миллионы долларов.
Тьюринг использовал для написания программ двоичный код, но все
понимали, что это непрактично. В 1949 году Грейс Хоппер (Grace
Hopper) придумала слово компилятор, а к 1952 году создала его
первую версию A-0. В конце 1953 года Джон Бэкус (John Backus)
представил первую спецификацию языка FORTRAN. К 1958 году
появились ALGOL и LISP.
Первый работающий транзистор был создан Джоном Бардином (John
Bardeen), Уолтером Браттейном (Walter Brattain) и Уильямом Шокли
(William Shockley) в 1947 году. В 1953 году был введен в эксплуатацию
первый транзисторный компьютер. Переход с электронных ламп на
транзисторы изменил все. Компьютеры стали меньше, быстрее, дешевле и намного надежнее.

32

Мастерство

К 1965 году IBM выпустила 10 тысяч компьютеров модели 1401. Они
сдавались в аренду за 2500 долларов в месяц, что было вполне доступно среднему бизнесу. Предприятия нуждались в программистах,
соответственно, спрос на них стал расти.
Кто программировал все эти машины? Университетских курсов не
существовало. В 1965 году не было возможности поступить в высшее
учебное заведение, чтобы научиться программировать. Поэтому программистов брали из бизнеса. Это были зрелые люди в возрасте от
30 до 50 лет.
К 1966 году IBM ежемесячно производила 1000 компьютеров серии
System/360. Бизнесу этого было мало. Это были машины с объемом
памяти 64 Кбайт и выше, умеющие выполнять сотни тысяч инструкций в секунду.
В том же году в Норвежском вычислительном центре в процессе работы над операционной системой Univac1107 Оле-Йохан Даль (OleJohan Dahl) и Кристен Нюгор (Kristen Nygard) изобрели Simula 67,
который можно считать объектным расширением языка ALGOL. Это
был первый объектно-ориентированный язык.
И все это всего через два десятка лет после лекции Алана Тьюринга!
Через два года, в марте 1968-го, Эдсгер Дейкстра (Edsger W. Dijkstra)
написал в журнал Communications of the ACM (CACM) свое знаменитое
письмо. Редактор озаглавил его «О вреде оператора goto»1. Так родилось структурное программирование.
В 1972 году в лабораториях Белла в штате Нью-Джерси Кен Томпсон (Ken Thompson) и Деннис Ритчи (Dennis Ritchie) в промежутке
между работой над собственными проектами выпросили у коллег из
соседней группы время на компьютере PDP 7 и изобрели операционную систему UNIX и язык программирования C.
После этого события стали развиваться с почти головокружительной
скоростью. Посмотрите на этот перечень ключевых дат. Задайте себе

1

Dijkstra E. W. Go To Statement Considered Harmful // Communications of the
ACM. 1968. № 3.

33

Глава 1. Мастерство

вопрос: сколько в мире компьютеров? А сколько программистов? Откуда они все взялись?
1970 — с 1965 года корпорация Digital Equipment Corporation продала свыше 50 тысяч компьютеров PDP-8.
1970 — Уинстон Ройс (Winston Royce) написал статью «Управление
разработкой больших программных систем», в которой описывалась каскадная модель разработки.
1971 — фирма Intel выпустила микропроцессор 4004.
1974 — фирма Intel выпустила микропроцессор 8080.
1977 — фирма Apple выпустила первый серийный персональный
компьютер Apple II.
1979 — фирма Motorola выпустила 16-битный микропроцессор 68000.
1980 — Бьёрн Страуструп (Bjarne Stroustrup) разработал язык про­
граммирования C, добавив к нему возможность работы с классами (чтобы сделать похожим на язык Simula).
1980 — Алан Кей (Alan Kay) изобрел объектно-ориентированный
язык Smalltalk.
1981 — фирма IBM выпустила первый массовый персональный
компьютер IBM PC.
1983 — фирма Apple выпустила первый персональный компьютер
Macintosh, имеющий 128 Kбайт памяти.
1983 — Бьёрн Страуструп переименовал C с классами в C++.
1985 — Министерство обороны США приняло каскадную модель
в качестве официального стандарта разработки программного
обеспечения (стандарт DOD-STD-2167A).
1986 — издательство Addison-Wesley выпустило книгу Бьёрна Страуструпа «Язык программирования C++».
1991 — издательство Benjamin/Cummings выпустило книгу Гради
Буча (Grady Booch) «Объектно-ориентированный анализ
и проектирование с примерами приложений».

34

Мастерство

1991 — Джеймс Гослинг (James Gosling) изобрел язык Java (изначально называвшийся Oak).
1991 — Гвидо ван Россум (Guido Van Rossum) придумал язык Python.
1995 — издательство Addison-Wesley выпустило книгу «Приемы
объ­ектно-ориентированного проектирования. Паттерны проектирования», написанную Эрихом Гаммой (Erich Gamma),
Ричардом Хелмом (Richard Helm), Джоном Влиссидесом
(John Vlissides) и Ральфом Джонсоном (Ralph Johnson).
1995 — Юкихиро Мацумото (Yukihiro Matsumoto) создал язык программирования Ruby.
1995 — Брендан Эйх (Brendan Eich) создал язык JavaScript.
1996 — компания Sun Microsystems выпустила первую официальную
версию языка Java.
1999 — компания Microsoft придумала язык C# (сначала называвшийся Cool) и платформу .NET.
2000 — проблема 2000 года.
2001 — написан Agile Manifesto (манифест гибкой разработки программного обес­печения).
За период с 1970 по 2000 год тактовая частота компьютеров увеличилась на три порядка, плотность упаковки данных — на четыре, дисковое пространство, как и объем оперативной памяти, — на шесть-семь
порядков. Причем если раньше за доллар можно было купить один бит
оперативной памяти, то теперь — гигабит. Немыслимо представить,
как изменилось аппаратное обеспечение, но даже если просто суммировать все вышеупомянутые мной вещи, можно сказать, что наши
возможности возросли примерно на 30 порядков.
И все это чуть более чем через полвека после лекции Алана Тьюринга.
Сколько сейчас программистов? Сколько строк кода написано? Насколько хорош этот код?
Попытайтесь сравнить это с историей становления авиации. Видите
сходство? Видите, как постепенно развивалась теоретическая часть,

35

Глава 1. Мастерство

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

Часть I. Принятые

практики

I

Принятые

практики

Часть I. Принятые практики

Что такое принятая практика? Это набор правил, состоящий из обязательной и произвольной частей. Обязательная часть — это то, что
делает практику действенной; это причина ее существования. Произвольная часть придает практике форму и содержание. Без произвольной части практика существовать не может.
Например, хирург перед операцией моет руки. Понаблюдав за этим
процессом, вы увидите, что мытье рук выполняется особым образом.
Хирург не просто намыливает их под проточной водой, как это делает
любой из нас. Он следует ритуальной процедуре, которая выглядит
примерно так:
zz взять соответствующее мыло;
zz взять подходящую щетку;
zz для каждого пальца сделать:

• десять движений по верхней стороне;
• десять движений по левой стороне;
• десять движений по тыльной стороне;
• десять движений по правой стороне;
• десять движений под ногтем;
zz и т. д.

Обязательная часть практики должна быть очевидной. Хирург обязан иметь очень чистые руки. Но обратили ли вы внимание на произвольную часть? Почему движений десять, а не восемь или двенадцать? Зачем делить палец на пять областей? Почему не на три
или на семь?
В данном случае все эти числа выбраны произвольно. Просто считается, что этого будет достаточно.
В этой книге я расскажу вам о пяти практиках профессиональной
разработки программного обеспечения. Одним из них уже полвека.
Другим всего пара десятков лет. Но все они показали свою полезность.
Без них в сфере создания ПО было бы практически немыслимым само
понятие профессионального мастерства.

38

Принятые практики

Каждая из этих практик имеет собственные обязательные и произвольные элементы. В процессе чтения вы можете столкнуться с тем,
что некоторые из этих практик покажутся вам необоснованными или
ненужными. В этом случае попытайтесь понять, какие элементы —
обязательные или только произвольные — вызывают это чувство.
Не позволяйте произвольным элементам сбить вас с толку. Как только
вы поймете сущность каждой практики, влияние произвольных элементов, скорее всего, уменьшится.
Скажем, в 1861 году Игнац Земмельвейс (Ignaz Semmelweis) опубликовал статью о необходимости мытья рук для врачей. Результаты его
исследований ошеломляли. Он смог показать, что в случаях, когда
перед осмотром беременных врачи тщательно мыли руки водным
раствором хлора, смертность от родильной горячки, от которой в то
время умирала каждая десятая женщина, падала практически до
нуля.
Но врачи того времени, рассматривая предложенную Земмельвейсом
практику, не смогли отделить обязательную часть от произвольной.
Водный раствор хлора был произвольной частью. Суть практики заключалась в самом факте мытья рук. Но мыть их хлорной водой было
неудобно, поэтому врачи отвергли саму идею.
Прошло много десятилетий, прежде чем они стали это делать.

ЭКСТРЕМАЛЬНОЕ ПРОГРАММИРОВАНИЕ
В 1970 году, после статьи Уинстона Ройса каскадная разработка стала
общепринятой практикой. Исправление этой ошибки заняло почти
30 лет.
К 1995 году специалисты в сфере программного обеспечения начали рассматривать другой, более поэтапный подход. Была предложена к рассмотрению методология Scrum, разработка, управляемая функциональностью (feature-driven development, FDD), метод
разработки динамических систем (dynamic systems development
method, DSDM) и методология Crystal. Но в целом в отрасли мало
что изменилось.

39

Часть I. Принятые практики

В 1999 году издательство Addison-Wesley выпустило книгу Кента
Бека Extreme Programming Explained1. Предложенная Беком концепция
базировалась на идеях из вышеперечисленных подходов, добавляя
к ним кое-что новое, а именно практики разработки.
Следующие два года энтузиазм по отношению к экстремальному программированию рос экспоненциально. Именно это и привело к Agileреволюции. Экстремальное программирование по сей день остается
наиболее определенным и наиболее полным из всех Agile-методов. Эта
глава посвящена принятым в нем практикам разработки.

Жизненный цик л
На рис. 1.1 вы видите жизненный цикл Рона Джеффриса, содержащий
перечень практик XP. Я расскажу вам о четырех практиках, расположенных в центре, и об одной крайней слева.

Рис. 1.1. Практики экстремального программирования

1

40

Бек К. Экстремальное программирование. — СПб.: Питер.

Принятые практики

В центре находятся такие практики, как разработка через тестирование (test-driven development, TDD), рефакторинг, простота
проектирования и парное программирование (которое мы будем
­называть совместным программированием). В крайней левой позиции располагается наиболее технически и инженерно-ориентированная из коммерческих практик XP — пользовательское тестирование.
Это основополагающие практики профессиональной разработки программного обеспечения.

РАЗРАБОТКА ЧЕРЕЗ ТЕСТИРОВАНИЕ
Разработка через тестирование — ключевая дисциплина. Без нее все
остальные практики невозможны или бесполезны. Именно поэтому
две следующие главы, в которых она описывается, занимают почти
половину книги и наполнены техническими деталями. Такой подход
может показаться вам несбалансированным. Более того, мне тоже
так кажется. Я изо всех сил пытался понять, как с этим быть, но пришел к выводу, что подобный перекос возник как следствие ситуации
в нашей отрасли. К сожалению, с этой практикой хорошо знакомо
слишком маленькое количество программистов.
Практика разработки через тестирование определяет действия программиста вплоть до секунд. Ее невозможно применить заранее или
постфактум. Ее нельзя не заметить, поскольку она пронизывает весь
процесс. Ее не получится придерживаться частично. Вы или практикуете разработку через тестирование, или нет.
Суть TDD очень проста. Все начинается с маленьких циклов и тестов. Причем тесты всегда оказываются на первом месте. Они первыми пишутся. Они первыми приводятся в порядок. Они предшествуют любой задаче. При этом все задачи разбиты на мельчайшие
циклы.
Время цикла измеряется не в минутах — в секундах. Оно измеряется
в символах, а не в строках. Цикл обратной связи замыкается, едва
открывшись.

41

Часть I. Принятые практики

Цель TDD — создать набор тестов, которому можно полностью доверять. Если этот набор тестов пройден, значит, при развертке кода
можно чувствовать себя в безопасности.
Разработка через тестирование — самая обременительная и сложная
из всех практик. Она обременительна, поскольку влияет на все остальные аспекты. С нее вы начинаете и ею заканчиваете. Она накладывает
ограничения на все действия. Она поддерживает темп работы, невзирая на давление окружающей среды.
Сложность разработки через тестирование обусловлена сложностью
кода. Для каждого варианта кода существует соответствующий вариант TDD. При этом тесты должны соответствовать коду, но не
быть с ним связаны, должны охватывать почти все аспекты кода, но
при этом выполняться за секунды. Разработка через тестирование —
скрупулезно нарабатываемый и сложный навык, который осваивается
с большим трудом, но дает потрясающие результаты.

РЕФАКТОРИНГ
Рефакторинг — это практика, позволяющая писать чистый код. Она
трудно реализуема, а порой и невозможна без TDD1. Соответственно,
получить чистый код без TDD сложно или невозможно.
Рефакторинг превращает плохо структурированный код в код с лучшей структурой, не меняя его поведения. Последняя часть здесь — самая важная. Именно неизменность поведения кода гарантирует, что
даже после изменения его структуры он останется безопасным.
Хотя программные системы и деградируют со временем, в них предпочитают не вмешиваться из страха повлиять на их поведение. Наличие безопасного способа очистки позволяет привести код в порядок,
остановив деградацию.
1

42

Существуют и другие практики, так же хорошо, как TDD, способствующие выполнению рефакторинга. Например, предложенная Кентом Беком концепция
TCR (test && commit || revert). Впрочем, на момент написания данного текста
она не получила широкого распространения и интересна исключительно с академической точки зрения.

Принятые практики

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

ПРОСТОТА ПРОЕКТИРОВАНИЯ
Жизнь на земле можно описывать на разных уровнях. Высший уровень — это экология, изучающая взаимодействие в рамках биосистем.
Ниже находится физиология, изучающая внутреннюю механику
жизни. Еще ниже — микробиология, рассматривающая клетки, нуклеиновые кислоты, белки и другие макромолекулярные системы.
Те, в свою очередь, описываются химией, которая базируется на
квантовой механике.
Если таким же способом рассматривать программирование, то TDD
окажется аналогом квантовой механики. Рефакторинг будет соответствовать химии, а простота проектирования — микробиологии. На
уровне физиологии у нас окажутся принципы SOLID, объектно-ориентированное проектирование и функциональное программирование,
а на уровне экологии — архитектура.
Простота проектирования практически невозможна без рефакторинга.
Ведь именно она выступает конечной целью рефакторинга, в то время
как сам он является единственным практическим средством для достижения этой цели. Именно рефакторинг позволяет получить проект,
состоящий из простых атомарных фрагментов, хорошо вписывающихся в более крупные структуры программ, систем и приложений.
Простота проектирования базируется на четырех несложных правилах, так что придерживаться данной практики легко. Однако,
в отличие от TDD и рефакторинга, это неточная дисциплина. Здесь

43

Часть I. Принятые практики

приходится опираться на рассуждения и опыт. Первый признак, по
которому можно отличить выучившего правила новичка от понимающего принципы специалиста, — отличный результат. Майкл Физерс
(Michael Feathers) назвал это чувствовать проект.

СОВМЕСТНОЕ ПРОГРАММИРОВАНИЕ
Практика совместного программирования отражена в искусстве работы в команде. Она включает в себя такие вещи, как парное или
групповое программирование, анализ кода и мозговые штурмы. Совместное программирование предполагает участие вообще всех членов
команды — как программистов, так и всех остальных. Это основной
способ поделиться знаниями, обеспечить согласованность работы
и объединить команду в функционирующее целое.
Совместное программирование — наименее техническая и наименее
директивная из всех практик. Но порой она оказывается самой важной, поскольку создать эффективную команду очень непросто, и это
большая ценность.

ПОЛЬЗОВАТЕЛЬСКОЕ ТЕСТИРОВАНИЕ
Практика пользовательского тестирования связывает команду разработчиков программного обеспечения с его заказчиками. Перед
разработчиками стоит цель — обеспечить поведение системы в соответствии с данной заказчиками спецификацией. Для проверки этого
соответствия пишутся тесты. Успешное их прохождение означает, что
система ведет себя так, как указано в требованиях.
Представители заказчика должны иметь возможность прочитать и понять эти тесты и внести в них коррективы. Наблюдение за процессом
тестирования и участие в нем позволяют заказчикам убедиться, что
программное обеспечение делает именно то, что от него требуется.

2

РАЗРАБОТКА
ЧЕРЕЗ ТЕСТИРОВАНИЕ

Часть I. Принятые практики

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

ОБЩИЕ СВЕДЕНИЯ
Ноль — очень важная цифра. Это значение равновесия. Когда оба
плеча весов в равновесии, стрелка показывает ноль. Атом, в ядре
которого количество электронов совпадает с количеством протонов,
имеет нулевой заряд, то есть оказывается электрически нейтральным.
Равна нулю и сумма сил, приложенных к находящемуся в состоянии
покоя объекту. Ноль — число баланса.
Вы когда-нибудь задумывались, почему состояние расчетного счета
в банке называют балансом? Дело в том, что это состояние представляет собой итог всех транзакций, всех снятий денег со счета
и внесений их на счет. При этом в транзакции принимают участие две
стороны, между которыми происходит перемещение денег.
Схематично это можно представить следующим образом: на ближней
стороне ваш банковский счет, а на дальней — счет второго участника
транзакции. Если на ближней стороне деньги вносятся на счет, значит,
где-то на дальней стороне эта сумма снимается со счета. Каждый раз,
когда вы выписываете чек, с вашего счета снимаются деньги и вносятся на какой-то другой. Сумма всех транзакций по счету и есть баланс.
Сумма всех ближних и дальних сторон, участвовавших в транзакциях,
должна быть равна нулю.
Две тысячи лет назад Гай Плиний Секунд, известный как Плиний
Старший, реализовал эту формулу бухгалтерского учета, придумав

46

Глава 2. Разработка через тестирование

принцип двойной записи. Этот принцип веками совершенствовался
каирскими банкирами, а затем венецианскими купцами. В 1494 году
друг Леонардо да Винчи монах-францисканец Лука Пачоли (Luca
Pacioli) впервые подробно описал принцип двойной записи в своей
книге. Печатный станок уже был изобретен, и это способствовало
распространению информации.
В 1772 году, когда Европа столкнулась с суровой рецессией, Джозайе
Веджвуду (Josiah Wedgwood) пришлось изрядно побороться за успех.
Продукция его завода керамики перестала пользоваться спросом.
Веджвуд ввел на предприятии бухгалтерский учет с двойной записью,
чтобы понять, где рождается прибыль и как ее увеличить. Это позволило предотвратить надвигающееся банкротство и построить бизнес,
который дожил до наших дней.
Веджвуд был не одинок. Индустриализация радикально изменила
экономическую ситуацию в Европе и Америке. Как следствие, все
больше и больше фирм начали использовать эту практику для управления денежными потоками.
Вот что писал в 1795 году Иоганн Вольфганг фон Гете в своем романе
«Годы учения Вильгельма Мейстера»:
— Отбрось ее, швырни в огонь! — возопил Вернер. — Идея ее отнюдь
не похвальна; этот опус претил мне с самого начала, а на тебя навлек неодобрение отца. Стихи, может, и складные, но изображение
фальшивое. До сих пор помню твою дряхлую, немощную колдунью — олицетворение ремесла. Верно, ты набрел на этот образ
в какой-нибудь убогой мелочной лавчонке. О торговом деле ты тогда
понятия не имел; я же не знаю человека, чей кругозор был бы шире,
должен быть шире, нежели кругозор настоящего коммерсанта.
Сколь многому учит нас порядок в ведении дел! Он позволяет нам
в любое время обозреть целое, не отвлекаясь на возню с мелочами.
Какие преимущества дает купцу двойная бухгалтерия! Это одно из
прекраснейших изобретений ума человеческого, и всякому хорошему
хозяину следует ввести ее в свой обиход.
Сегодня двойная запись используется практически во всех странах.
Эта практика в значительной степени лежит в основе профессии
бухгалтера.

47

Часть I. Принятые практики

Но обратите внимание, какими словами Гете описывает так ненавистные ему средства «коммерции»:
До сих пор помню твою дряхлую, немощную колдунью — олицетворение ремесла. Верно, ты набрел на этот образ в какой-нибудь
убогой мелочной лавчонке.
Вы когда-нибудь видели код, подходящий под это описание? Уверен,
что да. У меня тоже есть такой опыт. И если вы работаете так же долго,
как я, то вам явно доводилось в изобилии наблюдать такой код. И так
же как я, вы написали огромное количество такого кода.
Теперь еще раз обратимся к словам Гете:
Сколь многому учит нас порядок в ведении дел! Он позволяет нам
в любое время обозреть целое, не отвлекаясь на возню с мелочами.
Примечательно, что этими словами Гете описывает огромное преимущество, которое дает простая практика двойной записи.

Программное обеспечение
Для ведения современного бизнеса жизненно необходимо содержать
счета в порядке, а для этого нужна практика двойной записи. Но разве
надлежащее обслуживание программного обеспечения менее важно?
Ведь в XXI веке программное обеспечение лежит в основе любого
бизнеса.
Какая практика позволит разработчикам получить четкое представление о создаваемом ПО и контроль над ним, причем в той же степени,
какую практика двойной записи дает бухгалтерам и менеджерам? Возможно, вы считаете, что разработка программного обеспечения и бухгалтерия относятся к настолько разным сферам, что их невозможно, да
и незачем сопоставлять друг с другом. Позволю себе не согласиться.
Бухгалтерский учет сродни магическому искусству. Человек, несведущий в его ритуалах, мало что понимает в работе бухгалтера. Что
является результатом этой работы? Набор документов, систематизи-

48

Глава 2. Разработка через тестирование

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

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

49

Часть I. Принятые практики

2. Написание окончательной версии кода, достаточно несвязанной,
чтобы этот код можно было тестировать и подвергать рефакторингу.
3. Формирование чрезвычайно короткого цикла обратной связи,
который позволяет поддерживать стабильный ритм и производительность при написании программы.
4. Создание тестов и производственного кода, которые не связаны
в такой степени, что их удобно обслуживать и ничто не препятствует репликации вносимых в них изменений.
Практика TDD сводится к соблюдению трех законов, практически
полностью состоящих из произвольной части. Отсутствие обязательной части подтверждает, например, тот факт, что конечная цель может
достигаться разными средствами. В частности, с помощью предложенной Кентом Беком практики test && commit || revert (TCR). Несмотря
на свою непохожесть на TDD, TCR позволяет получить точно такие
же результаты.
Следовать трем законам, составляющим основу TDD, очень тяжело,
особенно поначалу. Для этого нужны навыки и знания, получить которые не так-то просто. Но без них попытки следовать законам практически всегда заканчиваются разочарованием и отказом от практики.
Постепенно я дам вам всю необходимую информацию, а пока просто
будьте готовы к внутреннему сопротивлению при изучении этого
материала. Так и должно быть.

Первый закон
Не пишите код, пока не напишете первый тест.
Опытному программисту этот закон может показаться глупым. У вас
тоже, скорее всего, возник вопрос, как написать тест без кода, который
следует протестировать. Это естественная реакция, ведь считается, что
сначала пишется код, а только потом тесты для него. Но если вдуматься, то само знание того, как должен работать код, позволяет написать
для него тест. Хотя, конечно, эта идея контринтуитивна и кажется
совершенно нелогичной.

50

Глава 2. Разработка через тестирование

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

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

ходит;
zz вы пишете строку производственного кода, позволяющую пройти

тест;
zz вы пишете следующую строку тестового кода, и тест снова не про-

ходит;
zz вы пишете следующий рабочий код, позволяющий пройти тест;
zz вы пишете еще одну-две строки тестового кода и обнаруживаете,

что проверка утверждения не пройдена;
zz вы пишете еще одну-две строки производственного кода, обеспечи-

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

51

Часть I. Принятые практики

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

Представьте команду разработчиков, следующую трем вышеупомянутым законам. О любом из них в любой момент времени можно сказать,
что код, который он пишет, совсем недавно успешно прошел все тесты.
А теперь попробуйте представить, как меняется жизнь разработчика,
о коде которого можно сказать, что минуту назад он успешно прошел
тестирование. Как вы думаете, насколько большой отладки требует его
код? Очевидно, что в какой-то особой отладке такой код не нуждается.
Вы хорошо знакомы с работой отладчика? В любой момент готовы
пометить функцию как отлаживаемую? Создали для себя множество
горячих клавиш? Привычно расставляете точки останова и точки наблюдения и с головой погружаетесь в процесс отладки?
Это совершенно ненужные навыки!
Дело в том, что отлично освоить отладчик можно только в процессе
его интенсивного использования. Но тратить много времени на отладку нерационально. Целесообразнее писать работающий код, а не
исправлять неработающий.
Мне бы хотелось, чтобы вы настолько редко прибегали к отладчику, что забыли бы назначение горячих клавиш. Я хочу, чтобы, на-

52

Глава 2. Разработка через тестирование

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

Если вы когда-либо интегрировали в систему пакеты сторонних
производителей, то знаете, что в составе каждого такого пакета есть
PDF-файл, созданный техническим писателем. Это файл с описанием
процедуры интеграции. В конце документа почти всегда можно найти
примеры кода.
Разумеется, первым делом вы заглядываете именно в конец. Зачем читать, что написал о коде технический писатель, если можно посмотреть
сам код. Код скажет вам гораздо больше, чем все, что о нем написано.
Если повезет, то его даже удастся скопировать в свое приложение
и заставить там работать.
Разработчик, который соблюдает три закона, пишет примеры кода для
всей системы. Ведь создаваемые тесты объясняют каждую мельчайшую деталь функционирования. Если вы хотите узнать, как создать
определенный объект, тесты покажут все возможные способы его
создания. А чтобы понять, как вызвать определенную функцию API,
опять же нужно обратиться к тестам, благо они демонстрируют как
саму функцию, так и все ее потенциальные ошибки и исключения.
В наборе тестов есть все, что требуется для знакомства с деталями
системы.
Тесты из этого набора представляют собой документацию, описывающую всю систему на самом низком уровне. И это документация на
хорошо понятном вам языке. Она полностью непротиворечива. Она

53

Часть I. Принятые практики

создана настолько в соответствии с законами, что работает. Она не
может рассинхронизироваться с системой.
Тесты — это почти идеальная документация.
Нет, преувеличивать их достоинства я не хочу. Цель и назначение
системы тесты описывают не очень хорошо. Для этого существует
документация высокого уровня. Однако на низком уровне они лучше
любого документа, который можно было бы написать. Потому что это
код. А он показывает, как все обстоит на самом деле.
Если вы боитесь, что тесты так же сложны для понимания, как и система в целом, то это не так. Каждый тест представляет собой небольшой фрагмент кода, сфокусированный на какой-то очень узкой части
системы. Сами по себе тесты систему не образуют. Друг о друге тесты
не знают, так что вам не придется иметь дело с запутанным нагромождением зависимостей. Каждый тест понятен сам по себе и сообщает
вам именно то, что нужно узнать в очень узкой части системы.
Разумеется, некоторые люди пишут непрозрачные и хитросплетенные
тесты, в которых не так-то просто разобраться. Поэтому одна из целей
моей книги — научить вас писать тесты, представляющие собой четкое
и понятное описание системы, лежащей в их основе.
Дыры в покрытии кода

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

54

Глава 2. Разработка через тестирование

в ситуацию, когда написание теста возможно только после внесения
изменений в проект.
Но такие шаги требуют огромного приложения сил и отнимают
массу времени. Более того, в процессе редактирования в код могут
вкрасться ошибки. А мы-то уже знаем, что все работает, так как
проверили вручную. Поэтому самым логичным выходом из этой
трудной ситуации видится отказ от написания теста. Не говорите
мне, будто никогда так не поступали. Я практически уверен, что
такой опыт у вас есть.
Понятное дело, что так поступали не только вы, но и другие члены
вашей рабочей группы, поэтому готовый набор тестов полон дыр.
Примерно представить количество этих дыр можно по громкости
и продолжительности смеха программистов в процессе тестирования.
Чем громче смех, тем меньше покрытие кода.
Очевидно, что пользы от такого набора тестов немного. Он может показать места сбоев и дать представление о том, что некоторые вещи
работают.
Но хороший набор тестов отличается полным покрытием и позволяет
уверенно рекомендовать развертывание системы. Если ваш набор
тестов не внушает такого же доверия, то какой от него прок?
Приятные эмоции

Соблюдение трех законов вызывает совсем другие эмоции, чем нудное написание тестов. Прежде всего, это весело. Нет, конечно, не так
весело, как бывает, когда ты сорвал джекпот в Вегасе. И не так весело,
как поход на вечеринку или настольные игры с ребенком. Пожалуй,
весело не совсем подходящее в данном случае слово.
Вспомните, что вы чувствовали, когда заработала ваша первая программа. Возможно, это произошло в местном компьютерном клубе,
где стоял TRS-80 или Commodore 64. Возможно, вы написали легкомысленный бесконечный цикл, который снова и снова выводил на
экран ваше имя, и ушли домой с легкой улыбкой на лице, чувствуя
себя хозяином Вселенной, которому со временем будут подчиняться
все компьютеры.

55

Часть I. Принятые практики

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

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

Итак, соблюдение трех законов TDD дает следующие преимущества:
zz тратится больше времени на написание работающего кода и мень-

ше — на отладку неработающего кода;
zz попутно генерируется практически идеальная низкоуровневая

документация;
zz это приносит приятные эмоции или, по крайней мере, вдохновляет;
zz создаваемый набор тестов позволяет уверенно выполнить раз-

вертывание;
zz проекты становятся менее связанными.

56

Глава 2. Разработка через тестирование

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

Программировать непросто. Возможно, это самый трудный из всех
навыков, в которых люди пытаются достичь мастерства. Сегодня наша
цивилизация зависит от сотен тысяч взаимосвязанных приложений,
каждое из которых состоит из сотен тысяч, если не десятков миллионов строк кода. Ни один из созданных людьми аппаратов больше не
имеет такого количества подвижных деталей.
Каждое из приложений поддерживается командами разработчиков,
которые до смерти боятся что-либо менять. Ирония в том, что все
это программное обеспечение написано, чтобы позволить нам легко
менять поведение машин.
Но разработчики знают, что каждое изменение сопряжено с риском
сбоя, причину которого бывает сложно обнаружить и устранить.
Представьте ужасно запутанный код. Скорее всего, вам не придется
напрягаться, чтобы вызвать в воображении такой образ. Большинство
из нас сталкивается с подобным каждый день.
Готов поклясться: при виде такого кода вы иногда начинаете испытывать желание привести его в порядок. Но я практически уверен, что
следом, как молот Тора, обрушивается мысль: «Я к этому не прикоснусь!» Ведь вы прекрасно знаете: если, прикоснувшись к этому коду,
вы что-то сломаете, то он станет вашим навсегда.
И вы испытываете страх. Вы боитесь кода, поддержкой которого занимаетесь. Вас пугают последствия в виде возможного сбоя.
В результате код начинает деградировать. Никто не будет приводить его в порядок. Никто не хочет его улучшать. Если какие-то
изменения неизбежны, то они делаются способом, не наиболее подходящим для системы, а наиболее безопасным для программиста.

57

Часть I. Принятые практики

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

Представьте набор тестов, которому вы настолько доверяете, что готовы рекомендовать развертывание системы, прошедшей эти тесты.
Более того, процесс тестирования занимает считаные секунды. В этом
случае будет ли вам страшно даже подумать об аккуратной очистке
системы?
Снова представьте на экране своего компьютера ужасно запутанный
код. Что помешает вам начать приводить его в порядок? У вас же
есть тесты, которые сообщат о любой неисправности в момент ее возникновения.
С помощью этого набора тестов вы можете безопасно очистить код.
С помощью этого набора тестов вы можете безопасно очистить код.
С помощью этого набора тестов вы можете безопасно очистить код.
Нет, это не опечатка. Просто я очень хотел, чтобы вы прочувствовали
этот момент. С помощью этого набора тестов вы можете безопасно
очистить код!
А раз это безопасно, вы, без сомнения, сделаете это. Аналогичным
образом поступят остальные члены команды. Потому что беспорядок
никто не любит.
Правило туриста

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

58

Глава 2. Разработка через тестирование

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

Сохранение кода в порядке. Его постоянная очистка. Именно поэтому
мы практикуем TDD. Потому что мы хотим гордиться своей работой.
Чтобы при взгляде на код сразу было понятно, что он чистый. Чтобы
мы знали, что каждое прикосновение к нему делает его лучше, чем
он был. И чтобы, глядя в зеркало перед сном, мы могли улыбнуться,
осознавая, что сегодня хорошо поработали.

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

59

Часть I. Принятые практики

Не стоит обманывать себя. Невозможно все делать одинаково хорошо. Сама по себе задача реализовать желаемое поведение кода уже
достаточно сложна. И в разы труднее сделать так, чтобы этот код еще
и имел правильную структуру. Здесь нам на помощь приходит совет
Кента Бека:
Сначала заставьте его работать, затем перепишите его
правильно.
В результате к трем законам TDD добавляется еще один: рефакторинг.
То есть сначала вы пишете небольшое количество тестового кода, затем — небольшой код, проходящий этот тест, а после этого приводите
весь написанный код в порядок. Наш светофор начинает выглядеть
так, как показано на рис. 2.1.

Рис. 2.1. Новый вид рабочего цикла

Скорее всего, вы уже имеете представление о том, что такое рефакторинг. Как я уже говорил, мы подробно разберем его в следующих ­главах. А пока позвольте развеять несколько мифов и заблуждений.
zz Рефакторинг выполняется постоянно. В каждом цикле TDD про-

исходит очистка кода.
zz Рефакторинг не меняет поведение. Он проводится только в случа-

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

60

Глава 2. Разработка через тестирование

zz Рефакторинг никогда не фигурирует в расписании или плане.

На него не нужно отдельно выделять время или спрашивать разрешения. Вы просто все время его делаете.
Рефакторинг имеет смысл воспринимать примерно так же, как мытье
рук после туалета. Действие, которое принято выполнять из соображений приличия.

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

Простые примеры
Скорее всего, встречая подобные примеры, вы не воспринимали их
всерьез, ведь они иллюстрируют слишком маленькие и слишком простые проблемы. Вы можете подумать, что TDD работает для таких вот
«детских» задач, но вряд ли подойдет для сложных систем. И серьезно
ошибетесь.
Основная цель любого хорошего разработчика программного обес­
печения — разбить большие и сложные системы на набор небольших
простых задач. Программисты вообще разбивают эти системы на
отдельные строки кода. Так что приведенные ниже примеры вполне
подходят для иллюстрации TDD вне зависимости от размеров проекта.
Я могу это подтвердить. Мне приходилось работать над большими
системами, которые были построены с помощью разработки через

61

Часть I. Принятые практики

тестирование, в связи с чем по собственному опыту могу сказать вам,
что ритм и методы TDD не зависят от масштабов проекта. Размер
в данном случае не имеет значения.
Вернее, он никак не влияет на процедуры и ритм. А вот на скорость
и связность тестов — очень сильно. Но об этом мы будем говорить
в следующих главах.

Стек
Видео для просмотра: Stack.
Для доступа к видео зарегистрируйтесь на сайте https://learning.oreilly.com/
videos/clean-craftsmanship-disciplines/9780137676385/.
Начнем с очень простой задачи: создание стека целых чисел. В процессе решения обратите внимание на тот факт, что тесты дают ответ на
любые вопросы о поведении стека. Таким образом становится видна
ценность тестов в качестве документации. Обратите также внимание,
что мы немного жульничаем, подставляя для прохождения теста абсолютные значения. В TDD это обычная стратегия, смысл которой
я объясню немного позже.
Итак, начнем:
// T: 00:00 StackTest.java
package stack;
import org.junit.Test;
public class StackTest {
@Test
public void nothing() throws Exception {
}
}

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

62

Глава 2. Разработка через тестирование

Ответить на него очень просто. Предположим, нам уже известен
код, который мы собираемся написать: public class stack. Здесь мы
вспоминаем первый закон и пишем тест, который будет успешно проходить такой код.
Правило 1. Напишите тест для проверки кода, который вы собираетесь написать.
Это первое из многих правил. Все эти правила, по большому счету,
эвристические. Они больше напоминают небольшие советы, которые
я буду время от времени давать в процессе рассмотрения примеров.
Правило 1 — не высшая математика. Ничего сложного для понимания
в нем нет. Если можно написать строку кода, то можно написать и тест,
который будет ее проверять. И ничто не мешает написать его первым.
Так и сделаем:
// T:00:44 StackTest.java
public class StackTest {
@Test
public void canCreateStack() throws Exception {
MyStack stack = new MyStack();
}
}

Полужирным шрифтом я выделяю изменения или добавления, показывая таким способом фрагменты, из-за которых код не компилируется. Я назвал переменную MyStack, так как название Stack в языке
Java уже зарезервировано и используется в качестве ключевого слова.
Обратите внимание, что во фрагменте кода я дал тесту более описательное название. Теперь в соответствии со вторым законом нужно
создать стек, поскольку без этого код MyStack компилироваться не
будет. При этом нужно придерживаться третьего правила: не писать
больше, чем требуется для прохождения теста:
// T: 00:54 Stack.java
package stack;
public class MyStack {
}

63

Часть I. Принятые практики

Прошло всего десять секунд, а наш тест уже компилируется и проходит. Причем большая часть из этих секунд ушла на перестановку окон
на экране, как показано на рис. 2.2. Это было сделано, чтобы можно
было видеть оба файла одновременно. В левом окне происходит тестирование, а справа располагается рабочий код.

Рис. 2.2. Новый вид экрана

Имя MyStack — не самый лучший выбор, но с его помощью мы избежали конфликта имен. Теперь, когда оно объявлено в пакете stack,
изменим его обратно на Stack. У меня это заняло 15 секунд. Тест все
еще прекрасно проходит.
// T:01:09 StackTest.java
public class StackTest {
@Test
public void canCreateStack() throws Exception {
Stack stack = new Stack();
}
}
// T: 01:09 Stack.java

64

Глава 2. Разработка через тестирование

package stack;
public class Stack {
}

Здесь мы подошли к следующему правилу: красный → зеленый →
рефакторинг. Никогда не упускайте возможность навести порядок.
Правило 2. Сделайте так, чтобы тест перестал проходить.
Сделайте так, чтобы он снова начал проходить. Очистите код.
Написать работающий код достаточно сложно. Написать работающий
и чистый код еще сложнее. К счастью, выполнение этой задачи можно
разбить на два этапа. Сначала пишем работающий код, не обращая
внимания на его качество. Затем, благодаря наличию тестов, мы легко
можем почистить этот код, сохранив его работоспособность.
То есть на каждом витке цикла TDD мы пользуемся возможностью
навести порядок в созданном собственными руками беспорядке.
Вы могли заметить, что наш тест не утверждает никакого поведения.
Он компилируется и проходит, но не дает информации о созданном
нами стеке. Это можно исправить за 15 секунд:
// T: 01:24 StackTest.java
public class StackTest {
@Test
public void canCreateStack() throws Exception {
Stack stack = new Stack();
assertTrue(stack.isEmpty());
}
}

Здесь вступает в дело второй закон, и нам нужно скомпилировать
этот код:
// T: 01:49
import static junit.framework.TestCase.assertTrue;
public class StackTest {
@Test
public void canCreateStack() throws Exception {
Stack stack = new Stack();

65

Часть I. Принятые практики

}

}

assertTrue(stack.isEmpty());

// T: 01:49 Stack.java
public class Stack {
public boolean isEmpty() {
return false;
}
}

Двадцать пять секунд спустя тест компилируется, но проваливается.
Это сделано преднамеренно. Я специально добавил утверждение
isEmpty, возвращающее значение false, поскольку, согласно первому
закону, тестирование должно окончиться неудачей. Зачем это нужно?
Чтобы убедиться, что в ситуациях, когда тест не должен проходить,
все так и есть. И мы наполовину проверили, как он работает. Проверим вторую половину, изменив утверждение isEmpty таким образом,
чтобы оно возвращало значение true:
// T: 01:58 Stack.java
public class Stack {
public boolean isEmpty() {
return true;
}
}

Все, теперь тест проходит. Мне потребовалось всего 9 секунд, чтобы
убедиться, что тест функционирует как нужно.
Как правило, когда программисты сначала видят значение false ,
а потом true , они смеются, поскольку происходящее напоминает
какие-то странные уловки. На самом же деле это всего лишь проверка
функционирования теста. Если мы можем убедиться, что там, где он
должен, он проходит, а там, где не должен, проваливается, то почему
этого не сделать?
Что дальше? Мне нужно добавить функцию push, поэтому в соответствии с правилом 1 я напишу тест, который будет проверять ее работу:
// T 02:24 StackTest.java
@Test
public void canPush() throws Exception {

66

Глава 2. Разработка через тестирование

}

Stack stack = new Stack();
stack.push(0);

Тест не компилируется, значит, согласно второму закону нужно написать код, который заставит его компилироваться:
// T: 02:31 Stack.java
public void push(int element) {
}

Теперь тест компилируется, но в нем отсутствует утверждение. Поэтому нужно добавить предикат, что после однократного применения
метода push стек перестает быть пустым:
// T: 02:54 StackTest.java
@Test
public void canPush() throws Exception {
Stack stack = new Stack();
stack.push(0);
assertFalse(stack.isEmpty());
}

Разумеется, такой тест не пройдет, поскольку метод isEmpty возвращает значение true. Нужна более интеллектуальная проверка, например,
добавим для отслеживания пустоты стека логический флаг:
// T: 03:46 Stack.java
public class Stack {
private boolean empty = true;

}

public boolean isEmpty() {
return empty;
}
public void push(int element) {
empty=false;
}

Этот тест уже проходит. Отмечу, что с момента прохождения предыдущего теста прошло 2 минуты. В соответствии с правилом 2 пришло
время заняться очисткой кода. У нас дублируется код создания стека,
поэтому превратим стек в поле класса и инициализируем его:

67

Часть I. Принятые практики

// T: 04:24 StackTest.java
public class StackTest {
private Stack stack = new Stack();
@Test
public void canCreateStack() throws Exception {
assertTrue(stack.isEmpty());
}

}

@Test
public void canPush() throws Exception {
stack.push(0);
assertFalse(stack.isEmpty());
}

Эта операция заняла 30 секунд, и теперь тест благополучно проходит.
Но мне не совсем нравится его имя canPush, я предпочитаю его поменять.
// T: 04:50 StackTest.java
@Test
public void afterOnePush_isNotEmpty() throws Exception {
stack.push(0);
assertFalse(stack.isEmpty());
}

Так он выглядит лучше и, конечно же, все еще продолжает проходить.
Теперь в соответствии с первым законом добавим еще одну проверку.
Если протолкнуть в стек один элемент и тут же его вытолкнуть, то
стек должен опустеть:
// T: 05:17 StackTest.java
@Test
public void afterOnePushAndOnePop_isEmpty() throws Exception {
stack.push(0);
stack.pop()
}

Исправленный код перестал компилироваться, поэтому действуем
в соответствии с первым законом:
// T: 05:31 Stack.java
public int pop() {

68

Глава 2. Разработка через тестирование

}

return -1;

Третий закон позволит нам закончить тест:
// T: 05:51
@Test
public void afterOnePushAndOnePop_isEmpty() throws Exception {
stack.push(0);
stack.pop();
assertTrue(stack.isEmpty());
}

Тест провален, поскольку флаг empty так и остается со значением true.
Исправим эту недоработку:
// T: 06:06 Stack.java
public int pop() {
empty=true;
return -1;
}

Тестирование благополучно завершено. С момента предыдущего теста
прошло 76 секунд.
Очистка тут не требуется, поэтому действуем в соответствии со вторым законом. После двух применений метода push размер стека должен стать равным 2:
// T: 06:48 StackTest.java
@Test
public void afterTwoPushes_sizeIsTwo() throws Exception {
stack.push(0);
stack.push(0);
assertEquals(2, stack.getSize());
}

Ошибки компиляции заставляют действовать в соответствии со вторым законом. Но исправить эти ошибки очень легко. Добавим в производственный код инструкцию import, а также следующую функцию:
// T: 07:23 Stack.java
public int getSize() {
return 0;
}

69

Часть I. Принятые практики

Теперь все компилируется, но тест не проходит.
Разумеется, для прохождения теста достаточно тривиальной правки:
// T: 07:32 Stack.java
public int getSize() {
return 2;
}

Наши действия выглядят несколько нелепо, зато мы убедились, что
тест проваливается, когда должен это делать, и проходит, когда все
так, как нам нужно, причем процесс проверки занял всего 11 секунд.
Так что у нас не было причин от нее отказываться.
Но полученное решение более чем примитивно, поэтому согласно правилу 1 я поищу лучший вариант. Ну да, с первого раза не получилось
(можете надо мной посмеяться):
// T: 08:06 StackTest.java
@Test
public void afterOnePushAndOnePop_isEmpty() throws Exception {
stack.push(0);
stack.pop();
assertTrue(stack.isEmpty());
assertEquals(1, stack.getSize());
}

Признаю, это было действительно глупо. Но программисты время от
времени совершают глупые ошибки, я не исключение. При первом
написании примера я не сразу заметил эту ошибку, поскольку ожидал,
что тестирование провалится.
Зато сейчас я полностью уверен, что мои тесты хорошо работают,
так что можно внести в код изменения, при которых они пройдут
успешно:
// T: 08:56
public class Stack {
private boolean empty = true;
private int size = 0;
public boolean isEmpty() {
return size == 0;
}

70

Глава 2. Разработка через тестирование

public void push(int element) {
size++;
}
public int pop() {
--size;
return -1;
}

}

public int getSize() {
return size;
}

К моему изумлению, тестирование провалилось и на этот раз. К счастью, я быстро обнаружил ошибку и внес необходимые коррективы:
// T: 09:28 StackTest.java
@Test
public void afterOnePushAndOnePop_isEmpty() throws Exception {
stack.push(0);
stack.pop();
assertTrue(stack.isEmpty());
assertEquals(0, stack.getSize());
}

Все тесты проходят благополучно. С момента предыдущего тестирования прошло 3 минуты и 22 секунды.
Для полноты картины я решил добавить еще и проверку размера:
// T: 09:51 StackTest.java
@Test
public void afterOnePush_isNotEmpty() throws Exception {
stack.push(0);
assertFalse(stack.isEmpty());
assertEquals(1, stack.getSize());
}

Разумеется, тест был пройден.
Вернемся к первому закону. Что должно произойти при опустошении
стека? Следует ожидать исключения, информирующего о недостаточном наполнении буфера:

71

Часть I. Принятые практики

// T: 10:27 StackTest.java
@Test(expected = Stack.Underflow.class)
public void poppingEmptyStack_throwsUnderflow() {
}

Следуя второму закону, добавим это исключение:
// T: 10:36 Stack.java
public class Underflow extends RuntimeException {
}

В результате сможем выполнить такой тест:
// T: 10:50 StackTest.java
@Test(expected = Stack.Underflow.class)
public void poppingEmptyStack_throwsUnderflow() {
stack.pop();
}

Тест, разумеется, провалится, но это легко исправить:
// T: 11:18 Stack.java
public int pop() {
if (size == 0)
throw new Underflow();
--size;
return -1;
}

Тест проходит. С момента предыдущего тестирования прошла 1 минута и 27 секунд.
Снова начнем действовать в соответствии с первым законом. Стек должен помнить, что в него было добавлено. Проверим простейший случай:
// T: 11:49 StackTest.java
@Test
public void afterPushingX_willPopX() throws Exception {
stack.push(99);
assertEquals(99, stack.pop());
}

Тест провален, поскольку метод pop в настоящее время возвращает –1.
Для прохождения теста сделаем так, чтобы он возвращал 99:

72

Глава 2. Разработка через тестирование

// T: 11:57 Stack.java
public int pop() {
if (size == 0)
throw new Underflow();
--size;
return 99;
}

Этого явно недостаточно, поэтому в соответствии с правилом 1 добавим к тесту необходимый минимум кода, который сделает его немного умнее:
// T: 12:18 StackTest.java
@Test
public void afterPushingX_willPopX() throws Exception {
stack.push(99);
assertEquals(99, stack.pop());
stack.push(88);
assertEquals(88, stack.pop());
}

Такой тест провалится из-за возвращаемого значения 99. Чтобы обес­
печить его прохождение, добавим поле для записи последнего добавленного в стек значения:
// T: 12:50 Stack.java
public class Stack {
private int size = 0;
private int element;
public void push(int element) {
size++;
this.element = element;
}

}

public int pop() {
if (size == 0)
throw new Underflow();
--size;
return element;
}

Теперь тест проходит. С момента предыдущего тестирования прошло
92 секунды.

73

Часть I. Принятые практики

Подозреваю, что к этому моменту я вам изрядно надоел. Возможно,
вы мысленно кричите на меня: «Перестань маяться дурью и просто
напиши этот проклятый стек!» Но я всего лишь следую правилу 3.
Правило 3. Не гонитесь за золотом.
Любого новичка в TDD посещает огромное искушение первым делом
заняться сложными или интересными вещами. Например, в случае
написания стека так соблазнительно начать с тестирования поведения FILO (first-in-last-out, «первым пришел — последним вышел»).
Именно такой подход называется «погоней за золотом». Я думаю, вы
обратили внимание, что я намеренно избегал тестирования чего-либо,
напоминающего стек. Я сосредоточился на вспомогательных элементах, таких как пустота и размер.
Почему я не погнался за золотом? Зачем вообще придумали правило 3? Дело в том, что слишком рано погнавшись за золотом, вы, как
правило, упускаете множество деталей. Скоро я вам покажу, что вместе с этим вы упускаете и возможность упростить код.
Впрочем, сейчас в соответствии с первым законом нужно написать
неработающий тест. И самый очевидный кандидат для тестирования
на этом этапе — поведение FILO:
// T: 13:36 StackTest.java
@Test
public void afterPushingXandY_willPopYthenX() {
stack.push(99);
stack.push(88);
assertEquals(88, stack.pop());
assertEquals(99, stack.pop());
}

Тест проваливается, так как для его прохождения следует помнить
более одного значения. Наверное, в этом случае имеет смысл использовать массив. Поэтому я переименую поле в elements и превращу
его в массив:
// T: 13:51 Stack.java
public class Stack {
private int size = 0;
private int[] elements = new int[2];

74

Глава 2. Разработка через тестирование

public void push(int element) {
size++;
this.elements = element;
}

}

public int pop() {
if (size == 0)
throw new Underflow();
--size;
return elements;
}

По очереди рассмотрим ошибки компиляции этого кода. Переменной
elements в методе push не хватает квадратных скобок:
// T: 14:01 Stack.java
public void push(int element) {
size++;
this.elements[] = element;
}

В эти скобки нужно что-то поместить. Хм-м-м. В предыдущем фрагменте кода есть инкремент переменной size++:
// T: 14:07 Stack.java
public void push(int element) {
this.elements[size++] = element;
}

Метод pop также содержит переменную elements , нуждающуюся
в квадратных скобках:
// T: 14:13
public int pop() {
if (size == 0)
throw new Underflow();
--size;
return elements[];
}

О, смотрите! Сюда можно поместить декремент переменной --size:
// T: 14:24
public int pop() {

75

Часть I. Принятые практики

}

if (size == 0)
throw new Underflow();
return elements[--size];

Тесты снова стали проходить. С момента предыдущего тестирования
прошло 94 секунды.
На этом все. Разумеется, можно было бы сделать больше. Пока наш
стек содержит всего два элемента и не умеет обрабатывать ситуацию
переполнения, но я уже продемонстрировал вам все, что хотел. Поэтому оставляю эти усовершенствования вам в качестве упражнения.
Итак, создание с нуля стека целых чисел заняло у меня 14 минут
и 24 секунды. Ритм, который вы наблюдали, достаточно типичен.
Именно так ощущается разработка через тестирование, независимо
от масштаба проекта.
Упражнение
Реализуйте с помощью показанной выше методики очередь целых чисел, обрабатываемых по принципу «первым пришел — первым ушел».
Используйте для хранения массив фиксированного размера. Для
отслеживания мест добавления и удаления элементов вам, вероятно,
понадобятся два указателя. Завершив работу, вы можете обнаружить,
что реализовали циклический буфер.

Простые множители
Видео для просмотра: Prime Factors.
Для доступа к видео зарегистрируйтесь на сайте https://learning.oreilly.com/
videos/clean-craftsmanship-disciplines/9780137676385/.
Этот пример имеет небольшую предысторию. К 2002 году я уже пару
лет использовал TDD и изучал язык Ruby. Мой сын Джастин попросил меня помочь с домашним заданием. Требовалось найти простые
множители для набора целых чисел.
Я сказал Джастину, что будет лучше, если он попытается решить
задачу самостоятельно. Но пообещал написать программу, которая

76

Глава 2. Разработка через тестирование

проверит его работу. Джастин ушел в свою комнату, а я принялся
обдумывать алгоритм нахождения простых множителей.
Самый очевидный подход в данном случае — создать список простых
чисел с помощью решета Эратосфена, а затем проверить, подходят ли
они в качестве множителей. Я уже собирался писать код, когда мне
пришла в голову мысль: а что, если я просто начну писать тесты
и посмотрю, что получится?
Лучше всего, если вы сможете посмотреть видео, поскольку многие
нюансы попросту невозможно описать словами. На этот раз я не буду
останавливаться на временных метках и упоминать ошибки компиляции и прочие мелочи. Думаю, вы уже составили мнение об этих
аспектах и теперь можно просто показывать постепенный прогресс
тестов и кода.
Я начал решение задачи с наиболее очевидного и вырожденного случая. Тем более что именно так предписывало правило 4.
Правило 4. Пишите самый простой, самый конкретный, самый
вырожденный1 тест, который не будет пройден.
В нашем случае — это умножение на 1. И самый вырожденный тест,
который не будет пройден, — просто вернуть значение null.
public class PrimeFactorsTest {
@Test
public void factors() throws Exception {
assertThat(factorsOf(1), is(empty()));
}

}

private List factorsOf(int n) {
return null;
}

Обратите внимание, что тестируемую функцию я добавил в тестовый
класс. Обычно так не делают, но в данном случае это очень удобно,
1

Слово «вырожденный» здесь используется для обозначения простейшей отправной точки.

77

Часть I. Принятые практики

так как мне не придется все время переключаться между двумя исходными файлами.
Мой тест не проходит, но это легко исправить. Достаточно вернуть
пустой список:
private List factorsOf(int n) {
return new ArrayList();
}

Все. Теперь тест проходит. Следующий наиболее вырожденный случай: умножение на 2:
assertThat(factorsOf(2), contains(2));

Тест провален, но ситуацию снова несложно исправить. Именно
поэтому на начальном этапе нужно писать тесты для вырожденных
случаев: прохождение таких тестов почти всегда легко обеспечить.
private List factorsOf(int n) {
ArrayList factors = new ArrayList();
if (n>1)
factors.add(2);
return factors;
}

Если вы смотрели видео, то уже знаете, что это делается в два этапа.
Сначала я извлек new ArrayList() в переменную factors, а затем
добавил оператор if.
Я отдельно упоминаю об этом, поскольку первый шаг сделан в соответствии с правилом 5.
Правило 5. По возможности обобщайте.
Исходная константа new ArrayList() имеет особенности. Ее можно
поместить в переменную для последующих манипуляций. Это не очень
большое обобщение, но зачастую даже такого вполне достаточно.
Тесты при этом без проблем проходятся. А вот тестирование следующего вырожденного случая дало интересный результат:
assertThat(factorsOf(3), contains(3));

78

Глава 2. Разработка через тестирование

Тест провалился. Согласно правилу 5, следовало выполнить обобщение. И это несложное обобщение привело к прохождению теста.
Скорее всего, вы сейчас удивлены. Присмотритесь к коду, чтобы понять, что произошло.
private List factorsOf(int n) {
ArrayList factors = new ArrayList();
if (n>1)
factors.add(n);
return factors;
}

Я помню, как изумился факту, что изменение всего одного символа,
простое обобщение, привело к тому, что код прошел как новый тест,
так и все предыдущие.
В первый момент показалось, что я добился успеха, но следующий шаг
меня разочаровал. При всей очевидности следующего теста:
assertThat(factorsOf(4), contains(2, 2));

я не понимал, как написать его в общей форме. Я смог придумать
только проверку делимости n на 2, но общим решением это нельзя
было назвать. Тем не менее другого у меня не было:
private List factorsOf(int n) {
ArrayList factors = new ArrayList();
if (n>1) {
if (n%2 == 0) {
factors.add(2);
n /= 2;
}
factors.add(n);
}
return factors;
}

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

79

Часть I. Принятые практики

private List factorsOf(int n) {
ArrayList factors = new ArrayList();
if (n > 1) {
if (n % 2 == 0) {
factors.add(2);
n /= 2;
}
if (n > 1)
factors.add(n);
}
return factors;
}

Вы можете справедливо заметить, что я обеспечиваю прохождение
тестов, добавляя все новые условия if. По большому счету, вы правы.
Более того, вы можете обвинить меня еще и в нарушении правила 5,
ведь ни один из недавно добавленных фрагментов кода нельзя назвать универсальным. Однако на тот момент я просто не видел других
вариантов.
Впрочем, возможность обобщения все-таки есть. Обратите внимание на одинаковые предикаты двух операторов if . Как будто
перед нами фрагменты развалившегося цикла. Действительно, нет
причин, по которым второй оператор if должен находиться внутри
первого.
private List factorsOf(int n) {
ArrayList factors = new ArrayList();
if (n > 1) {
if (n % 2 == 0) {
factors.add(2);
n /= 2;
}
}
if (n > 1)
factors.add(n);
return factors;
}

В таком виде тест проходит. Как и следующие три:
assertThat(factorsOf(5), contains(5));
assertThat(factorsOf(6), contains(2,3));
assertThat(factorsOf(7), contains(7));

80

Глава 2. Разработка через тестирование

Это свидетельствовало, что я на правильном пути, и уменьшило мои
переживания из-за уродливых операторов if.
Следующий тест для вырожденного случая должен провалиться,
поскольку код решения просто не может поместить в список три
элемента:
assertThat(factorsOf(8), contains(2, 2, 2));

Но особым сюрпризом для меня стал способ, которым я добился прохождения теста. В соответствии с правилом 5 я заменил оператор if
циклом while:
private List factorsOf(int n) {
ArrayList factors = new ArrayList();
if (n > 1) {
while (n % 2 == 0) {
factors.add(2);
n /= 2;
}
}
if (n > 1)
factors.add(n);
return factors;
}

Помню, как сидел перед компьютером и восхищался. Мне казалось,
что произошло нечто особенное. В чем дело, я понял позже и сейчас
поделюсь с вами. Сработало правило 5. Оказывается, цикл while —
это общая форма оператора if, а оператор if — вырожденная форма
цикла while.
Следующий тест, для множителя 9, также должен завершиться неудачно, поскольку наше решение никак не учитывает тройки:
assertThat(factorsOf(9), contains(3, 3));

Для прохождения теста следует устранить данную недоработку. Я сделал это следующим образом:
private List factorsOf(int n) {
ArrayList factors = new ArrayList();
if (n > 1) {

81

Часть I. Принятые практики

while (n % 2 == 0) {
factors.add(2);
n /= 2;
}
while (n % 3 == 0) {
factors.add(3);
n /= 3;
}

}

}
if (n > 1)
factors.add(n);
return factors;

Получилось ужасно. Это не только грубое нарушение правила 5, но
и огромное дублирование кода. Даже не знаю, что из этого хуже!
И здесь начинает действовать принцип обобщения:
По мере того как тесты становятся более конкретными, код
становится более универсальным.
Добавление каждого нового теста делает набор тестов более конкретным. Каждое применение правила 5 делает код решения более общим.
Я еще вернусь к этому принципу. Он критически важен для проектирования тестов и для предотвращения их нестабильности.
Убрать дублирование и устранить нарушение правила 5 можно, поместив исходный код разложения на множители внутрь цикла:
private List factorsOf(int n) {
ArrayList factors = new ArrayList();
int divisor = 2;
while (n > 1) {
while (n % divisor == 0) {
factors.add(divisor);
n /= divisor;
}
divisor++;
}
if (n > 1)
factors.add(n);
return factors;
}

82

Глава 2. Разработка через тестирование

В видео вы могли заметить, что это делалось в несколько этапов. Первым делом три двойки были извлечены в переменную divisor. Следующим шагом стало введение инкремента divisor++. Затем я перенес
инициализацию переменной divisor выше оператора if. И наконец,
заменил if на цикл while.
И снова этот переход if –> while. Заметили, что предикат исходного
оператора if стал предикатом внешнего цикла while? Мне это показалось удивительным. В этом есть что-то от наследования. Как будто
существо, которое я попытался создать, постепенно эволюционировало путем череды крошечных мутаций.
Теперь оператор if внизу попросту не нужен. Цикл завершается только при n = 1. А именно это условие проверял нижний оператор if для
завершения моего состоящего из двух частей цикла!
private List factorsOf(int n) {
ArrayList factors = new ArrayList();
int divisor = 2;
while (n > 1) {
while (n % divisor == 0) {
factors.add(divisor);
n /= divisor;
}
divisor++;
}
}

return factors;

После небольшого рефакторинга получим:
private List factorsOf(int n) {
ArrayList factors = new ArrayList();
for (int divisor = 2; n > 1; divisor++)
for (; n % divisor == 0; n /= divisor)
factors.add(divisor);
}

return factors;

Готово! В видео после этого я добавляю еще один тест, проверяющий
достаточность алгоритма.

83

Часть I. Принятые практики

Помню, как я четко увидел структуру этого алгоритма и задался вопросом: откуда он взялся и как работает?
Очевидно, что породил его я. В конце концов, именно мои пальцы
бегали по клавиатуре. Но это был совсем не тот алгоритм, который
я хотел использовать изначально. Куда делось решето Эратосфена?
Где список простых чисел? Ничего подобного в коде не было!
Хуже того, я не понимал, почему алгоритм работает. Меня поразило,
что я могу создать алгоритм, не понимая принципа его действия. Пришлось потратить некоторое время на его изучение, чтобы осознать, что
произошло. Меня ставил в тупик инкрементный счетчик divisor++
внешнего цикла, который гарантировал проверку как множителей
всех целых чисел, включая составные! Например, для целого числа 12
проверялось, является ли 4 множителем. Почему в списке отсутствовало значение 4?
Ответ на этот вопрос можно найти, если обратить внимание на порядок выполнения. К моменту, когда счетчик достигал значения 4, из
переменной n уже удалялись все двойки. И если вдуматься, это все то
же решето Эратосфена, просто в другой, необычной форме.
Суть в том, что я вывел этот алгоритм, по очереди тестируя частные
случаи. Я не продумывал его заранее. Приступая к работе, я понятия
не имел, как он будет выглядеть. Казалось, что он сам собой сгенерировался на моих глазах. Это действительно напоминало эмбрион, шаг
за шагом эволюционирующий во все более сложный организм.
Даже сейчас, вглядевшись в код, можно увидеть скромное начало. Это
и остатки первого оператора if, и фрагменты остальных изменений.
Как хлебные крошки, указывающие путь.
Нам осталась волнующая перспектива. Возможно, TDD — универсальный метод постепенного построения алгоритмов. Может быть,
правильно упорядоченный набор тестов позволит использовать TDD
для пошагового детерминативного написания любой компьютерной
программы.
В 1936 году Алан Тьюринг и Алонзо Черч по отдельности доказали, что
не существует обобщенной процедуры, определяющей возможность на-

84

Глава 2. Разработка через тестирование

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

Игра в боулинг
В 1999 году мы с Бобом Коссом (Bob Koss) были на конференции по
C++. В свободное время нам пришла идея попрактиковать новую на
тот момент концепцию TDD. За основу было решено взять простую
задачу: подсчет очков при игре в боулинг.
Партия в боулинг состоит из десяти фреймов. В каждом из них игрок
может совершить два броска, за каждый из которых начисляются очки
по количеству сбитых кеглей. Если игрок сбивает все десять кеглей
в первом броске, это называется страйк. Если сбивает за два броска,
это называется спэр. Шар, свалившийся в желоб (рис. 2.3), вообще не
приносит очков.

Рис. 2.3. Печально известная ситуация в боулинге
1

Речь идет о «проблеме разрешимости» Гильберта. Давид Гильберт искал универсальные методы определения разрешимости произвольного диофантова уравнения. Оно представляет собой математическую функцию с целочисленными
входами и выходами. Компьютерная программа также представляет собой математическую функцию с целочисленными входами и выходами. Следовательно,
проблему Гильберта можно описать в терминах компьютерных программ.

85

Часть I. Принятые практики

Краткая формулировка правил подсчета очков выглядит так:
zz в случае страйка начисляется 10 очков за кегли, сбитые в этом

фрейме, плюс количество сбитых кеглей за следующие два броска;
zz если сбит спэр, то начисляется 10 очков плюс количество сбитых

следующим шаром кеглей;
zz в остальных случаях засчитывается количество кеглей, сбитых

двумя бросками.
На рис. 2.4 показана типичная таблица подсчета очков.

Рис. 2.4. Запись счета типичной игры

С первой попытки игрок сбил одну кеглю, со второй — еще четыре,
в сумме набрав 5 очков.
Во втором фрейме он сбил сначала четыре, а потом пять кеглей, что
дало ему 9 очков за фрейм и 14 в сумме.
В третьем фрейме он сбил сначала шесть, а затем четыре кегли (спэр).
Очки в этом случае нельзя сосчитать, пока игрок не начнет следующий фрейм.
В четвертом фрейме выбито пять кеглей. Теперь можно подсчитать
очки для предыдущего фрейма. За него игрок получит 15 очков, а его
общий счет станет равен 29.
Выбитый в четвертом фрейме спэр может быть подсчитан только после пятого фрейма, в котором игрок выбивает страйк. В результате за
четвертый фрейм он получает 20 очков, а всего 49.
Выбитый в пятом фрейме страйк не может быть засчитан, пока игрок
не бросит еще два шара. К сожалению, он выбивает 0 и 1, что приносит ему за пятый фрейм всего 11 очков. Общая сумма при этом
достигает 60.

86

Глава 2. Разработка через тестирование

Так продолжается до десятого, последнего фрейма. Здесь выбивается
спэр, что дает возможность бросить один дополнительный шар.
Теперь посмотрим на эту информацию с точки зрения объектно-ориентированного программирования. Какие классы и отношения вы
бы использовали для вычисления счета игры в боулинг? Сможете
нарисовать их средствами UML?1
Скорее всего, ваша диаграмма будет похожа на представленную на
рис. 2.5.

Рис. 2.5. UML-диаграмма подсчета очков в боулинге

Игра (Game) состоит из десяти фреймов (Frames). В каждом фрейме
один или два броска (Rolls), за исключением подкласса TenthFrame,
который наследует 1..2 и добавляет еще один бросок, получая 2..3.
Каждый объект Frame указывает на следующий объект Frame, так что
функция подсчета очков (score) в случае спэра или страйка может
заглядывать вперед.
В классе Game две функции. Функция roll вызывается при каждом
броске шара, и ей передается количество сбитых игроком кеглей.
Функция score вызывается после всех бросков и возвращает счет за
игру.

1

Унифицированный язык моделирования. Если вы не знакомы с UML, то не
волнуйтесь — это просто набор стрелок и прямоугольников.

87

Часть I. Принятые практики

Хорошая, простая объектно-ориентированная модель, код которой напи­
сать несложно. Будь у нас команда из четырех человек, работу можно
было бы разделить на четыре класса, а примерно через день встретиться
и объединить все эти классы, заставив их работать как целое.
А еще можно воспользоваться TDD. Если вы еще не посмотрели
видео, то сделайте это сейчас и переходите к дальнейшему чтению.
Посмотрите видео: Bowling Game.
Для доступа к видео зарегистрируйтесь на сайте https://learning.oreilly.com/
videos/clean-craftsmanship-disciplines/9780137676385/.
Начнем, как обычно, с теста, который ничего не делает, чтобы проверить возможность компиляции и выполнения. После этой проверки
тест удаляется:
public class BowlingTest {
@Test
public void nothing() throws Exception {
}
}

Далее добавим утверждение, что можем создать экземпляр класса
Game:
@Test
public void canCreateGame() throws Exception {
Game g = new Game();
}

Заставим этот код компилироваться и дадим нашей IDE указание
создать отсутствующий класс, обеспечив прохождение теста:
public class Game {
}

Теперь попробуем бросить шар:
@Test
public void canRoll() throws Exception {
Game g = new Game();
g.roll(0);
}

88

Глава 2. Разработка через тестирование

Для прохождения этого теста укажем IDE, что нужно создать функцию roll, аргумент которой будет носить значимое имя pins (кегли):
public class Game {
public void roll(int pins) {
}
}

Подозреваю, вы уже заскучали. Пока что ничего нового не происходит.
Но потерпите немного, скоро станет интересно. Тесты уже начали понемногу дублироваться. От дублирующегося кода нужно избавиться,
поэтому процедуру создания игры вынесем в функцию setup:
public class BowlingTest {
private Game g;

}

@Before
public void setUp() throws Exception {
g = new Game();
}

В результате первый тест перестает работать, поэтому мы его удаляем. Второй тест также становится бесполезным, поскольку в нем
отсутствуют утверждения. Его тоже можно удалить. Свою роль эти
тесты-ступеньки уже сыграли.
Цель некоторых тестов — заставить нас создавать классы, функции или другие необходимые структуры. Иногда такие тесты настолько вырождены, что ничего не утверждают или утверждают
нечто очень примитивное. Часто они со временем заменяются более
полными тестами и могут быть безопасно удалены. Такие тесты
называют тестами-ступеньками, так как они напоминают ступеньки лестницы, позволяющие постепенно поднимать сложность
до нужного уровня.
Теперь добавим утверждение, что мы можем подсчитать количество
очков. Правда, для этого игра сначала должна быть пройдена полностью. Напомню, что функцию score можно вызвать только после
броска последнего шара.

89

Часть I. Принятые практики

В соответствии с правилом 4 запустим самую простую и самую вырожденную игру, какую только можно придумать:
@Test
public void gutterGame() throws Exception {
for (int i=0; i 1) {
for (int firstIndex=0; firstIndex < list.size()-1; firstIndex++) {
int secondIndex = firstIndex + 1;
if (list.get(firstIndex) > list.get(secondIndex)) {
int first = list.get(firstIndex);
int second = list.get(secondIndex);
list.set(firstIndex, second);
list.set(secondIndex, first);
}
}
}
return list;
}

Можете сказать, к чему все идет? Большинство из вас, вероятно, да.
Для следующего провального теста возьмем случай с тремя элементами в обратном порядке:
assertEquals(asList(1, 2, 3), sort(asList(3, 2, 1)));

Провальный результат говорит сам за себя. Функция sort возвращает
значение [2, 1, 3]. Обратите внимание, что цифра 3 оказалась в конце
списка. Это хорошо! Но первые два элемента все равно идут не по порядку. Нетрудно понять, почему так произошло. Сначала тройка поменялась
местами с двойкой, а затем — с единицей. Но при этом двойка и единица
остались неупорядоченными. Их нужно еще раз поменять местами.
То есть для прохождения этого теста нужно поместить цикл сравнения
и перестановки в другой цикл, который постепенно будет уменьшать
длину сравниваемого массива. Наверное, это проще понять, посмотрев
на код:
private List sort(List list) {
if (list.size() > 1) {
for (int limit = list.size() - 1; limit > 0; limit--) {
for (int firstIndex = 0; firstIndex < limit; firstIndex++) {
int secondIndex = firstIndex + 1;
if (list.get(firstIndex) > list.get(secondIndex)) {
int first = list.get(firstIndex);

106

Глава 3. Дополнительные возможности TDD

}

}

int second = list.get(secondIndex);
list.set(firstIndex, second);
list.set(secondIndex, first);

}
}
return list;
}

В качестве завершающего штриха проведем более масштабное тестирование:
assertEquals(
asList(1, 1, 2, 3, 3, 3, 4, 5, 5, 5, 6, 7, 8, 9, 9, 9),
sort(asList(3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9,
3)));

Тест пройден, поэтому наш алгоритм сортировки можно считать готовым.
Откуда он взялся? Мы же не проектировали это заранее. Все произошло само собой путем принятия небольших решений, необходимых
для прохождения каждого следующего теста. Вуаля!
Что это за алгоритм? Разумеется, сортировка простыми обменами,
или пузырьковая сортировка, — один из худших возможных алгоритмов.
Так что, возможно, TDD — хороший способ пошаговой разработки
плохих алгоритмов.

СОРТИРОВКА 2
Сделаем еще одну попытку. На этот раз я пойду немного другим
путем. Снова рекомендую начать с просмотра видео и после этого
продолжить чтение.
Видео: SORT 2.
Для доступа к видео зарегистрируйтесь на сайте https://learning.oreilly.com/
videos/clean-craftsmanship-disciplines/9780137676385/.

107

Часть I. Принятые практики

Начнем, как и раньше, с тестов для самых вырожденных случаев
и кода, который их проходит:
public class SortTest {
@Test
public void testSort() throws Exception {
assertEquals(asList(), sort(asList()));
assertEquals(asList(1), sort(asList(1)));
assertEquals(asList(1, 2), sort(asList(1, 2)));
}

}

private List sort(List list) {
return list;
}

Опять возьмем два неупорядоченных элемента:
assertEquals(asList(1, 2), sort(asList(2, 1)));

Но если в первый раз мы сравнивали их и меняли местами непосредственно во входном списке, то теперь результаты сравнения будут
записываться в новый список:
private List sort(List list) {
if (list.size() second)
return asList(second, first);
else
return asList(first, second);
}
}

Здесь желательно остановиться и подумать. Впервые столкнувшись
с этим тестом, я беспечно написал решение, считая его единственно
возможным. Но ошибся. Как видите, есть и другой путь.
Действительно, время от времени для прохождения теста можно написать более одного варианта кода. Это как развилка на дороге. И нужно
понять, каким путем мы пойдем дальше.

108

Глава 3. Дополнительные возможности TDD

Посмотрим, как происходит выбор пути следования.
Очевидно, что дальше нужно протестировать три упорядоченных
элемента:
assertEquals(asList(1, 2, 3), sort(asList(1, 2, 3)));

Но в отличие от предыдущего случая, тест провалится. Причина неудачи в том, что вне зависимости от выбранного пути прохождения
код не может вернуть список, содержащий более двух элементов.
Но мы очень легко можем обеспечить прохождение вот такого теста:
private List sort(List list) {
if (list.size() second)
return asList(second, first);
else
return asList(first, second);
}
else {
return list;
}
}

Конечно, все это выглядит просто до примитивности, но следующий
тест — для трех элементов, из которых первые два идут не по порядку, — не оставляет от этой простоты камня на камне. Понятно, что этот
тест не будет пройден:
assertEquals(asList(1, 2, 3), sort(asList(2, 1, 3)));

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

109

Часть I. Принятые практики

Согласно этому закону, между двумя числами A и B возможны
только три отношения: A < B, A = B или A > B. Поэтому я произвольно выберу из списка элемент и посмотрю, как он соотносится
с другими.
Соответствующий код выглядит так:
else {
int first = list.get(0);
int middle = list.get(1);
int last = list.get(2);
List lessers = new ArrayList();
List greaters = new ArrayList();
if (first < middle)
lessers.add(first);
if (last < middle)
lessers.add(last);
if (first > middle)
greaters.add(first);
if (last > middle)
greaters.add(last);

}

List result = new ArrayList();
result.addAll(lessers);
result.add(middle);
result.addAll(greaters);
return result;

Не пугайтесь, а просто внимательно посмотрите, что делает этот код.
Сначала три значения извлекаются в три переменные: first, middle
и last. Это сделано для удобства, чтобы не засорять код кучей вызовов
list.get(x).
Затем создается список для элементов, которые меньше, чем middle,
и еще один список для элементов, которые больше, чем middle. Обратите внимание, что переменная middle в нашем списке представлена
в единственном экземпляре.
Далее с помощью четырех операторов if мы помещаем элементы
first и last в соответствующие списки.

110

Глава 3. Дополнительные возможности TDD

После этого остается создать список result, куда мы по очереди поместим значения из списка lessers, переменную middle и значения
из списка greaters.
Понимаю, что вам может не нравиться этот код. Мне он тоже не очень
нравится. Но он работает. Тест пройден.
Следующие два теста также проходят:
assertEquals(asList(1, 2, 3), sort(asList(1, 3, 2)));
assertEquals(asList(1, 2, 3), sort(asList(3, 2, 1)));

К этому моменту проверены четыре из шести возможных вариантов
для списка из трех уникальных элементов. Но проверка двух оставшихся [2,3,1] и [3,1,2] ожидаемым образом потерпит неудачу.
А теперь представим, что из-за нетерпения или по недосмотру мы
сразу перешли к тестированию списков с четырьмя элементами:
assertEquals(asList(1, 2, 3, 4), sort(asList(1, 2, 3, 4)));

Разумеется, тест закончится неудачей, поскольку текущее решение
предполагает не более трех элементов в списке. Перестанет работать
и упрощение в виде переменных first, middle и last. Более того, у вас
может появиться вопрос, почему переменная middle была выбрана
в качестве элемента 1. Почему она не может быть элементом 0?
Что ж, превратим последний тест в комментарий и сделаем переменную middle элементом 0:
int first = list.get(1);
int middle = list.get(0);
int last = list.get(2);

Сюрприз! Тест со списком [1,3,2] не проходит. Понимаете почему?
Раз переменная middle равна 1, то значения 3 и 2 добавляются в список greaters в неправильном порядке.
Получается, написанное решение уже умеет сортировать список из двух
элементов. А в списке greaters именно два элемента; соответственно,
чтобы пройти тест, достаточно вызвать для этого списка метод sort:

111

Часть I. Принятые практики

List result = new ArrayList();
result.addAll(lessers);
result.add(middle);
result.addAll(sort(greaters));
return result;

Теперь для значений [1,3,2] тест проходит, а вот для значений
[3,2,1] проваливается, поскольку в этом случае неупорядоченным
оказывается список lessers. Но это легко исправить:
List result = new ArrayList();
result.addAll(sort(lessers));
result.add(middle);
result.addAll(sort(greaters));
return result;

Как видите, прежде чем переходить к списку из четырех элементов,
стоило протестировать два оставшихся варианта с тремя элементами.
Правило 7. Полностью протестируйте текущий, более простой
случай и только потом переходите к более сложному.
Как бы то ни было, теперь нужно пройти тест для списка из четырех
элементов. Я в этот момент превратил тест из комментариев обратно
в код и увидел, что он провалился (здесь это не показано).
Текущий алгоритм сортировки трехэлементного списка можно обобщить, особенно теперь, когда переменная middle стала первым элементом. Для формирования списков lessers и greaters достаточно
применить фильтры:
else {
int middle = list.get(0);
List lessers =
list.stream().filter(x -> x x>middle).collect(toList());

}

112

List result = new ArrayList();
result.addAll(sort(lessers));
result.add(middle);
result.addAll(sort(greaters));
return result;

Глава 3. Дополнительные возможности TDD

Неудивительно, что теперь легко проходит и этот тест, и следующие
два:
assertEquals(asList(1, 2, 3, 4), sort(asList(2, 1, 3, 4)));
assertEquals(asList(1, 2, 3, 4), sort(asList(4, 3, 2, 1)));

Теперь неплохо бы подробнее изучить элемент middle. Представим,
что он не уникален:
assertEquals(asList(1, 1, 2, 3), sort(asList(1, 3, 1, 2)));

Тест провален. Это означает, что нужно перестать относиться к переменной middle как к чему-то особенному:
else {
int middle = list.get(0);
List middles = list.stream().filter(x -> x == middle).
collect(toList());
List lessers = list.stream().filter(x -> x x>middle).
collect(toList());

}

List result = new ArrayList();
result.addAll(sort(lessers));
result.addAll(middles);
result.addAll(sort(greaters));
return result;

Теперь тест проходит. Но вы помните, что располагалось выше else?
Сейчас я вам покажу:
if (list.size() second)
return asList(second, first);
else
return asList(first, second);
}

113

Часть I. Принятые практики

А нужно ли нам присваивание ==2? Нет. После его удаления все тесты
по-прежнему проходят.
Хорошо, а как насчет первого оператора if ? Он все еще нужен?
Вообще-то его можно поменять на кое-что получше. Я просто покажу
вам окончательный алгоритм:
private List sort(List list) {
List result = new ArrayList();
if (list.size() == 0)
return result;
else {
int middle = list.get(0);
List middles = list.stream().filter(x -> x == middle).
collect(toList());
List lessers = list.stream().filter(x -> x < middle).
collect(toList());
List greaters = list.stream().filter(x -> x > middle).
collect(toList());

}

}

result.addAll(sort(lessers));
result.addAll(middles);
result.addAll(sort(greaters));
return result;

У этого алгоритма есть название: быстрая сортировка. Это один из
лучших известных алгоритмов сортировки.
Насколько он лучше? На моем ноутбуке он провел сортировку массива из миллиона случайных целых чисел от нуля до миллиона за 1,5 секунды. Пузырьковая сортировка из предыдущего раздела справится
с этой задачей примерно за шесть месяцев.
Этот момент вызывает некоторое беспокойство. Для сортировки
списка с двумя неупорядоченными элементами нашлось два решения.
Одно привело меня к пузырьковой сортировке, другое — к быстрой
сортировке.

114

Глава 3. Дополнительные возможности TDD

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

МЕРТВАЯ ТОЧКА
Думаю, к этому моменту вы уже просмотрели достаточно видеороликов, чтобы составить представление о ритме TDD. С этого момента
видео уже не будет, дальше пойдет только текст.
Новички в TDD часто попадают в затруднительное положение. Они
пишут отличный тест, а затем обнаруживают, что его можно пройти,
только реализовав алгоритм целиком. Я называю это «достижением
мертвой точки».
Для выхода из мертвой точки нужно удалить последний тест и придумать что-то более простое.
Правило 8. Если для прохождения текущего теста требуется
написать слишком много кода, то удалите этот тест и напишите более простой, который будет легче пройти.
На занятиях я часто использую упражнение, часто приводящее учеников в мертвую точку. Более половины тех, кто пытается его проделать,
застревают и, что интересно, отступать не хотят.
Это старая добрая задача на перенос слов. В сплошной текст нужно
вставить знаки переноса строки таким образом, чтобы он поместился
в столбец шириной в N символов. При малейшей возможности разбивайте на отдельные слова.
Студентам предлагается написать такую функцию:
Wrapper.wrap(String s, int w);

115

Часть I. Принятые практики

В качестве входной строки я предлагаю начало Геттисбергской речи
Авраама Линкольна:
"Four score and seven years ago our fathers brought forth upon this
continent a new nation conceived in liberty and dedicated to the
proposition that all men are created equal"1

При желаемой ширине 30 символов вывод выглядит вот так:
====:====:====:====:====:====:
Four score and seven years ago
Our fathers brought forth upon
This continent a new nation
Conceived in liberty and
Dedicated to the proposition
That all men are created equal
====:====:====:====:====:====:

Как написать этот алгоритм путем разработки через тестирование?
Попробуем начать с такого теста:
public class WrapTest {
@Test
public void testWrap() throws Exception {
assertEquals("Four", wrap("Four", 7));
}

}

private String wrap(String s, int w) {
return null;
}

Сколько законов TDD я нарушил? Можете назвать их? Как бы то ни
было, продолжим. Обеспечить прохождение этого теста несложно:
private String wrap(String s, int w) {
return "Four";
}

1

116

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

Глава 3. Дополнительные возможности TDD

Следующий тест кажется довольно очевидным:
assertEquals("Four\nscore", wrap("Four score", 7));

Код, обеспечивающий его прохождение, тоже довольно очевиден:
private String wrap(String s, int w) {
return s.replace(" ", "\n");
}

Я просто заменил все пробелы знаками конца строки. Идеально.
Но прежде чем пойдем дальше, мы немного почистим этот код:
private void assertWrapped(String s, int width, String expected) {
assertEquals(expected, wrap(s, width));
}
@Test
public void testWrap() throws Exception {
assertWrapped("Four", 7, "Four");
assertWrapped("Four score", 7, "Four\nscore");
}

Теперь код выглядит лучше, и можно написать следующий провальный тест. Если просто следовать за текстом Геттисбергской речи, то
следующий тест будет выглядеть так:
assertWrapped("Four score and seven years ago our", 7,
"Four\nscore\nand\nseven\nyears\nago our");

Он и в самом деле провалится. Его можно даже немного усилить следующим образом:
assertWrapped("ago our", 7, "ago our");

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

117

Часть I. Принятые практики

тест можно только при условии, что мы сразу напишем очень большую
часть алгоритма переноса слов.
Нам остается только удалить один или несколько тестов и заменить
их более простыми, которые мы сможем проходить постепенно. Попробуем сделать это:
@Test
public void testWrap() throws Exception {
assertWrapped("", 1, "");
}
private String wrap(String s, int w) {
return "";
}

Вот теперь я написал действительно вырожденный тест, не так ли?
Я сделал это, следуя правилу, которым пренебрег ранее.
Теперь нужен еще один тест для вырожденного случая. Как насчет
вот такого?
assertWrapped("x", 1, "x");

Это довольно простой тест, прохождение которого можно легко обеспечить:
private String wrap(String s, int w) {
return s;
}

Снова тот же шаблон. Я прошел первый тест, возвращая вырожденную константу. А прохождение второго теста я обеспечил, возвращая
входные данные. Очень интересно. Какой вырожденный случай протестировать теперь?
assertWrapped("xx", 1, "x\nx");

Тест провален, поскольку мой код возвращает "xx". Однако обеспечить его прохождение несложно:
private String wrap(String s, int w) {
if (w >= s.length())

118

Глава 3. Дополнительные возможности TDD

return s;
else
return s.substring(0, w) + "\n" + s.substring(w);
}

Все легко получилось. Какой следующий вырожденный случай мы
будем тестировать?
assertWrapped("xx", 2, "xx");

На этот раз тест пройден. Хорошо. Тогда попробуем вот такой тест:
assertWrapped("xxx", 1, "x\nx\nx");

Тест провален. Здесь напрашивается какой-то цикл. Хотя подождите.
Есть более простой способ:
private String wrap(String s, int w) {
if (w >= s.length())
return s;
else
return s.substring(0, w) + "\n" + wrap(s.substring(w), w);
}

О рекурсии мы вспоминаем достаточно редко, не так ли? Возможно,
имеет смысл делать это чаще.
В тестах уже просматривается некий шаблон, как вы считаете? Там
пока нет ни слов, ни даже пробелов. Просто строка из символов x
и счетчик от 1 до размера этой строки. Следующий тест будет выглядеть так:
assertWrapped("xxx", 2, "xx\nx");

И мы его пройдем. Как и следующий тест:
assertWrapped("xxx", 3, "xxx");

Вероятно, дальше рассматривать этот шаблон уже нет смысла. Пришло время добавлять пробелы:
assertWrapped("x x", 1, "x\nx");

119

Часть I. Принятые практики

Тест провалится, поскольку код возвращает "x\n \nx". Ситуацию можно исправить, удалив все префиксные пробелы перед рекурсивным
вызовом метода wrap.
return s.substring(0, w) + "\n" + wrap(s.substring(w).trim(), w);

Теперь тест проходит. А у нас появился шаблон, в соответствии с которым мы напишем следующий тест:
assertWrapped("x x", 2, "x\nx");

Тест не проходит из-за пробела, завершающего первую подстроку.
От него можно избавиться с помощью еще одного вызова метода trim:
return s.substring(0, w).trim() + "\n" + wrap(s.substring(w).trim(), w);

Тест пройден. Следующий тест, построенный по этому шаблону, также
проходит:
assertWrapped("x x", 3, "x x");

Что дальше? Наверное, можно попробовать вот это:
assertWrapped("x
assertWrapped("x
assertWrapped("x
assertWrapped("x
assertWrapped("x

x
x
x
x
x

x",
x",
x",
x",
x",

1,
2,
3,
4,
5,

"x\nx\nx");
"x\nx\nx");
"x x\nx");
"x x\nx");
"x x x");

Все тесты без проблем проходят. Вероятно, нет особого смысла смотреть, что получится, если добавить четвертый x.
Лучше попробуем сделать вот так:
assertWrapped("xx xx", 1, "x\nx\nx\nx");

Тест пройден. Как и следующие два теста в последовательности:
assertWrapped("xx xx", 2, "xx\nxx");
assertWrapped("xx xx", 3, "xx\nxx");

А вот этот тест уже провалится:
assertWrapped("xx xx", 4, "xx\nxx");

120

Глава 3. Дополнительные возможности TDD

Причина неудачи в том, что код возвращает "xx x\nx". Ведь в нем
отсутствует пробел между двумя «словами». Где этот пробел? Перед
символом wth. Для его поиска нам нужно двигаться в обратном от
w направлении:
private String wrap(String s, int w) {
if (w >= s.length())
return s;
else {
int br = s.lastIndexOf(" ", w);
if (br == -1)
br = w;
return s.substring(0, br).trim() + "\n" +
wrap(s.substring(br).trim(), w);
}
}

Теперь тест проходит. У меня появилось чувство, что работа закончена. На всякий случай проведем еще несколько тестов:
assertWrapped("xx
assertWrapped("xx
assertWrapped("xx
assertWrapped("xx
assertWrapped("xx
assertWrapped("xx
assertWrapped("xx
assertWrapped("xx
assertWrapped("xx

xx", 5,
xx xx",
xx xx",
xx xx",
xx xx",
xx xx",
xx xx",
xx xx",
xx xx",

"xx xx");
1, "x\nx\nx\nx\nx\nx");
2, "xx\nxx\nxx");
3, "xx\nxx\nxx");
4, "xx\nxx\nxx");
5, "xx xx\nxx");
6, "xx xx\nxx");
7, "xx xx\nxx");
8, "xx xx xx");

Все эти тесты проходят. Думаю, теперь все. Попробуем Геттисбергскую речь с длиной строки 15:
Four score and
seven years ago
our fathers
brought forth
upon this
continent a new
nation
conceived in
liberty and
dedicated to
the proposition
that all men

121

Часть I. Принятые практики

are created
equal
That looks right.

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

НАСТРОЙ, ДЕЙСТВУЙ, ПРОВЕРЬ
А теперь кардинально сменим тему.
Много лет назад Билл Уэйк (Bill Wake) определил фундаментальную
для всех тестов закономерность. Он назвал ее шаблоном 3А, или ААА.
Эта аббревиатура расшифровывается как Arrange/Act/Assert (настрой/действуй/проверь).
При написании теста первым делом следует упорядочить предназначенные для тестирования данные. Обычно это делается в методе Setup
или в самом начале тестовой функции. Цель в том, чтобы привести
систему в состояние, необходимое для запуска тестирования.
Затем приходит время действия. Когда тест делает то, для чего он
предназначен.
После этого выполняется проверка. Обычно это просмотр результатов действия с целью убедиться, что система оказалась в нужном
состоянии.
В качестве простого примера рассмотрим тест для программы, подсчитывающей очки при игре в боулинг, которую мы писали в главе 2:
@Test
public void gutterGame() throws Exception {
rollMany(20, 0);
assertEquals(0, g.score());
}

122

Глава 3. Дополнительные возможности TDD

Процесс настройки в этом тесте заключается в создании класса Game
в функции Setup и функции rollMany(20, 0), которая вычисляет счет
при попадании шара в желоб.
Активная часть теста — вызов метода g.score().
Проверяющую часть теста составляет утверждение assertEquals.
За два с половиной десятилетия моей практики TDD я ни разу не
видел тест, построенный не по этой схеме.

Введение в BDD
В 2003 году практикующий преподаватель TDD Дэн Норт (Dan
North) совместно с Крисом Стивенсоном (Chris Stevenson) и Крисом
Матцем (Chris Matz) сделали то же открытие, что и Билл Уэйк, но
назвали его по-другому: Given-When-Then (GWT).
Это послужило началом новой методологии: разработки, управляемой
поведением (behavior-driven development, BDD).
Сначала BDD считали улучшенным способом написания тестов. Дэну
и другим сторонникам этой методологии новый словарь понравился больше, и его добавили в такие инструменты тестирования, как
JBehave и RSpec.
В терминах BDD тест gutterGame будет выглядеть так:
Если дано (Given), что за игру шар попал в желоб 20 раз,
Когда (When) запрашивается счет игры,
Тогда (Then) этот счет равен нулю.

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

123

Часть I. Принятые практики

В 2013 году Лиз Кио (Liz Keogh) сказала о BDD:
Это использование примеров для того, чтобы объяснить, как ведет
себя приложение… И разговоры об этих примерах.
Тем не менее полностью отделить BDD от тестирования очень трудно,
хотя бы из-за синонимичности словарей GWT и AAA. Если у вас есть
какие-либо сомнения по этому поводу, то смотрите:
zz если дано, что (Given) тестовые данные были упорядочены (Ar­

ranged);
zz когда (When) выполняется протестированное действие (Act);
zz затем (Then) доказывается (Asserted) ожидаемый результат.

Конечные автоматы
Причина, по которой я уделил такое внимание синонимичности GWT
и AAA, заключается в том, что есть еще одна известная структура из
трех элементов, которая часто встречается в программном обеспечении: переход между состояниями конечного автомата.
В качестве примера рассмотрим диаграмму состояний/переходов
турникета в метро (рис. 3.1).

Рис. 3.1. Диаграмма состояний турникета метро

124

Глава 3. Дополнительные возможности TDD

Турникет начинает работу в заблокированном состоянии. Опущенная
монета переведет его в разблокированное состояние. После прохода
через него турникет возвращается в заблокированное состояние.
В случае прохода без оплаты турникет срабатывает. Если кто-то опускает две монеты, то лишняя монета возвращается.
Эту диаграмму можно превратить вот в такую таблицу переходов состояний (табл. 3.1).
Таблица 3.1. Переходы состояний
Текущее состояние

Событие

Следующее состояние

Заблокирован

Монета

Разблокирован

Заблокирован

Проход

Сигнал тревоги

Разблокирован

Монета

Возврат монеты

Разблокирован

Проход

Заблокирован

Возврат монеты

Монета возвращена

Разблокирован

Сигнал тревоги

Сброс

Заблокирован

Каждая строка таблицы представляет собой переход из текущего
состояния в следующее, вызванный каким-то событием. Причем это
структура из трех элементов, аналогичная GWT или AAA. Сейчас
я вам покажу, что у каждой такой структуры есть синоним в соответствующей тройке GWT или AAA:
Если дано (Given), что турникет заблокирован (Locked)
Когда (When) происходит событие опускание монеты (Coin)
Тогда (Then) он переходит в состояние "разблокирован" (Unlocked).

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

125

Часть I. Принятые практики

Знали ли вы, что программа, которую вы пишете, является конечным
автоматом? Это действительно так. Каждая программа представляет
собой конечный автомат. Поскольку компьютеры — не что иное, как
обработчики состояний конечного автомата. Сам компьютер с каждой
выполняемой инструкцией переходит из одного конечного состояния
в другое.
Соответственно, тесты, которые вы пишете в процессе TDD, и поведения, описываемые средствами BDD, являются всего лишь переходами
конечного автомата, который вы пытаетесь создать. А готовый набор
тестов и есть этот конечный автомат.
Возникает очевидный вопрос: как убедиться, что все переходы, которые по вашему замыслу должен обрабатывать этот конечный автомат,
закодированы в виде тестов? Как гарантировать, что описываемый
тестами конечный автомат является тем самым, который должна реализовывать ваша программа?
Что может быть лучше, чем сначала сформулировать все переходы
в виде тестов, а затем написать реализующий их производственный
код?

И снова про BDD
Не кажется ли вам удивительным и даже немного забавным тот факт,
что сторонники BDD, возможно, сами того не осознавая, пришли к выводу, что лучший способ описать поведение системы — это описать ее
в виде конечного автомата?

ТЕСТОВЫЕ ДВОЙНИКИ
В 2000 году Стив Фриман (Steve Freeman), Тим Маккиннон (Tim
McKinnon) и Филип Крейг (Philip Craig) опубликовали статью1 под
1

126

Статья Фримана, Маккиннона и Крейга Endo-Testing: Unit Testing with Mock Objects была представлена на конференции XP2000. Она доступна по адресу http://
citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.23.3214&rep=rep1&type=pdf.

Глава 3. Дополнительные возможности TDD

названием «Эндоскопическое тестирование: модульное тестирование
с подставными объектами». О том, как эта статья повлияла на сообщество разработчиков программного обеспечения, свидетельствует
распространенность придуманного ими термина: подставной объект
(mock). Появился даже соответствующий глагол. В настоящее время
мы используем фиктивные фреймворки для имитации (mock) различных вещей.
В те годы идея TDD только начинала проникать в сообщество
программистов. Большинство из нас никогда не применяло для
тестирования кода такую вещь, как объектно-ориентированное
проектирование. Большинство из нас никогда не применяло для
тестирования кода какой-либо дизайн. И это вызывало всевозможные проблемы.
Нет, мы умели тестировать простые вещи, такие как примеры из
предыдущих глав. Но существовали и задачи, процесс тестирования
которых было невозможно представить. Например, как протестировать код, реагирующий на сбой ввода/вывода? Мы же не можем
заставить устройство ввода-вывода дать сбой во время модульного
теста. Или как протестировать код, взаимодействующий с внешним
сервисом? Потребуется ли для этого подключать внешний сервис?
И как выполнить тестирование кода, который обрабатывает сбои
этого сервиса?
Первые приверженцы TDD писали программы на языке Smalltalk.
Для них слово «объект» означало нечто из физического мира. Наверняка они использовали подставные объекты, но скорее всего, совершенно не задумывались на эту тему. Более того, когда в 1999 году
я представил идею подставного объекта в языке Java одному эксперту
по языку Smalltalker и по TDD, он ответил: «Чрезмерно громоздкий
механизм».
Тем не менее метод прижился и стал базисным элементом в TDD.
Но прежде чем я начну подробный рассказ на эту тему, нужно определиться с терминологией. Почти все мы неправильно используем
термин mock object, по крайней мере в формальном смысле. Современные тестовые двойники сильно отличаются от тех, о которых шла
речь в статье 2000 года об эндоскопическом тестировании. Разница

127

Часть I. Принятые практики

уже настолько значительна, что для отдельных значений был принят
другой словарь.
В 2007 году вышла книга Джерарда Мессароша (Gerard Meszaros)
xUnit Test Patterns: Refactoring Test Code1. В ней он определил формальную лексику, которой мы пользуемся в настоящее время. Неофициально мы все еще применяем словосочетание mock objects для
обозначения любых тестовых двойников, но когда требуется точность,
используем словарь Мессароша.
Мессарош выделил пять типов объектов, подпадающих в категорию mock objects. Это фиктивные объекты: пустышки (dummies),
заглушки (stubs), шпионы (spies), подставные объекты (mocks)
и имитации (fakes). Мессарош назвал их все тестовыми двойниками
(test doubles).
И это действительно очень хорошее название. При съемках опасных
сцен актера заменяют каскадером, а если нужно снять, например,
крупный план рук, выполняющих какие-то действия, которые не умеет выполнять актер, то в кадре окажутся руки дублера. Или дублеры
тела, которых снимают в отдельных кадрах со спины или с правильного ракурса. Именно такую функцию выполняют тестовые двойники.
Они заменяют другие объекты в процессе тестирования.
Существует своего рода иерархия тестовых двойников (рис. 3.2). Самые простые — пустышки. Заглушки — это пустышки, шпионы — это
заглушки, а подставные объекты — это шпионы. Имитации выделяются в отдельный вид.
Механизм, который используют все тестовые двойники (и который
мой приятель-эксперт по языку Smalltalker счел «чрезмерным»), — это
всего лишь полиморфизм. Например, для тестирования кода управления внешней службой нужно изолировать эту службу с помощью
интерфейса, допускающего разные реализации. После чего остается
создать реализацию, которая выступит вместо этой службы. Именно
она называется тестовым двойником.
Но, пожалуй, проще всего объяснить все это путем демонстрации.
1

128

Мессарош Дж. Шаблоны тестирования xUnit. Рефакторинг кода тестов.

Глава 3. Дополнительные возможности TDD

Рис. 3.2. Тестовые двойники

Пустышка
Создание тестового двойника обычно начинается с интерфейса —
абстрактного класса без реализованных методов. Например, вот
такого:
public interface Authenticator {
public Boolean authenticate(String username, String password);
}

Цель этого интерфейса — предоставить приложению способ аутентификации через имена пользователей и пароли. Для подтвержденных
пользователей функция authenticate возвращает значение true, для
неподтвержденных — значение false.
Предположим, мы хотим проверить, можно ли щелчком на значке
закрытия убрать окно для ввода пользовательских данных до того,
как пользователь введет туда свое имя и пароль. Тест в этом случае
может выглядеть так:

129

Часть I. Принятые практики

@Test
public void whenClosed_loginIsCancelled() throws Exception {
Authenticator authenticator = new ???;
LoginDialog dialog = new LoginDialog(authenticator);
dialog.show();
boolean success = dialog.sendEvent(Event.CLOSE);
assertTrue(success);
}

Обратите внимание, что класс LoginDialog необходимо создавать
с помощью интерфейса Authenticator. Но в тесте обращений к этому
интерфейсу нет, соответственно, непонятно, что мы должны передать
в LoginDialog.
Создание реального интерфейса RealAuthenticator — затратная операция, так как в его конструктор нужно передавать экземпляр класса
DatabaseConnection. И скорее всего, конструктор этого класса потребует реальных пользовательских данных для полей databaseUser
и databaseAuthCode. (Уверен, что вы уже сталкивались с подобными
ситуациями.)
public class RealAuthenticator implements Authenticator {
public RealAuthenticator(DatabaseConnection connection) {
//...
}
//...
}
public class DatabaseConnection {
public DatabaseConnection(UID databaseUser, UID databaseAuthCode) {
//...
}
}

Чтобы воспользоваться в тесте интерфейсом RealAuthenticator, придется сделать нечто ужасное:
@Test
public void whenClosed_loginIsCancelled() throws Exception {
UID dbUser = SecretCodes.databaseUserUID;
UID dbAuth = SecretCodes.databaseAuthCode;
DatabaseConnection connection = new DatabaseConnection(dbUser,

130

Глава 3. Дополнительные возможности TDD

dbAuth);
Authenticator authenticator = new RealAuthenticator(connection);
LoginDialog dialog = new LoginDialog(authenticator);
dialog.show();
boolean success = dialog.sendEvent(Event.CLOSE);
assertTrue(success);
}

Этот огромный код добавляется в наш тест только для создания
интерфейса Authenticator, который нужен лишь на стадии тестирования и потом никогда не будет использоваться. Вдобавок в тесте
появляются две ненужные зависимости, которые могут вызвать сбой
или на стадии компиляции, или во время загрузки. Ну и зачем нам
эта головная боль?
Правило 10. Не включайте в тесты вещи, которые там не
требуются.
Вместо этого воспользуемся объектом-пустышкой (рис. 3.3).

Рис. 3.3. Фиктивный объект

131

Часть I. Принятые практики

Пустышка — это реализация, которая ничего не делает. Каждый метод
интерфейса-пустышки реализован так, чтобы не выполнять никаких
действий. Если метод должен возвращать значение, то значение, возвращаемое пустышкой, должно быть как можно ближе к null, или нулю.
В нашем примере интерфейс AuthenticatorDummy будет выглядеть так:
public class AuthenticatorDummy implements Authenticator {
public Boolean authenticate(String username, String password) {
return null;
}
}

Если быть точным, то это именно та реализация, которую создает моя
IDE по команде Implement Interface.
Теперь тест можно написать без лишнего кода и ненужных зависимостей:
@Test
public void whenClosed_loginIsCancelled() throws Exception {
Authenticator authenticator = new AuthenticatorDummy();
LoginDialog dialog = new LoginDialog(authenticator);
dialog.show();
boolean success = dialog.sendEvent(Event.CLOSE);
assertTrue(success);
}

Итак, пустышка — это тестовый двойник, реализующий интерфейс, не
выполняющий никаких действий. Он применяется, когда тестируемая
функция принимает в качестве аргумента объект, но логика теста не
требует наличия этого объекта.
Я использую пустышки не очень часто по двум причинам. Во-первых,
мне не нравится, когда в коде не фигурируют аргументы имеющейся
функции. Во-вторых, я не люблю объекты с цепочками зависимостей,
таких как LoginDialog–>Authenticator–>DatabaseConnection–>UID.
В будущем подобные цепочки всегда становятся источниками проблем.
Разумеется, бывают случаи, когда эти проблемы неизбежны. Тогда
я предпочитаю использовать пустышку, вместо того чтобы бороться
со сложными объектами из приложения.

132

Глава 3. Дополнительные возможности TDD

Заглушка
Как мы видим на рис. 3.4, по сути заглушка (stub) — это пустышка;
она тоже реализуется таким образом, чтобы не выполнять никаких
действий. Но в отличие от пустышки, возвращает не ноль, или null,
а те значения, которые тестируемая функция должна возвращать при
разных сценариях.

Рис. 3.4. Заглушка

Рассмотрим тест, который проверяет, заканчивается ли неудачей попытка входа, когда интерфейс Authenticator отклоняет параметры
username и password:
public void whenAuthorizerRejects_loginFails() throws Exception {
Authenticator authenticator = new ?;
LoginDialog dialog = new LoginDialog(authenticator);
dialog.show();
boolean success = dialog.submit("bad username", "bad password");
assertFalse(success);
}

133

Часть I. Принятые практики

Если бы здесь использовался интерфейс RealAuthenticator, то появилась бы проблема его инициализации со всеми этими неприятными DatabaseConnection и UID. И это была бы не единственная наша
проблема. Непонятно, какое имя пользователя и пароль тут можно
указать.
Знай мы содержимое базы данных пользователей, можно было бы
выбрать значения переменных username и password. Другое дело, что
так поступать ни в коем случае не следует из-за формирования зависимости между данными тестов и производственными данными.
В результате любое изменение производственных данных выводит
тест из строя.
Правило 11. Не используйте в тестах производственные данные.
Вместо этого воспользуемся заглушкой. Для этого теста нам понадобится интерфейс RejectingAuthenticator, который будет возвращать
из метода authorize значение false:
public class RejectingAuthenticator implements Authenticator {
public Boolean authenticate(String username, String password) {
return false;
}
}

Просто добавим эту заглушку в наш тест:
public void whenAuthorizerRejects_loginFails() throws Exception {
Authenticator authenticator = new RejectingAuthenticator();
LoginDialog dialog = new LoginDialog(authenticator);
dialog.show();
boolean success = dialog.submit("bad username", "bad password");
assertFalse(success);
}

Мы ожидаем, что метод submit объекта LoginDialog вызовет функцию
authorize, которая вернет значение false.
Для проверки успешности входа в систему в случае, когда интерфейс
принимает имя пользователя и пароль, в эту игру можно сыграть
с другой заглушкой:

134

Глава 3. Дополнительные возможности TDD

public class PromiscuousAuthenticator implements Authenticator {
public Boolean authenticate(String username, String password) {
return true;
}
}
@Test
public void whenAuthorizerAccepts_loginSucceeds() throws Exception {
Authenticator authenticator = new PromiscuousAuthenticator();
LoginDialog dialog = new LoginDialog(authenticator);
dialog.show();
boolean success = dialog.submit("good username", "good password");
assertTrue(success);
}

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

Шпион
Шпион (рис. 3.5) — это заглушка. Он тоже возвращает предопределенные результаты вызовов. При этом он еще и помнит, что с ним было
сделано, и позволяет это тестировать.
Проще всего это можно объяснить на примере:
public class AuthenticatorSpy implements Authenticator {
private int count = 0;
private boolean result = false;
private String lastUsername = "";
private String lastPassword = "";
public Boolean authenticate(String username, String password) {
count++;
lastPassword = password;
lastUsername = username;
return result;
}

}

public
public
public
public

void setResult(boolean result) {this.result = result;}
int getCount() {return count;}
String getLastUsername() {return lastUsername;}
String getLastPassword() {return lastPassword;}

135

Часть I. Принятые практики

Рис. 3.5. Шпион

Обратите внимание, что метод authenticate отслеживает количество
своих вызовов, а также последние значения параметров username
и password и предоставляет методы доступа к ним. Именно такое поведение делает этот класс шпионом.
Важно и то, что метод authenticate возвращает переменную result,
значение которой может быть задано методом setResult. Фактически
наш шпион представляет собой программируемую заглушку.
Ниже представлен тест, в котором мы можем его использовать:
@Test
public void loginDialog_correctlyInvokesAuthenticator() throws
Exception {
AuthenticatorSpy spy = new AuthenticatorSpy();
LoginDialog dialog = new LoginDialog(spy);
spy.setResult(true);
dialog.show();
boolean success = dialog.submit("user", "pw");
assertTrue(success);

136

Глава 3. Дополнительные возможности TDD

}

assertEquals(1, spy.getCount());
assertEquals("user", spy.getLastUsername());
assertEquals("pw", spy.getLastPassword());

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

Подставной объект
Наконец, очередь дошла до настоящих mock-объектов (рис. 3.6).
Именно их Маккиннон, Фриман и Крейг описывали в статье, посвященной эндоскопическому тестированию.
Подставной объект (mock) — это шпион. Он возвращает предопределенные для каждого сценария тестирования значения и запоминает,
что происходило в процессе тестирования. И в дополнение к этому
подставной объект осведомлен о том, что от него ожидается, и в зависимости от этого обеспечивает прохождение или провал теста.
Другими словами, в него записываются все тестовые утверждения.
Впрочем, лучше один раз увидеть на практике, поэтому создадим
класс AuthenticatorMock:
public class AuthenticatorMock extends AuthenticatorSpy{
private String expectedUsername;
private String expectedPassword;
private int expectedCount;

137

Часть I. Принятые практики

public AuthenticatorMock(String username, String password,
int count) {
expectedUsername = username;
expectedPassword = password;
expectedCount = count;
}

}

public boolean validate() {
return getCount() == expectedCount &&
getLastPassword().equals(expectedPassword) &&
getLastPassword().equals(expectedUsername);
}

Рис. 3.6. Подставной объект

Как видите, у подставного объекта три ожидаемых поля (их имена
начинаются с expected), которые устанавливаются конструктором.
Соответственно, это программируемый подставной объект. Обратите
также внимание, что класс AuthenticatorMock является производным

138

Глава 3. Дополнительные возможности TDD

от класса AuthenticatorSpy. В подставном объекте повторно используется код шпиона.
Финальное сравнение выполняет функция validate. Если собранные
шпионом параметры count, lastPassword и lastUsername соответствуют указанным в подставном объекте ожиданиям, то данная функция
возвращает значение true.
Теперь вы легко сможете понять смысл теста, в котором используется
этот подставной объект:
@Test
public void loginDialogCallToAuthenticator_validated() throws
Exception {
AuthenticatorMock mock = new AuthenticatorMock("Bob", "xyzzy", 1);
LoginDialog dialog = new LoginDialog(mock);
mock.setResult(true);
dialog.show();
boolean success = dialog.submit("Bob", "xyzzy");
assertTrue(success);
assertTrue(mock.validate());
}

В подставном объекте мы указали свои ожидания. Это имя пользователя "Bob", пароль "xyzzy", а количество вызовов метода authen­
ticate — 1.
Затем создается LoginDialog с подставным объектом, который представляет собой интерфейс Authenticator. Подставной объект программируется на прохождение теста. После чего мы отображаем окно
диалога и отправляем запрос на вход с данными "Bob" и "xyzzy" .
Удостоверившись в успешном входе в систему, мы утверждаем, что
заданные в подставном объекте ожидания оправдались.
Вот так все работает. При этом подставные объекты могут быть очень
сложными. Например, представьте, что мы ожидаем три вызова функции f с тремя разными наборами аргументов и возврат трех разных
значений. А функция g при этом должна вызываться один раз между
первыми двумя вызовами f. Осмелитесь написать такой подставной
объект без модульных тестов?

139

Часть I. Принятые практики

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

Имитация
Пришло время разобраться с последним из тестовых двойников —
имитацией (fake) (рис. 3.7). Это не пустышка, не заглушка, не шпион
и не подставной объект. Это совершенно другая разновидность тестового двойника — симулятор.

Рис. 3.7. Имитация

В конце 1970-х я работал в компании, которая разработала систему
тестирования телефонных линий. Центральный компьютер из сервисного центра (service area computer, SAC) через модемные каналы свя-

140

Глава 3. Дополнительные возможности TDD

зывался с компьютерами, установленными на коммутационных станциях. Последние назывались тестерами абонентской линии (central
office line tester, COLT).
Подключенный к коммутационному оборудованию COLT мог создавать электрическое соединение между любой телефонной линией узла
и измерительным оборудованием. Результаты измерения электронных
характеристик телефонной линии передавались в SAC.
Анализируя эти результаты, SAC определял наличие и местоположение ошибок.
Как мы тестировали такую систему?
Мы построили имитацию в виде COLT, интерфейс переключения которого заменили симулятором. Этот симулятор имитировал звонок по
телефонной линии и измерение ее характеристик. Зафиксированные
необработанные результаты он возвращал, базируясь на номере телефона, который его попросили протестировать.
Такой подход позволил нам протестировать программное обеспечение
SAC для связи, управления и анализа, обойдясь без установки настоящего COLT и даже без установки настоящего коммутационного
оборудования и «реальных» телефонных линий.
Сегодня имитация — это тестовый двойник, реализующий какие-то
рудиментарные бизнес-правила, причем тесты могут задавать поведение имитаций. Впрочем, проще показать на примере:
@Test
public void badPasswordAttempt_loginFails() throws Exception {
Authenticator authenticator = new FakeAuthenticator();
LoginDialog dialog = new LoginDialog(authenticator);
dialog.show();
boolean success = dialog.submit("user", "bad password");
assertFalse(success);
}
@Test
public void goodPasswordAttempt_loginSucceeds() throws Exception {
Authenticator authenticator = new FakeAuthenticator();
LoginDialog dialog = new LoginDialog(authenticator);

141

Часть I. Принятые практики

}

dialog.show();
boolean success = dialog.submit("user", "good password");
assertTrue(success);

Оба теста используют объект класса FakeAuthorizer, но передают
в него разные пароли. Предполагается, что при вводе неверного пароля вход в систему невозможен.
Код класса FakeAuthenticator написать легко:
public class FakeAuthenticator implements Authenticator {
public Boolean authenticate(String username, String password)
{
return (username.equals("user") && password.equals("good
password"));
}
}

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

Принцип неопределенности TDD
Использовать подставные объекты или не использовать, вот в чем
вопрос. Хотя, разумеется, в данном случае ответ очевиден, а вопрос
состоит в том, когда их использовать.
На этот счет есть две точки зрения: лондонская и чикагская. Именно им посвящен конец этой главы. Но прежде чем углубиться в эту
тему, важно понять суть проблемы. Проблема возникает из-за неопределенности, с которой порой приходится сталкиваться в процессе TDD.
Чтобы как можно более наглядно проиллюстрировать эту неопределенность, мне придется пойти на крайние меры. В качестве примера

142

Глава 3. Дополнительные возможности TDD

я рассмотрю ситуацию, невозможную в реальности, зато очень четко
демонстрирующую вещи, которые я пытаюсь до вас донести.
Напишем функцию, вычисляющую синус угла. Угол задается в радианах. Каким должен быть наш первый тест?
Если помните, начинать имеет смысл с самого вырожденного случая,
поэтому удостоверимся, что наша функция может вычислить синус
нуля:
public class SineTest {
private static final double EPSILON = 0.0001;
Test
public void sines() throws Exception {
assertEquals(0, sin(0), EPSILON);
}

}

double sin(double radians) {
return 0.0;
}

У тех, кто привык думать на перспективу, такой код должен вызвать
беспокойство. Ведь этот тест накладывает ограничение только на
значения функции — sin(0).
Большинство функций, которые пишутся путем TDD, растущий набор тестов ограничивает настолько, что в какой-то момент функция
начинает проходить любые тесты, которые мы добавляем в набор. Вы
это видели в примерах с простыми множителями и с игрой в боулинг.
Каждый тест сужал пространство возможных решений, пока, наконец,
не оставалось всего одно.
Но функция sin(r) ведет себя не так. Тест для утверждения sin(0) == 0
проходит, но похоже, что решение не ограничено одной точкой.
Это станет более очевидным после следующего теста. Каким он должен быть? Почему бы не попробовать вариант sin(π)?
public class SineTest {
private static final double EPSILON = 0.0001;
@Test
public void sines() throws Exception {

143

Часть I. Принятые практики

}

}

assertEquals(0, sin(0), EPSILON);
assertEquals(0, sin(Math.PI), EPSILON);

double sin(double radians) {
return 0.0;
}

И снова возникает чувство, что мы ничем не ограничены. Кажется,
этот тест ничего не добавляет к решению. Он не содержит даже косвенных намеков на то, как решить задачу. Поэтому попробуем вариант
со значением π/2:
public class SineTest {
private static final double EPSILON = 0.0001;
@Test
public void sines() throws Exception {
assertEquals(0, sin(0), EPSILON);
assertEquals(0, sin(Math.PI), EPSILON);
assertEquals(1, sin(Math.PI/2), EPSILON);
}
double sin(double radians) {
return 0.0;
}
}

Этот тест провален. Как обеспечить его прохождение? Тест не дает
нам никаких подсказок. Возможно, имеет смысл добавить какой-нибудь ужасный оператор if, но это приведет только к тому, что таких
операторов постепенно станет много.
На данном этапе можно вспомнить, что наш синус раскладывается
в ряд Тейлора, и попытаться это реализовать.
Написать такой код не слишком сложно:
public class SineTest {
private static final double EPSILON = 0.0001;
@Test
public void sines() throws Exception {
assertEquals(0, sin(0), EPSILON);
assertEquals(0, sin(Math.PI), EPSILON);

144

Глава 3. Дополнительные возможности TDD

}

assertEquals(1, sin(Math.PI/2), EPSILON);

double sin(double radians) {
double r2 = radians * radians;
double r3 = r2*radians;
double r5 = r3 * r2;
double r7 = r5 * r2;
double r9 = r7 * r2;
double r11 = r9 * r2;
double r13 = r11 * r2;
return (radians - r3/6 + r5/120 - r7/5040 + r9/362880 r11/39916800.0 + r13/6227020800.0);
}
}

Тест пройден, но код выглядит откровенно некрасиво. Тем не менее
попробуем вычислить таким способом несколько других синусов:
public void sines() throws Exception {
assertEquals(0, sin(0), EPSILON);
assertEquals(0, sin(Math.PI), EPSILON);
assertEquals(1, sin(Math.PI/2), EPSILON);
assertEquals(0.8660, sin(Math.PI/3), EPSILON);
assertEquals(0.7071, sin(Math.PI/4), EPSILON);
assertEquals(0.5877, sin(Math.PI/5), EPSILON);
}

Все тесты проходят. Но решение уродливо, так как его точность ограничена. Для получения предельного значения с нужной точностью
в ряду Тейлора должно быть достаточное количество членов. (Обратите внимание, как меняется константа ESPILON.)
public class SineTest {
private static final double EPSILON = 0.000000001;
@Test
public void sines() throws Exception {
assertEquals(0, sin(0), EPSILON);
assertEquals(0, sin(Math.PI), EPSILON);
assertEquals(1, sin(Math.PI/2), EPSILON);
assertEquals(0.8660254038, sin(Math.PI/3), EPSILON);
assertEquals(0.7071067812, sin(Math.PI/4), EPSILON);
assertEquals(0.5877852523, sin(Math.PI/5), EPSILON);
}

145

Часть I. Принятые практики

double sin(double radians) {
double result = radians;
double lastResult = 2;
double m1 = -1;
double sign = 1;
double power = radians;
double fac = 1;
double r2 = radians * radians;
int n = 1;
while (!close(result, lastResult)) {
lastResult = result;
power *= r2;
fac *= (n+1) * (n+2);
n += 2;
sign *= m1;
double term = sign * power / fac;
result += term;
}
}

}

return result;

boolean close(double a, double b) {
return Math.abs(a - b) < .0000001;
}

Кажется, мы получили ожидаемый результат. Хотя подождите. Что
случилось с TDD? И откуда нам знать, что алгоритм работает корректно? Его код получился очень длинным. Как определить, правильный
ли этот код?
Наверное, стоит проверить еще несколько значений. Вот только наши
тесты уже стали неудобочитаемыми, поэтому выполним небольшой
рефакторинг:
private void checkSin(double radians, double sin) {
assertEquals(sin, sin(radians), EPSILON);
}
@Test
public void sines() throws Exception {
checkSin(0, 0);

146

Глава 3. Дополнительные возможности TDD

checkSin(PI, 0);
checkSin(PI/2, 1);
checkSin(PI/3, 0.8660254038);
checkSin(PI/4, 0.7071067812);
checkSin(PI/5, 0.5877852523);
}

checkSin(3* PI/2, -1);

Тестирование проходит благополучно. Добавим еще парочку проверок:
checkSin(2*PI, 0);
checkSin(3*PI, 0);

Вариант с 2π работает, а с 3π — нет. Хотя мы подошли достаточно близко: 4.6130E-9. Вероятно, можно исправить ситуацию, увеличив предел
сравнения в функции close() , но это уже напоминает подтасовку
и для значений 100π или 1000π, скорее всего, не сработает. Лучшим
решением было бы уменьшить угол, сохраняя значения в промежутке
между 0 и 2π.
double sin(double radians) {
radians %= 2*PI;
double result = radians;

Все работает. А как насчет отрицательных чисел?
checkSin(-PI, 0);
checkSin(-PI/2, -1);
checkSin(-3*PI/2, 1);
checkSin(-1000*PI, 0);

В данном случае тоже все хорошо. Проверим большие значения, которые не являются кратными 2π:
checkSin(1000*PI + PI/3, sin(PI/3));

Этот тест тоже прошел. Какие еще варианты можно попробовать?
Существуют ли значения, для которых тесты проходить не будут?
Увы! Я не знаю.

147

Часть I. Принятые практики

Суть принципа неопределенности
Итак, мы столкнулись с ситуацией неопределенности. Сколько бы
значений ни проверяли, мы не будем уверены в том, что не упустили
некий момент. Будет казаться, что существует какое-то входное значение, которое не даст правильного выходного значения.
К счастью, большинства функций эта ситуация не касается. После
того как написан последний тест, вы точно знаете, что все работает
должным образом. Но бывают и менее приятные функции, заставляющие задуматься о том, что, возможно, существуют значения, при
которых тестирование проваливается.
Решить эту проблему с помощью тестов можно только одним способом: проверить все возможные значения. А поскольку числа
двойной точности занимают в памяти 64 бита, то получается, нам
нужно написать чуть меньше чем 2 × 1019 тестов. Я к таким подвигам
не готов.
Итак, какой информации о рассматриваемой функции мы можем доверять? Верно ли то, что с помощью ряда Тейлора можно вычислить
синус угла, заданного в радианах? Да, мы видели математическое
доказательство этого факта и можем быть уверены, что ряд Тейлора
сойдется к правильному значению.
Но как написать набор тестов, которые докажут, что мы корректно
производим все расчеты?
Наверное, можно проанализировать каждый член ряда Тейлора. Например, для sin(π) члены ряда Тейлора равны 3,141592653589793,
−2,0261201264601763, 0,5240439134171688, −0,07522061590362306,
0,006925270707505149, −4,4516023820919976E-4, 2,114256755841263E-5,
−7,727858894175775E-7, 2,2419510729973346E-8.
Впрочем, этот тест ничем не лучше уже написанного. Вышеприведенные значения применимы только к конкретному тесту, и мы не знаем,
можно ли их использовать для расчета синуса другого угла.
Нет, нужно что-то другое. Что-то однозначное, доказывающее, что наш
алгоритм действительно корректно считает ряд Тейлора.

148

Глава 3. Дополнительные возможности TDD

Но что собой представляет этот ряд? Это бесконечная знакопеременная сумма нечетных степеней x, деленная на нечетные факториалы:

Или, другими словами,

Как это нам поможет? При наличии шпиона, информирующего о том,
как рассчитываются члены ряда Тейлора, можно написать вот такой
тест:
@Test
public void taylorTerms() throws Exception {
SineTaylorCalculatorSpy c = new SineTaylorCalculatorSpy();
double r = Math.random() * PI;
for (int n = 1; n grace)
return amount + amount * (days - grace);
return amount;
}

public class Rental {
public String title;
public int days;
public VideoType type;

}

public Rental(String title, int days) {
this.title = title;
this.days = days;
type = VideoRegistry.getType(title);
}

Этот код не сможет пройти старый тест, поскольку в классе Customer
теперь суммируются два платежа:
@Test
public void RegularMovie_SecondAndThirdDayFree() throws Exception {
customer.addRental("RegularMovie", 2);
assertFeeAndPoints(150, 1);
customer.addRental("RegularMovie", 3);
assertFeeAndPoints(150, 1);
}

Придется поделить этот тест на две части. Пожалуй, вот так будет
лучше:
@Test
public void RegularMovie_SecondDayFree() throws Exception {
customer.addRental("RegularMovie", 2);
assertFeeAndPoints(150, 1);
}

187

Часть I. Принятые практики

@Test
public void RegularMovie_ThirdDayFree() throws Exception {
customer.addRental("RegularMovie", 3);
assertFeeAndPoints(150, 1);
}

Рефакторинг: теперь мне многое не нравится в классе Customer. Выделим из этих двух уродливых циклов со странными операторами if
несколько более удобных методов.
public int getRentalFee() {
int fee = 0;
for (Rental rental : rentals)
fee += feeFor(rental);
return fee;
}
private int feeFor(Rental rental) {
int fee = 0;
if (rental.getType() == REGULAR)
fee += applyGracePeriod(150, rental.getDays(), 3);
else
fee += rental.getDays() * 100;
return fee;
}
public int getRenterPoints() {
int points = 0;
for (Rental rental : rentals)
points += pointsFor(rental);
return points;
}
private int pointsFor(Rental rental) {
int points = 0;
if (rental.getType() == REGULAR)
points += applyGracePeriod(1, rental.getDays(), 3);
else
points++;
return points;
}

Кажется, эти две закрытые функции больше работают с классом
Rental, чем с классом Customer. Переместим их в этот класс вместе со

188

Глава 4. Разработка тестов

вспомогательной функцией applyGracePeriod, чтобы немного очистить класс Customer.
public class Customer {
private List rentals = new ArrayList();
public void addRental(String title, int days) {
rentals.add(new Rental(title, days));
}
public int getRentalFee() {
int fee = 0;
for (Rental rental : rentals)
fee += rental.getFee();
return fee;
}

}

public int getRenterPoints() {
int points = 0;
for (Rental rental : rentals)
points += rental.getPoints();
return points;
}

Увеличившийся код класса Rental теперь выглядит совсем некрасиво:
public class Rental {
private String title;
private int days;
private VideoType type;
public Rental(String title, int days) {
this.title = title;
this.days = days;
type = VideoRegistry.getType(title);
}
public String getTitle() {
return title;
}
public VideoType getType() {
return type;
}

189

Часть I. Принятые практики

public int getFee() {
int fee = 0;
if (getType() == REGULAR)
fee += applyGracePeriod(150, days, 3);
else
fee += getDays() * 100;
return fee;
}
public int getPoints() {
int points = 0;
if (getType() == REGULAR)
points += applyGracePeriod(1, days, 3);
else
points++;
return points;
}
private static int applyGracePeriod(int amount, int days, int
grace) {
if (days > grace)
return amount + amount * (days - grace);
return amount;
}
}

Надо бы избавиться от некрасивых операторов if. Каждый новый тип
видео будет означать появление нового условия. Очистку мы начнем
с некоторых подклассов и полиморфизма.
Перво-наперво, в абстрактном классе Movie находится вспомогательная функция applyGracePeriod и две абстрактные функции для получения суммы к оплате и количества бонусных баллов.
public abstract class Movie {
private String title;
public Movie(String title) {
this.title = title;
}
public String getTitle() {
return title;

190

Глава 4. Разработка тестов

}
public abstract int getFee(int days, Rental rental);
public abstract int getPoints(int days, Rental rental);
protected static int applyGracePeriod(int amount, int days,
int grace) {

}

}

if (days > grace)
return amount + amount * (days - grace);
return amount;

Класс RegularMovie очень простой:
public class RegularMovie extends Movie {
public RegularMovie(String title) {
super(title);
}
public int getFee(int days, Rental rental) {
return applyGracePeriod(150, days, 3);
}

}

public int getPoints(int days, Rental rental) {
return applyGracePeriod(1, days, 3);
}

А класс ChildrensMovie еще проще:
public class ChildrensMovie extends Movie {
public ChildrensMovie(String title) {
super(title);
}
public int getFee(int days, Rental rental) {
return days * 100;
}

}

public int getPoints(int days, Rental rental) {
return 1;
}

191

Часть I. Принятые практики

От класса Rental осталось немного — всего пара функций-делегатов:
public class Rental {
private int days;
private Movie movie;
public Rental(String title, int days) {
this.days = days;
movie = VideoRegistry.getMovie(title);
}
public String getTitle() {
return movie.getTitle();
}
public int getFee() {
return movie.getFee(days, this);
}

}

public int getPoints() {
return movie.getPoints(days, this);
}

Класс VideoRegistry превратился в фабрику для класса Movie.
public class VideoRegistry {
public enum VideoType {REGULAR, CHILDRENS;}
private static Map videoRegistry =
new HashMap();
public static Movie getMovie(String title) {
switch (videoRegistry.get(title)) {
case REGULAR:
return new RegularMovie(title);
case CHILDRENS:
return new ChildrensMovie(title);
}
return null;
}

}

192

public static void addMovie(String title, VideoType type) {
videoRegistry.put(title, type);
}

Глава 4. Разработка тестов

А что с классом Customer? У него просто все это время было неправильное имя. А на самом деле это класс RentalCalculator, который
экранирует наши тесты от семейства обслуживающих его классов.
public class RentalCalculator {
private List rentals = new ArrayList();
public void addRental(String title, int days) {
rentals.add(new Rental(title, days));
}
public int getRentalFee() {
int fee = 0;
for (Rental rental : rentals)
fee += rental.getFee();
return fee;
}

}

public int getRenterPoints() {
int points = 0;
for (Rental rental : rentals)
points += rental.getPoints();
return points;
}

Полученный результат схематично представлен на рис. 4.10.

Рис. 4.10. Результат

193

Часть I. Принятые практики

Все классы справа от RentalCalculator создавались в процессе рефакторинга. Но класс RentalCalculatorTest ничего о них не знает. Ему
известен только класс VideoRegistry, который он должен инициализировать тестовыми данными. Более того, эти классы не использует
ни один другой тестовый модуль. Модуль RentalCalculatorTest
косвенно тестирует все остальные классы. Мы убрали однозначное
соответствие.
Именно так хорошие программисты экранируют структуру производственного кода от структуры тестов, тем самым избегая проблемы
хрупких тестов.
Разумеется, в больших системах этот шаблон будет часто повторяться.
Будут многочисленные семейства модулей, защищенных от проверяющих их тестовых модулей собственными фасадными методами или
интерфейсными модулями.
Некоторые из вас могут предположить, что тесты, управляющие
семейством модулей через фасадные методы, являются интеграционными. Но о них мы поговорим позже. Пока скажу только, что
у интеграционных тестов совсем другая цель. Это тесты для программистов, написанные другими программистами в целях определения
поведения.

Конкретика против общности
Существует еще один параметр разделения тестов и производственного кода. Я рассказывал об этом в главе 2 в примере с простыми
множителями. Там я дал вам пару рекомендаций, которые сейчас
сформулирую в виде правила.
Правило 13. По мере того как тесты становятся более конкретными, код становится более общим.
Количество модулей производственного кода растет по мере увеличения количества тестов. Но развиваются тесты и производственный
код в разных направлениях.

194

Глава 4. Разработка тестов

С добавлением каждого нового теста набор становится все более конкретным. А вот семейство тестируемых модулей должно развиваться
в противоположном направлении — становиться все более обобщенным (рис. 4.11).

Рис. 4.11. Набор тестов становится более конкретным, а семейство
тестируемых модулей — более общим

Это одна из целей рефакторинга. Как это реализуется, вы могли наблюдать в примере с магазином видеопроката. Сначала я написал тест.
Затем — некрасивый код для прохождения этого теста. Данный код не
был универсальным. Более того, он был глубоко конкретным. Однако
на этапе рефакторинга я преобразовал его в более общую форму.
Такое дивергентное развитие тестов и производственного кода означает, что в итоге они приобретут разную форму. Тесты превратятся
в линейный список ограничений и спецификаций. Производственный
код вырастет в богатый набор логических конструкций разных видов
поведения, организованных для взаимодействия с управляющей приложением базовой абстракцией.

195

Часть I. Принятые практики

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

ОПРЕДЕЛЕНИЕ ОЧЕРЕДНОСТИ
ПРЕОБРАЗОВАНИЙ
Итак, многочисленные примеры показали, что в процессе разработки
через тестирование мы постепенно конкретизируем тесты, одновременно стремясь придать производственному коду как можно более
общий вид. Но как совершаются эти преобразования?
Конкретизация тестов сводится к добавлению нового утверждения
в существующий набор или к добавлению нового метода тестирования
для упорядочивания, действия и последующего утверждения нового
ограничения. Это аддитивные операции. Существующий тестовый код
при этом не меняется. Мы просто добавляем новый код.
А вот когда после добавления нового ограничения мы редактируем
производственный код, обеспечивая прохождение теста, — это уже не
аддитивный процесс. Производственный код необходимо преобразовать, заставив его вести себя по-другому. То есть эти преобразования
меняют поведение кода.
После этого производственный код подвергается рефакторингу в целях его очистки. Рефакторинг — это тоже изменение производственного кода, но с сохранением его поведения.
Надеюсь, вы уже обнаружили корреляцию с циклом «красный →
зеленый → рефакторинг». Красный этап — аддитивный. Во время
зеленого происходит редактирование. Последний этап — восстановительный.

196

Глава 4. Разработка тестов

О рефакторинге мы подробно поговорим в следующей главе, а пока
подробно рассмотрим преобразования.
Это небольшое редактирование кода, меняющее его поведение и одновременно обобщающее решение. Их суть лучше всего объяснять на
примере.
Вспомним упражнения на поиск простых множителей из главы 2. Мы
начинали с проваленного теста и реализации для вырожденного случая.
public class PrimeFactorsTest {
@Test
public void factors() throws Exception {
assertThat(factorsOf(1), is(empty()));
}

}

private List factorsOf(int n) {
return null;
}

Чтобы обеспечить прохождение теста, я преобразовал значение null
в константу new ArrayList():
private List factorsOf(int n) {
return new ArrayList();
}

Это преобразование изменило поведение решения, попутно придав
ему более общий вид. Значение null представляло собой чрезвычайно конкретный случай. Я же заменил его более универсальной
константой.
Следующий проваленный тест также привел к обобщающим преобразованиям:
assertThat(factorsOf(2), contains(2));
private List factorsOf(int n) {
ArrayList factors = new ArrayList();
if (n>1)
factors.add(2);
return factors;
}

197

Часть I. Принятые практики

Для начала я выделил ArrayList в переменную factor, добавив оператор if. Оба этих преобразования являются обобщающими. Переменные всегда более универсальны, чем константы. Правда, условный
оператор if выполняет лишь частичное обобщение. С одной стороны,
в связи с тестом он рассматривает частный случай для множителей 1 и 2, но с другой стороны, эта конкретность смягчается неравенством n>1. Причем это неравенство фигурирует и в конечном варианте
нашего обобщенного решения.
Вооружившись этими знаниями, посмотрим на другие преобразования.

{} → ничто
Обычно это самое первое преобразование, фигурирующее в начале
сеанса TDD. Изначально код попросту отсутствует. Мы пишем тест
для самого вырожденного случая, какой только можем придумать.
Чтобы данный тест начал компилироваться, но при этом не проходил,
мы заставляем тестируемую функцию возвращать значение null1, как
это было сделано в упражнении с простыми множителями.
private List factorsOf(int n) {
return null;
}

Этот код преобразует ничто в функцию, которая ничего не возвращает. Такое преобразование редко приводит к прохождению теста,
поэтому обычно за ним сразу же следует другое преобразование.

Ничто → константа
Пример такого преобразования вы тоже видели в упражнении с простыми множителями. Возвращаемое значение null преобразовывалось
в пустой список целых чисел.
1

198

Или самое вырожденное допустимое значение.

Глава 4. Разработка тестов

private List factorsOf(int n) {
return new ArrayList();
}

Видели вы такое преобразование и в упражнении с подсчетом очков
в боулинге в главе 2, хотя в этом случае стадия «{} → ничто» отсутствовала, так как мы сразу перешли к константе.
public int score() {
return 0;
}

Константа → переменная
Следующий этап — это превращение константы в переменную. Мы
видели такое преобразование в упражнении по созданию стека целых
чисел (глава 2). Помните, я создал для значения true, возвращаемого
утверждением isEmpty, переменную empty?
public class Stack {
private boolean empty = true;

}

public boolean isEmpty() {
return empty;
}
. . .

Фигурировало это преобразование и в упражнении на поиск простых
множителей, когда, чтобы пройти тест для значения 3, я заменил константу 2 аргументом n.
private List factorsOf(int n) {
ArrayList factors = new ArrayList();
if (n>1)
factors.add(n);
return factors;
}

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

199

Часть I. Принятые практики

в ­несколько более общее. Каждый раз это обобщение выступает способом сделать так, чтобы код стал обрабатывать более широкий набор
ограничений.
Если хорошенько подумать, то становится понятным, что каждое из
этих преобразований расширяет возможности гораздо больше, чем
ограничение, которое накладывает на код текущий неудачный тест.
И по мере применения таких преобразований гонка между ограничениями тестов и обобщениями кода должна закончиться в пользу
обобщений. В итоге производственный код станет настолько универсальным, что сможет соответствовать всем будущим ограничениям
в рамках текущих требований.
Но я отвлекся.

Отсутствие условий → выбор
Это преобразование добавляет оператор if или его эквивалент. Далеко
не всегда это обобщение. Сделать так, чтобы условное выражение,
которое должно обеспечить прохождение теста, не стало слишком
конкретным, — задача программиста.
Это преобразование вы видели в упражнении с простыми множителями при разложении значения 2. Обратите внимание, что выбранное
мной условие не (n==2), так как это было бы слишком конкретно.
Я попытался сделать этот оператор более универсальным, взяв в качестве условия неравенство (n>1).
private List factorsOf(int n) {
ArrayList factors = new ArrayList();
if (n>1)
factors.add(2);
return factors;
}

Значение → список
Это обобщающее преобразование превращает переменную в список
значений. Роль списка может играть массив или более сложный кон-

200

Глава 4. Разработка тестов

тейнер. Вы видели это преобразование в упражнении со стеком, когда
я превратил переменную element в массив elements.
public class Stack {
private int size = 0;
private int[] elements = new int[2];
public void push(int element) {
this.elements[size++] = element;
}

}

public int pop() {
if (size == 0)
throw new Underflow();
return elements[--size];
}

Оператор → рекурсия
Это обобщающее преобразование делает оператор рекурсивным.
Такого рода преобразования очень распространены в языках, поддерживающих рекурсию, особенно в Lisp и Logo, где циклы реализуются
исключительно таким способом. Преобразование изменяет однократно
вычисляемое выражение на выражение, вычисляемое на основе самого себя. Его пример вы видели в упражнении с разбиением на строки
в главе 3.
private String wrap(String s, int w) {
if (w >= s.length())
return s;
else
return s.substring(0, w) + "\n" + wrap(s.substring(w), w);
}

Выбор → итерация
В упражнении на поиск простых множителей я несколько раз преобразовывал операторы if в цикл while. Это явное обобщение, поскольку итерация — обобщенный вариант выбора, а выбор — просто
вырожденная итерация.

201

Часть I. Принятые практики

private List factorsOf(int n) {
ArrayList factors = new ArrayList();
if (n > 1) {
while (n % 2 == 0) {
factors.add(2);
n /= 2;
}
}
if (n > 1)
factors.add(n);
return factors;
}

Значение → измененное значение
Это преобразование изменяет значение переменной, обычно с целью
накопления частичных значений в цикле или в инкрементных вычислениях. Примеры вы видели в нескольких упражнениях, и самое показательное из них, наверное, — упражнение на сортировку в главе 3.
Обратите внимание, что первые два присвоения — это обычная инициализация переменных first и second. А вот операции list.set(...)
фактически изменяют элементы в списке.
private List sort(List list) {
if (list.size() > 1) {
if (list.get(0) > list.get(1)) {
int first = list.get(0);
int second = list.get(1);
list.set(0, second);
list.set(1, first);
}
}
return list;
}

Пример: числа Фибоначчи
Попробуем выполнить простое упражнение и проследим за трансформациями. Напомню формулу расчета чисел Фибоначчи: fib(0) = 1,
fib(1) = 1 и fib(n) = fib(n-1) + fib(n-2).

202

Глава 4. Разработка тестов

Начнем, как обычно, с написания теста. Почему я использую тип
BigInteger? Дело в том, что числа Фибоначчи очень быстро становятся большими.
public class FibTest {
@Test
public void testFibs() throws Exception {
assertThat(fib(0), equalTo(BigInteger.ONE));
}

}

private BigInteger fib(int n) {
return null;
}

Чтобы обеспечить прохождение теста, воспользуемся преобразованием «ничто → константа».
private BigInteger fib(int n) {
return new BigInteger("1");
}

Мне тоже показался странным аргумент типа String, но в библиотеке
Java это вот так.
Для прохождения следующего теста ничего не придется делать:
@Test
public void testFibs() throws Exception {
assertThat(fib(0), equalTo(BigInteger.ONE));
assertThat(fib(1), equalTo(BigInteger.ONE));
}

А вот этот тест уже не проходит:
@Test
public void testFibs() throws Exception {
assertThat(fib(0), equalTo(BigInteger.ONE));
assertThat(fib(1), equalTo(BigInteger.ONE));
assertThat(fib(2), equalTo(new BigInteger("2")));
}

Для его прохождения выполним преобразование «отсутствие условия → выбор»:

203

Часть I. Принятые практики

private BigInteger fib(int n) {
if (n > 1)
return new BigInteger("2");
else
return new BigInteger("1");
}

Решение получается скорее конкретным, зато содержит потенциал
для использования в функции fib отрицательных аргументов.
Следующий тест заставляет меня отправиться за золотом:
assertThat(fib(3), equalTo(new BigInteger("3")));

Для его прохождения используем преобразование «оператор → рекурсия»:
private BigInteger fib(int n) {
if (n > 1)
return fib(n-1).add(fib(n-2));
else
return new BigInteger("1");
}

Это очень элегантное решение требует чрезмерно больших затрат
времени1 и памяти. Слишком ранняя погоня за золотом часто дорого
обходится. Можно ли выполнить то же самое другим способом?
Конечно да:
private BigInteger fib(int n) {
return fib(BigInteger.ONE, BigInteger.ONE, n);
}
private BigInteger fib(BigInteger fm2, BigInteger fm1, int n) {
if (n>1)
return fib(fm1, fm1.add(fm2), n-1);
else
return fm1;
}
1

204

На моем MacBook Pro с процессором 2,3 ГГц расчет fib(40)==165580141 занимает 9 секунд.

Глава 4. Разработка тестов

Это достаточно быстрый1 алгоритм хвостовой рекурсии.
Может показаться, что последнее преобразование представляет собой
вариант «оператор → рекурсия», но это не так. Фактически это преобразование «выбор → итерация». На самом деле, если бы компилятор
Java соизволил предложить нам оптимизацию хвостовой рекурсии2,
то мы бы практически гарантированно получили следующий код.
Обратите внимание на подразумеваемое преобразование if → while.
private BigInteger
BigInteger fm2 =
BigInteger fm1 =
while (n>1) {
BigInteger f =
fm2 = fm1;
fm1 = f;
n--;
}
return fm1;
}

fib(int n) {
BigInteger.ONE;
BigInteger.ONE;
fm1.add(fm2);

Это небольшое отступление появилось, чтобы сделать важное замечание:
Правило 14. Если одно преобразование приводит к неоптимальному решению, то попробуйте другое преобразование.
Мы уже во второй раз сталкиваемся с ситуацией, когда одно преобразование приводит к неоптимальному решению, в то время как
другое дает гораздо лучшие результаты. Первый раз вы наблюдали
это в упражнении на сортировку, где именно преобразование «значение → измененное значение» привело к пузырьковой сортировке. Его
замена на преобразование «отсутствие условий → выбор» позволила
реализовать быструю сортировку. Это был переломный шаг:
private List sort(List list) {
if (list.size() second)
return asList(second, first);
else
return asList(first, second);
}

Определение очередности преобразований
Итак, что же делать с ситуацией, когда в процессе разработки через
тестирование мы оказались на развилке? Направление дальнейшего
движения зависит от того, каким преобразованием мы воспользуемся,
чтобы обеспечить прохождение текущего теста. Есть ли способ выбрать лучшее преобразование? Как оценить, какое из преобразований
лучше? Может быть, у них существует приоритет?
Я считаю, что приоритет преобразований существует. И сейчас о нем
расскажу. Но сначала хотелось бы пояснить, что мое мнение в данном
случае — всего лишь постулат. У меня нет математического доказательства, более того, я не уверен, что указанная последовательность
работает во всех случаях. С относительной уверенностью могу утверждать только то, что вы, скорее всего, получите лучшую реализацию, если будете выбирать преобразования примерно в следующем
порядке:
zz {} → ничто;
zz ничто → константа;
zz константа → переменная;
zz отсутствие условия → выбор;
zz значение → список;
zz выбор → итерация;
zz оператор → рекурсия;
zz значение → измененное значение.

206

Глава 4. Разработка тестов

Не стоит думать, что это закономерный порядок, который нельзя
нарушать (например, прибегать к преобразованию «константа →
переменная» до завершения преобразования «ничто → константа»).
Иногда тест можно пройти, скажем, преобразовав ничто в выбор из
двух констант и вообще опустив шаг «ничто → константа».
Другими словами, если вы испытываете искушение пройти тест, комбинируя два или более преобразования, то можно пропустить один
или несколько тестов. Попробуйте найти тест, который можно пройти,
используя только одно из этих преобразований, а затем, оказавшись
на развилке, выберите путь, на который ведет преобразование, расположенное выше по списку.
Всегда ли работает этот механизм? Наверное, нет, но мне обычно
с ним очень везло. Как вы могли убедиться, описанный подход позволил получить лучший результат как для алгоритмов сортировки,
так и для алгоритмов Фибоначчи.
Проницательный читатель уже понял, что указанный порядок преобразований приводит нас к реализации решений в стиле функционального программирования.

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

5

РЕФАКТОРИНГ

Глава 5. Рефакторинг

В 1999 году я прочитал «Рефакторинг» Мартина Фаулера. Эта книга
уже стала классикой, и я призываю вас обязательно ознакомиться
с ней. Недавно он опубликовал второе издание, значительно дополненное и модернизированное. Первое издание содержало примеры на
языке Java, а второе — на языке JavaScript.
Я читал первое издание, когда мой двенадцатилетний сын Джастин
играл в хоккейной команде. Для тех из вас, у кого нет ребенка-хоккеиста, расскажу, что собственно игра занимает пять минут, а потом дети
проводят десять-пятнадцать минут вне льда, чтобы остыть.
И вот в этих перерывах между играми я читал замечательную книгу
Мартина. Это была первая книга, в которой код представлялся чем-то
податливым. Большинство изданий того периода давали код в окончательной форме. В книге же Мартина демонстрировалось, как взять
плохой код и очистить его.
В процессе чтения я слышал, как родители болели за детей на льду.
Я тоже болел, но не за игру. Меня полностью захватила книга у меня
в руках. Во многом именно она стала предпосылкой для написания
«Чистого кода».
Никто не сказал лучше Мартина:
Написать код, понятный компьютеру, может кто угодно. Хорошие
программисты пишут код, понятный людям.
В этой главе я познакомлю вас с моей точкой зрения на искусство рефакторинга. Но она не заменит вам знакомства с книгой Мартина.

ЧТО ТАКОЕ РЕФАКТОРИНГ
На этот раз я перефразирую Мартина:
Рефакторинг — это последовательность небольших изменений,
улучшающих структуру программного обеспечения без изменения
его поведения, что подтверждается прохождением комплексного
набора тестов после каждого изменения в последовательности.

209

Часть I. Принятые практики

В этом определении есть два важных момента.
Во-первых, сохранение поведения. После одного или нескольких
рефакторингов поведение программного обеспечения остается неизменным. Удостовериться в этом можно единственным способом:
постоянно проводя всеобъемлющее тестирование.
Во-вторых, каждый отдельный рефакторинг имеет маленький размер.
Насколько маленький? У меня есть принцип: достаточно маленький,
чтобы не пришлось прибегать к отладке.
Существует множество методов рефакторинга, и некоторые из них
я опишу на следующих страницах. Бывают и изменения кода, которые не считаются частью канонического рефакторинга, но представляют собой структурные изменения, сохраняющие поведение.
Есть настолько шаблонные методы рефакторинга, что за вас их
может сделать IDE. Некоторые из методов настолько просты, что
их можно без опасений проводить вручную. Более сложные требуют
значительного внимания. В этом случае я вспоминаю следующий
принцип: если опасаюсь, что мне придется использовать отладчик,
то разбиваю планируемое изменение на более мелкие и безопасные
части. Если даже в этом случае не удается избежать отладки, то
я провожу дальнейшее разбиение.
Правило 15. Избегайте использования отладчиков.
Цель рефакторинга — очистить код. Это часть цикла «красный → зеленый → рефакторинг». Это постоянная, а не запланированная и выполняемая по расписанию деятельность. Вы поддерживаете чистоту
кода, выполняя рефакторинг на каждом витке цикла TDD.
Иногда возникают ситуации, когда требуется более масштабный рефакторинг. Рано или поздно вы неизбежно обнаружите, что структура
системы нуждается в обновлении, и захотите отредактировать весь
код. Это незапланированный процесс. Для проведения рефакторинга не нужно останавливать добавление функционала и исправление
ошибок. Достаточно приложить немного дополнительных усилий
по рефакторингу к циклу «красный → зеленый → рефакторинг»

210

Глава 5. Рефакторинг

и постепенно внести нужные изменения, не прекращая остальной
деятельности.

ОСНОВНОЙ ИНСТРУМЕНТАРИЙ
К некоторым методам рефакторинга я прибегаю чаще, чем к остальным. Для их автоматизации я использовал свою IDE. Рекомендую вам
выучить эти приемы наизусть и понять тонкости их автоматизации
вашей IDE.

Переименование
В моей книге «Чистый код» есть глава, посвященная правильному
именованию. Об этом писали и многие другие1. Это очень важный
аспект.
Дело в том, что правильно выбрать имя непросто. Поиск подходящего
имени часто представляет собой процесс последовательных постепенных улучшений. Не бойтесь искать правильные имена. На ранней
стадии существования проекта улучшайте их как можно чаще.
С течением времени менять имена становится все труднее. Все больше
программистов их запоминают и плохо реагируют на внезапные переименования. Рано или поздно наступает момент, когда переименование важных классов и функций становится возможным только после
обсуждения и поиска консенсуса.
Поэтому пока новый код не получил слишком широкой известности,
экспериментируйте с именами. Часто переименовывайте классы
и методы. В процессе вы обнаружите, что их нужно группировать
по-другому. Придется перемещать методы из одного класса в другой, чтобы они соответствовали новым именам. Придется менять
1

Другой хороший источник информации книга: Эванс Э. Предметно-ориентированное проектирование (DDD): структуризация сложных программных
систем.

211

Часть I. Принятые практики

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

Выделение методов
Метод рефакторинга Extract Method, возможно, — самый важный из
всех. Мне кажется, именно этот механизм отвечает за поддержание
чистоты и хорошую систематизацию кода.
Мой вам совет: выделяйте, пока есть что выделять.
Такой подход преследует достижение двух целей. Во-первых, каждая
функция должна делать только одну вещь1. Во-вторых, код должен
читаться как хорошо написанная проза2.
Если функция выполняет одно действие, то никакую другую функцию выделить из нее уже не получится. Чтобы добиться такого
эффекта, нужно выделять, выделять и выделять до тех пор, пока это
возможно.
Разумеется, это породит множество крошечных функций. Может показаться, что среди этого изобилия непросто понять назначение кода.
Да и в принципе в огромном рое функций легко заблудиться.
Однако на самом деле происходит обратная вещь. Назначение кода
становится намного более очевидным. Появляются четкие уровни
абстракции с понятными границами между ними.
Современные языки изобилуют модулями, классами и пространствами имен. Это позволяет строить иерархию имен, в которой разме1
2

212

Мартин Р. С. Чистый код. — С. 30.
Там же.

Глава 5. Рефакторинг

щаются функции. Классы находятся в пространствах имен и, в свою
очередь, содержат функции. Закрытые функции используются внутри
открытых. Классы содержат внутренние и вложенные классы. И так
далее. Используйте эту иерархию для создания структуры, позволяющей другим программистам легко находить написанные вами
функции.
И обязательно выбирайте для них хорошие имена. Напомню, что длина имени функции должна быть обратно пропорциональна ее области
видимости. Имена открытых функций должны быть относительно
короткими. Закрытым функциям можно присваивать более длинные
имена.
По мере выделения все новых функций их имена будут становиться
все длиннее, поскольку цель каждой следующей выделенной функции — становиться все менее общей. Большинство этих функций будут вызываться только из одного места, соответственно, их назначение
будет чрезвычайно конкретным. Имена таких специализированных
функций должны быть длинными. Скорее всего, это будут целые выражения или даже предложения.
Вызываться они будут внутри циклов while и операторов if. Возможны и вызовы из тел этих операторов, порождающие вот такой
код:
if (employeeShouldHaveFullBenefits())
AddFullBenefitsToEmployee();

В результате ваш код будет читаться как хорошо написанная проза.
Выделение методов позволяет следовать правилу понижения (step­
down rule)1. Нужно сделать так, чтобы по мере чтения списка функций
мы последовательно опускались по уровням абстракции. Для этого
мы выделяем все фрагменты кода из функции, расположенной ниже
желаемого уровня.

1

Там же. — С. 61.

213

Часть I. Принятые практики

Выделение переменной
Если метод Extract Method считается самым важным из вариантов рефакторинга, то метод Extract Variable — его помощник. Оказывается,
процесс выделения методов часто приходится начинать с выделения
переменных.
В качестве примера рассмотрим рефакторинг из упражнения по созданию алгоритма подсчета очков в боулинге, которое мы выполняли
в главе 2. Вначале был вот такой код:
@Test
public void allOnes() throws Exception {
for (int i=0; i 60 && employee.salary > 150000)
ScheduleForEarlyRetirement(employee);

С помощью объясняющей переменной его можно сделать более читабельным:
boolean isEligibleForEarlyRetirement = employee.age > 60 && employee.
salary > 150000
if (isEligibleForEarlyRetirement)
ScheduleForEarlyRetirement(employee);

Выделение поля
Этот метод рефакторинга может оказывать глубоко положительный
эффект. Я использую его нечасто, но каждый раз, когда я к нему прибегаю, код существенно улучшается.
Все начинается с неудачного выделения метода. Рассмотрим класс,
преобразующий в отчет CSV-файл с данными. Его код несколько не
упорядочен.
public class NewCasesReporter {
public String makeReport(String countyCsv) {
int totalCases = 0;
Map stateCounts = new HashMap();
List counties = new ArrayList();
String[] lines = countyCsv.split("\n");
for (String line : lines) {
String[] tokens = line.split(",");
County county = new County();

1

Beck K. Smalltalk Best Practice Patterns. — Addison-Wesley, 1997. Р. 108.

215

Часть I. Принятые практики

county.county = tokens[0].trim();
county.state = tokens[1].trim();
// вычисление скользящего среднего
int lastDay = tokens.length - 1;
int firstDay = lastDay - 7 + 1;
if (firstDay < 2)
firstDay = 2;
double n = lastDay - firstDay + 1;
int sum = 0;
for (int day = firstDay; day