Получение характеристик кода для рефакторинга

Как внести изменения в унаследованный производственный код без нежелательных побочных эффектов

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

Б.Дж. Оллмон, директор по технологии и производству, IQ Innovations (iQity)

Б.Дж. Оллмон (B. J. Allmon) ― опытный специалист по гибкой разработке программного обеспечения и директор по технологии и производству компании IQ Innovations. Кроме того, он ― писатель, лектор, автор песен и основатель некоммерческой организации Experience Foundation, которая предоставляет возможности по обучению и производственной практике начинающим программистам. Адрес Б.Дж.Оллмона в Twitter: @bjallmon.



24.05.2013

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

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

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

Остерегайтесь внесения изменений "на скорую руку"

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

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

Кто-то создал новую запись о дефекте, в которой описывает проблему, связанную с возможными состояниями системы. Похоже, что для легенд типа Feature отсутствует состояние Ship It! (в отгрузку), как показано в карточке на рисунке 1.

Рисунок 1. В легенде Feature отсутствует одно из состояний
Скриншот гипотетической системы отслеживания функций, где в функции отсутствует состояние Ship It!

На странице системы с формой для редактирования легенд вместо строки Ship It! в списке доступных состояний появляется вопросительный знак в угловых скобках (<?>), как показано на рисунке 2.

Рисунок 2. Вместо пункта Ship It! форма отображает <?>
Вместо пункта Ship It! форма отображает <?>

Более того, состояние Ship It! отсутствует как в представлении списка, так и в форме для редактирования. В результате расследования быстро обнаруживается, что проблема кроется в перечислении StoryStatus enum, показанном в листинге 1.

Листинг 1. StoryStatus enum
package com.experiencefoundation.stories

public enum StoryStatus {

    NEW,
    TODO,
    IN_PROGRESS,
    DONE_ISH,
    QA,
    STAGING,
    SHIP_IT

    public String friendlyName() {
        if("NEW".equals(name())) {
            return "New"
        } else if("TODO".equals(name())) {
            return "Todo"
        } else if("IN_PROGRESS".equals(name())) {
            return "In Progress"
        } else if("DONE_ISH".equals(name())) {
            return "Done'ish"
        } else if("QA".equals(name())) {
            return "QA"
        } else if("STAGING".equals(name())) {
            return "Staging"
        } else if("SHIP_IT!".equals(name())) {
            return "Ship It!"
        } else {
            return "<?>"
        }
    }

}

Как видно из листинга 1, дефект является результатом попытки подставить жестко запрограммированное значение SHIP_IT! вместо имени значения enumSHIP_IT. Жестко запрограммированное значение содержит восклицательный знак (!), а в имени его нет, в результате чего приложение переходит к последнему оператору return через блок else. Поэтому всякий раз, когда StoryStatus является экземпляром StoryStatus.SHIP_IT, метод friendlyName() возвращает <?>.

В листинге 1 есть также несколько типичных проблем «уродливого кода»:

Исправления "на скорую руку" – от дьявола

В книге Практика гибкой разработки Венката Субраманиама и Энди Ханта (см. раздел Ресурсы) говорится, что исправления, выполненные "на скорую руку", сродни зыбучим пескам: ясность кода уходит, уступая место путанице. Авторы дают следующие рекомендации:

  • хорошо изучите архитектуру и дизайн;
  • разберитесь в коде, который вы изменяете;
  • избегайте скоропалительных решений;
  • устраняйте проблему, а не симптомы;
  • не программируйте в изоляции (например, используйте рецензирование кода);
  • выполняйте модульные тесты.

Следование этим рекомендациям позволит повысить производительность труда группы и получить чистый код.

  • в метод friendlyName() введена цикломатическая сложность вместо того, чтобы просто использовать возможности перечисляемого типа enum (в этом методе даже можно использовать простое сопоставление, если код выходит за пределы enum);
  • в коде отдается предпочтение дублированию вместо использования метода с аргументами — опять же в пренебрежение возможностями типа enum;
  • большое количество операторов return приводит к многословности;
  • подобный метод должен занимать меньше пяти строк кода Groovy; в представленном виде он неоправданно длинен;
  • код содержит много жестко закодированных значений. Такие значения и увлечение "магическими числами" создают путаницу в больших приложениях и могут быть опасны, особенно при отсутствии тестирования.

