В главе "5: Роли agile" http://www.intuit.ru/studies/courses/3505/747/lecture/26297?page=2 есть упоминание: Скримшир [Scrimshire сайт] но нет адреса сайта. Укажите пожалуйста адрес в тексте лекции. Или речь о человеке James Scrimshire? |
Практики agile: технические
7.4 Рефакторинг
Agile альтернатива "предваряющему анализу" состоит в адаптации постоянно сменяющихся последовательных версий программы, поиске в проекте и коде "плохо пахнущих" участков (неудовлетворительных элементов) и их коррекции. Этот процесс известен как рефакторинг.
7.4.1 Концепция рефакторинга
Типичным примером "плохо пахнущего кода" является дублирование. Всегда плохо в разных частях программы иметь тот же самый код или почти тот же: две части необходимо отлаживать, две части необходимо корректировать, если возникает потребность, две части следует изменять при смене требований.
Типичный рефакторинг дублирования состоит в применении абстракции – выделение отдельного модуля: в объектно-ориентированном программировании дублируемый код помещается в новый создаваемый класс, представляющий общую абстракцию, существующие классы становятся наследниками абстрактного класса.
Описанный прием – это один из способов удаления дублирования и не всегда самый подходящий. Программисты выполняют рефакторинг, идентифицируя "запах кода", находя в каждом случае, будет ли известный образец (паттерн) рефакторинга применим и желателен.
Некоторые приемы рефакторинга менее монументальны, но все же полезны. Например, можно изменять имя метода или поля класса в целях внесения ясности или согласованности.
Современные среды разработки включают инструментарий, позволяющий некоторые виды рефакторинга выполнять автоматически.
Не каждому образцу программных изменений соответствует паттерн рефакторинга. Необходимо выполнение двух условий:
- рефакторинг не должен изменять семантику программы;
- рефакторинг должен улучшать качество кода или архитектуры.
Первое условие означает, что программа после рефакторинга должна давать те же результаты, как и перед проведением рефакторинга. Рефакторинг не означает поиск и устранение ошибок, изменение функциональности, даже пользовательский интерфейс не меняется при рефакторинге. Эти виды изменений также необходимы, но рефакторинг направлен только на улучшение качества архитектуры.
Из требования сохранения функциональности вытекает необходимость автоматической поддержки рефакторинга. Даже такое концептуально простое изменение, как переименование, не только громоздко, но и чревато ошибками, когда выполняется вручную, так как имя должно измениться не только в определении, но и во всех точках, где происходит обращение к этому имени. Другими словами, преимущество инструментария рефакторинга в том, что изменения выполняются не только автоматически, но и безопасно.
В соответствии со вторым условием требуется определить само понятие качества кода и, что более важно, качества архитектуры. В то время как нет единого всеобъемлющего определения, современная литература по проектированию предлагает ряд критериев. Ясно, например, что плохо спроектирован класс, содержащий единственный метод, плоха архитектура, содержащая глубокую и единственную ветвь наследования (где каждый класс, не являющийся корнем или листом, содержит ровно одного родителя и одного потомка). Эти примеры содержат признаки плохого проектирования, указывая на потенциально "плохо пахнущий код". (К слову, легче указать примеры плохого проектирования – "антиобразцы", чем примеры проектирования архитектуры высокого качества).
У Бека дается более специфическое условие: рефакторинг должен "упростить проект". Его понятие простоты включает отсутствие дублирования, минимальное число классов, минимальное число методов.
7.4.2 Преимущества и пределы рефакторинга
Внимание к важности рефакторинга было одним из наиболее видимых эффектов agile методов, в особенности экстремального программирования. Рефакторинг стал одним из принципиальных инструментов современного программиста.
Как и в случае многих других идей, положительный вклад (используйте эту технику) более интересен, чем негативный (эти приемы являются заменой традиционных). Стремление всегда искать возможные улучшения архитектуры прекрасно. Но рефакторинг не дает права отрицать необходимость предваряющего анализа. Если вы не уделили должного внимания начальному этапу проектирования, а просто строите "простейшее решение, которое еще может работать", то можно снова и снова переделывать проект, поскольку начальное решение, хотя и работающее, адаптируется с большим трудом. В описании этого процесса Бек также указывает на его ограничения:
Не всякий рефакторинг может быть выполнен в течение нескольких минут. Если вы обнаружите, что построили запутанную иерархическую структуру, то на ее распутывание может потребоваться месяц упорной работы. Но у вас нет месяца на такую работу, поскольку нужно поставлять очередную историю до окончания итерации.
В этом случае большой рефакторинг нужно выполнять малыми шагами (возрастющие изменения). Находясь в центре тестового варианта, вы найдете шанс сделать один шаг на пути к большой цели. Сделайте этот шаг. Передвиньте метод сюда, а переменную туда. Постепенно от большого рефакторинга останется малая часть. Тогда вы сможете закончить ее в течение нескольких минут.
По-настоящему "большой рефакторинг" не является суммой малых рефакторингов. Я знаком с проектом компилятора, в котором команда в некоторый момент не справилась с потерей производительности. Причиной была неэффективная структура данных, хранящая трек элементов системы – классов и методов. При повторном проектировании удалось идентифицировать каждый элемент одним целым числом, а не сложным объектом, как ранее. Но это была большая система – с тысячами классов, более чем двумя миллионами строк кода, для которой такой хирургический рефакторинг – ненавистная операция. Изменения запутанны, болезненны, влияют почти на каждый модуль системы, не привнося никакой новой функциональности. Единственное преимущество – существенное улучшение скорости и прочный фундамент для дальнейших разработок. Если вы решитесь на такой рефакторинг, то нет способа двигаться "малыми шагами", невозможно в одной части системы использовать целые, а в другой объекты. Вы должны согласиться на "месяц упорной работы", а может, и на больший срок. Вы можете найти, что "овчинка выделки не стоит", но решение идти вперед – это выбор "все или ничего".
Совет Бека – еще один случай "неправомерного обобщения". Некоторые изменения могут носить "возрастающий характер": сделайте немножко здесь, немножко там, и в одно прекрасное утро вы, к своей радости, увидите, что осталось совсем чуть-чуть, что можно выполнить за несколько минут. Переименовать классы и методы для большей согласованности, локально изменить отношения наследования между малым числом классов, превратить атрибут (поле класса) в локальную переменную – типичные примеры простого рефакторинга. Но некоторые изменения не могут идти таким путем.
Было бы прекрасно поверить в истинность мантры – "начните с простейшей вещи, которая может работать, и путем возрастающих улучшений вы придете к архитектуре, образующей великолепный продукт". К несчастью, это не тот случай. Применим старый принцип GIGO – "мусор сюда, мусор туда" (Garbage In – Garbage Out)3В русском языке есть пословица: "Красна печь пирогами, а изба углами". Если вы хотите, чтобы изба была прекрасной, то мусор нужно вымести из всех углов, а не мести его из одного угла в другой.. Как отмечалось в предыдущей главе, хлам от рефакторинга остается хламом. Это наблюдение не означает осуждение рефакторинга. Просто отмечается, что рефакторинг хорошо работает, когда
- применяется к изначально прочной, хотя и несовершенной архитектуре;
- комбинируется с предваряющим анализом.
В следующих двух разделах обсуждаются детали этих двух утверждений.
7.4.3 Непредвиденные и существенные изменения
Начальное проектирование может привести к несовершенной архитектуре по двум причинам: что-то не учли или вследствие существенного недопонимания. Непредвиденные несовершенства можно скорректировать через рефакторинг, но существенные нет. Несогласованное именование – непредвиденный фактор, ошибочный выбор абстракции – существенный. Проблемы компилятора, обсуждаемые выше, были примером существенного несовершенства. Вот еще один.
Вы создали множество классов, описывающих тесно связанные концепции, скажем, работы в компании. У вас есть также список объектов этих типов. Через некоторое время вы осознаете, что вам необходима новая функциональность, применимая ко всем объектам из списка. Например, вы хотите распечатать содержимое списка, так что придется добавить в каждый класс метод "print". Затем вы добавляете в классы метод "encode", позволяющий компактное хранение объектов. В следующий раз необходим метод, создающий XML форму.
Это все функциональные изменения, не рефакторинг, и вы их уже выполнили. Но вы осознаете, что такие ситуации будут возникать и в будущем, так что приходите к решению остановить постоянную модификацию существующих классов. Возможно, это произойдет и помимо вашей воли, поскольку классы уже попали в повторно используемую библиотеку и вышли из-под вашего контроля.
Техническое решение хорошо известно: следует использовать паттерн "Посетитель" (Visitor), который позволяет выполнять произвольные операции над произвольными экземплярами класса, где сами операции определены где-нибудь, совсем необязательно в самом классе. Адаптация этого решения требует одного изменения, применимого ко всем классам, – нужно сделать классы "посещаемыми" путем наследования их от общего класса Visitor с подходящим методом "update". Вы должны также удалить лишний для классов код, который был добавлен как временное решение, и поместить его в нужное место. Вы решаете, что долговременные преимущества гибкости проекта стоят кратковременной боли.
Это важное изменение. Оно требует, может быть, не месяца работы, но, по меньшей мере, нескольких дней в зависимости от числа классов и влияния на другие части архитектуры. Для достижения лучших результатов нежелательно выполнять их небольшими шажками. Лучше выполнить эту операцию за один присест.
Во избежание попадания в такие ситуации нет другого выхода, кроме предваряющего анализа. Даже и в этом случае не всегда возможно совершенное предвидение. Когда такие ситуации возникают, это приводит к повторному проектированию в глубину, не покрываемому никаким видом возрастающего рефакторинга, предлагаемого XP и другими agile подходами.
Понимание разницы между непредвиденным и существенным – это ключ к проблеме расширяемости системы, определяющий границы применимости рефакторинга. Различие связано с тем, что мы уже изучали ранее, – различие между аддитивной и мультипликативной сложностью. В целом изменение является непредвиденным, когда оно воздействует на аддитивные элементы – функциональность с немногими зависимостями с другими частями системы. Если же зависимости сложны (имеют мультипликативную сложность), изменения носят существенный характер и не могут быть устранены простым рефакторингом.
7.4.4 Комбинирование априорного и апостериорного подходов
Рефакторинг, рассматриваемый защитниками agile как техника "Или – Или", может быть лучше использован как "И" техника. Он работает наилучшим образом, когда комбинируется с идеями, отвергаемыми аджилистами, в данном случае с предваряющим анализом.
Никакое количество рефакторинга неспособно скорректировать дефектную архитектуру. Первичная обязанность любого проектировщика – идентифицировать фундаментальные абстракции, составляющие скелет архитектуры. Сделаете это нужным образом, и вам останется еще много работы, которую необходимо выполнить. Но если ошибиться на этом этапе, в конечном счете вам придется (метафору выберите сами) ставить латку на латку, тушить огонь керосином, лепить пластырь на пластырь.
Если архитектура не отвечает сути, то нет другого способа, кроме ее перестройки, каких бы усилий это ни стоило. Когда суть схвачена, то это еще не означает, что вы выбрались из леса, поскольку не все прекрасно и несовершенства будут подстерегать вас, но здесь помогает рефакторинг.
Agile методы учат нас, что никогда нельзя терять готовность критически рассматривать собственные проектные решения, мы должны быть способны чувствовать запахи "плохо пахнущего кода и архитектуры", идентифицировать их и исправлять на лету.
7.5 Разработка "Вначале Тест" и разработка, управляемая тестами
Заключительная техническая практика, рассматриваемая в этой главе, является некоторым экстремальным следствием той центральной роли, которую подходы agile, начиная с XP, отводят тестам. Идея, предварительно рассмотренная при обсуждении принципов, состоит в том, что разработка должна быть разработкой, управляемой тестами, для краткости TDD (Test Driven Development). Разработка TDD является следствием TFD (Test First Development) – "Вначале Тест" разработки.
7.5.1 TDD метод программной разработки
TDD не является приемом тестирования – это полноценный метод разработки ПО. В начале книги, в которой раскрывается эта идея, Бек [Beck 2003] определяет TDD как повторение следующего основного цикла:
- Быстро добавить тест.
- Выполнить все тесты и увидеть, что новый тест "падает".
- Выполнить небольшое изменение системы.
- Убедиться, что все тесты проходят.
- Выполнить рефакторинг, удаляя дублирование.
Так работаем с самого начала, когда тест добавляется в первоначально пустую базу проекта. Процесс, определенный таким образом, имеет четыре важных следствия.
Первое следствие: тест всегда пишется, прежде чем создается соответствующий программный элемент. Если здесь остановиться, то получим TFD (разработку "вначале тест"), которая является подмножеством TDD, но только подмножеством, поскольку опущены шаги 2, 4 и 5 основного цикла TDD.
Второе следствие: процесс непрерывно возрастает, каждый раз добавляется новый тест, позволяющий проверить новую добавляемую функциональность или ранее не обработанный вариант использования.
Без шага 5 мы пришли бы к чистому, дилетантскому стилю разработки: обработай одно входное значение, обнови соответственно код, добавь следующее и так далее. Мы могли бы, в конечном счете, иметь проект в виде одного огромного "if … then …elseif … else…" с одной ветвью для каждого значения, встречаемого в тестах. TDD умнее, конечно, и шаг 5 является ключом – после каждого добавления кода выполняется рефакторинг. Нельзя быть полностью счастливым, когда проходит тест, поскольку нужно еще позаботиться о качестве полученной архитектуры и, если она недостаточно хороша (недостаточно проста по Беку, например, обрабатывает один случай за другим вместо того, чтобы некоторым образом унифицировать их), следует исправить ее, прежде чем двигаться дальше.
Четвертым следствием является правило, которое мы уже обсуждали при рассмотрении принципов agile. Оно отражено в четвертом шаге основного цикла – не идти дальше, пока все тесты не завершатся успешно. И это второй секрет (наряду с рефакторингом), благодаря которому разработка не превращается в дилетантскую рутинную работу. Если бы требовалось при внесении изменения просто проверить, что проходит тест, подготовленный для этого изменения, то жизнь была бы проста. Но следует убедиться, что внесенные изменения не изменяют корректность работы всей остальной функциональности проекта, не становятся причиной нарушения регрессионных тестов. Весь набор регрессионных тестов должен успешно выполняться на каждом шаге разработки, и этот набор пополняется с каждым новым шагом, он растет вместе с проектом, обеспечивая больше гарантий качества в сравнении с правилом, что все тесты должны всегда проходить.
Формулировка шага 2, на первый взгляд, кажется удивительной: почему мы должны ожидать, что тест не пройдет? Однако это согласуется с TDD как с методом разработки, так как метод запрещает реализовать новую функциональность до того, как для ее тестирования не будет написан тест. Следовательно, новый тест проверяет нечто большее, чем уже реализованная функциональность, поэтому он не должен проходить, пока не будет успешно реализована новая функциональность, для проверки которой он и написан. Наиболее очевидным примером является начало разработки, когда никакой код не написан, мы имеем дело с пустой программой, которая должна падать на любом нетривиальном тесте. Позднее, в принципе, возможно, что вновь созданный тест будет успешно проходить на уже работающей программе, но с позиций TDD это неправильный, неинтересный тест, поскольку он не ломается на некорректно реализованной функциональности.
А кстати, что такое тест? TDD имеет смысл только в сочетании с современной технологией тестирования, обеспечивающей механизм для подготовки многочисленных тестов, каждый описываемый множеством входов и ожидаемых результатов, автоматического выполнения всех этих тестов (регрессионного набора). Инструментарий, получивший общее имя xUnit, разработан, что, впрочем, неудивительно, людьми, предложившими XP. Он будет обсуждаться в главе, посвященной артефактам agile. Там мы рассмотрим, как задавать входные данные для теста, как специфицировать ожидаемые свойства результатов (этот механизм называется оракулом), о форме условий, которые должны выполняться, известных как "утверждения". Инструментарий позволяет затем автоматически запускать сотни тысяч точно определенных тестов и оценивать полученные результаты.
7.5.2 Оценка TFD и TDD
TFD и TDD внесли важный вклад в современную программную инженерию. Оценку достоинств этого вклада оставим напоследок, а начнем с тех аспектов, которые заслуживают некоторой критики.
Наиболее спорная идея, неявно высказанная в TDD, но лежащая в основе всего подхода, состоит в предположении, что тесты – это все, что нужно для специфицирования программы. Это очень плохая идея. Многое из сказанного ранее по поводу того, что сценарии и пользовательские истории не обладают достаточной общностью спецификации, применимо и здесь. На самом деле даже в большей степени, поскольку тест более специфичен, чем пользовательская история. В ранее упомянутом примере тесты, которые для начального отрезка целых вырабатывают значения 0, 1, 4, 9, 25, не обладают той общностью абстракции, которую несет задание функции .
Действительно, с ростом набора тестов все меньше вероятность того, что некоторый вариант данных будет вести себя непредсказуемо и не будет охвачен набором тестов. Но малая вероятность не означает невозможность возникновения варианта. Многие отказы в программных системах связаны именно со специальными случаями, не охваченными тестами. Написание спецификаций означает абстрагирование от частных случаев и поиск общих правил. Следует также отметить, что тесты можно генерировать по спецификациям (в этом направлении проводится масса исследований в программной инженерии), но нет способа генерировать спецификации по тестам.
Возникают вопросы, связанные с еще одним аспектом TDD, правда, по другим причинам: следует ли запрещать двигаться дальше, разрабатывая новую функциональность, пока не пройдут все тесты. Доводы "за" и "против" этого принципа обсуждались в "Принципы agile" .
На практике немногие организации применяют строгий процесс TDD в форме повторяющейся последовательности шагов, описанной выше. Реальный вклад имеет идея "вначале создай тест", а фактически более общая идея – "каждый новый код должен сопровождаться новыми тестами". Не так уж критично, что раньше – код или тест, никогда не создавайте одно без другого.
Эта идея получила широкое распространение, она должна приниматься как универсальная идея. В этом один из главных вкладов agile методов.