Чтобы исправить значение оператора return в нарушенном состоянии, нужно знать, зависят ли от SHIP_IT какие-либо другие элементы приложения, но это неизвестно даже в таком довольно тривиальном примере. Во многих более сложных случаях эта неопределенность неизбежно ведет к тому, что «исправление» на скорую руку нарушит что-то другое.

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


Приступаем к определению характеристик кода

Уровень абстракции

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

Если оказалось, что проблема связана с функционированием пользовательского интерфейса, то для того чтобы исправить ее, следует написать функциональный тест. Для этого существуют многочисленные инструменты, включая Selenium, Watir, Cucumber with Watir, HtmlUnit и Geb. Если обнаружилась проблема в JavaScript, рассмотрите возможность использования Jasmine для непосредственного модульного тестирования JavaScript. Можно также подумать о сочетании разных инструментов тестирования для обеспечения адекватного тестового покрытие более широкой проблемной области. В конечном итоге вы должны выбрать то, что наиболее целесообразно. (См. ссылки на дополнительные сведения об упомянутых здесь средствах тестирования в разделе Ресурсы.)

Один из способов приступить к характеристическому тестированию ― поиск ссылок на проблемный код, который необходимо изменить, или обращений к нему. Результаты поиска помогут составить архитектурную картину (легенду) того, как в настоящее время используется код. Найденные обращения укажут и наивысший уровень абстракции, который должны охватывать характеристические тесты. Другими словами, поиск помогает определить, откуда начинать написание модульных тестов. А также какого типа тесты необходимы для достижения наибольшего успеха при наилучшем возможном покрытии. Тесты покажут, что происходит при текущем поведении, а не то, что должно происходить. Однако текущее поведение не следует изменять до тех пор, пока не собрана вся информация; затем можно написать тесты с целью фактического рефакторинга кода.

К счастью, после поиска ссылок с помощью своего предпочтительного инструмента или среды разработки вы обнаружите, что в примере StoryStatus такие ссылки есть всего лишь на нескольких страницах пользовательского интерфейса и в самом классе Story. Страницы пользовательского интерфейса отображают наглядное имя, которое берется из StoryStatus enum, и ничего более. (Однако если функциональных тестов для этих страниц представлений нет, то их обязательно нужно создать.)

Вот фрагмент примера того, что вы увидите в формах создания и редактирования Story:

<g:select name="storyStatus" from="${StoryStatus?.values()*.friendlyName()}"
keys="${StoryStatus?.values()*.name()}" value="${storyInstance?.storyStatus?.name()}"/>

Как видите, тег storyStatus select непосредственно использует значения friendlyName() из StoryStatus enum только для целей отображения. Тег <g:select .../> ― это библиотека тегов Grails.

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

Тесты, написанные для enum, обеспечат обратную связь при внесении любых изменений в значения или размер enum. Эта обратная связь вынуждает изменять ожидаемое покрытие теста в случае необходимости и при выполнении автоматических тестов. Также будет полезно добавить тестовое покрытие специальных методов, таких как friendlyName(), как показано в листинге 2.

Листинг 2. StoryStatusTests обеспечивает необходимое покрытие
package com.experiencefoundation.stories

import org.junit.Test

class StoryStatusTests {

    @Test
    public void validateStoryStatusSizeAndValues() {
        def expectedValues = [
                StoryStatus.NEW,
                StoryStatus.TODO,
                StoryStatus.IN_PROGRESS,
                StoryStatus.DONE_ISH,
                StoryStatus.QA,
                StoryStatus.STAGING,
                StoryStatus.SHIP_IT
        ]
        assert expectedValues == StoryStatus.values()
    }

    @Test
    public void returnsFriendlyNameForStoryStatus() {
        def expectedValues = [
                "New",
                "Todo",
                "In Progress",
                "Done'ish",
                "QA",
                "Staging",
                "<?>"
        ]
        assert expectedValues == StoryStatus.values()*.friendlyName()
    }
}

Класс StoryStatusTests тестирует текущее поведение приложения. Обратите внимание, что из теста returnsFriendlyNameForStoryStatus() видно, что ожидаемым значением после Staging действительно является <?>.

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

Листинг 3. Измененный StoryStatusTests, который возвращает "Ship It!"
@Test
public void returnsFriendlyNameForStoryStatus() {
    def expectedValues = [
        "New",
        "Todo",
        "In Progress",
        "Done'ish",
        "QA",
        "Staging",
        "Ship It!"
    ]
    assert expectedValues == StoryStatus.values()*.friendlyName()
}

Отредактированный тест из листинга 3 показывает, что единственное, что требуется изменить, это список expectedValues, и делается это путем замены <?> на Ship It!

Тест будет неудачным, потому что результат пока находится в нарушенном состоянии. Это видно на рисунке 3.

Рисунок 3. StoryStatusTests демонстрирует красную полосу, и это не случайно
Скриншот с неудачным результатом StoryStatusTests

Теперь, когда мы получили «правильный» отказ, можно перейти к исправлению дефектного кода. В листинге 4 демонстрируется, как исправить код, используя возможности перечисляемого типа enum и инъекцию конструктора.

Листинг 4. Исправленный код
package com.experiencefoundation.stories

public enum StoryStatus {

    NEW("New"),
    TODO("Todo"),
    IN_PROGRESS("In Progress"),
    DONE_ISH("Done'ish"),
    QA("QA"),
    STAGING("Staging"),
    SHIP_IT("Ship It!")

    private String friendlyName

    public StoryStatus(String friendlyName) {
        this.friendlyName = friendlyName
    }

    public String friendlyName() {
        this.friendlyName
    }

}

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

Рисунок 4. Список в исправленном виде
Скриншот правильного представления списка

Новое представление формы показано на рисунке 5.

Рисунок 5. Форма в исправленном виде
Скриншот правильного представления формы

Измерение результатов рефакторинга

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

grails test-app -coverage

Результат показан на рисунке 6.

Рисунок 6. Результат измерения покрытия кода с помощью Cobertura
Результат измерения покрытия кода с помощью Cobertura

Результат положительный. При уже имеющемся тестовом покрытии, плюс новые модульные тесты, составленные для StoryStatus enum в ходе рефакторинга, Cobertura показывает почти 100%-й охват.


Заключение

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

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

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

Ресурсы

Научиться

  • Оригинал статьи: Characterizing code for refactoring.
  • Working Effectively with Legacy Code (Michael Feathers, Prentice Hall, 2004): книга о рефакторинге унаследованных версий кода и TDD.
  • Practices of an Agile Developer: Working in the Real World (Venkat Subramanian and Andy Hunt, Pragmatic Bookshelf, 2005): о привычках, идеях и подходах опытных программистов, практикующих гибкую разработку программного обеспечения.
  • Kanban: Kanban помогает группам разработчиков сделать модель непрерывного рабочего процесса наглядной.
  • Grails: подробнее о Web-платформе разработки Grails.
  • Groovy: Grails использует динамический язык для JVM Groovy.

Получить продукты и технологии

  • Selenium, Watir и HtmlUnit: эти инструменты позволяют создавать автоматизированные функциональные тесты, которые работают в браузере.
  • Cucumber или Geb: попробуйте разработку на основе поведения, поработав с Cucumber или Geb (Groovy-средой).
  • Jasmine: используйте TDD для своего кода JavaScript с помощью Jasmine.
  • Cobertura и Emma: оценивайте тестовое покрытие с помощью этих и подобных им инструментов.

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=931276
ArticleTitle=Получение характеристик кода для рефакторинга
publish-date=05242013