Код без тестов — легаси
Если вы работаете в IT, то о легаси вы слышите часто — обычно с множеством негативных коннотаций. Понятно, что это не «хороший код», но какой? Может старый, может не поддерживаемый или не обновляемый, а может просто чужой? Есть ли «полноценное» определение «легаси», на которое можно ссылаться? А когда разберемся — что нам делать с легаси? Попробуем разобраться. Спойлер: выводы неочевидны.

Автор — Николас Карло, веб-разработчик в Busbud (Монреаль, Канада). Специализируется на легаси. В свободное время организует митап Software Crafters и помогает с конференциями SoCraTes Canada и The Legacy of SoCraTes.
Данная статья была скомпилирована (и отредактирована) из двух статей Николаса: «What is Legacy Code? Is it code without tests?» и «The key points of Working Effectively with Legacy Code». Показалось логичным рассказать о том, что такое легаси, а потом — как с ним работать.
Что такое «легаси»?
Возможно, если вы задавались этим вопросом, то встречали определение от Майкла Физерса. Майкл выпустил книгу «Working Effectively with Legacy Code» в 2004 году, но она до сих пор актуальна. Комикс это отлично иллюстрирует.

В своей книге Майкл пишет своё определение:
«Для меня легаси — это просто код без тестов».
Почему Физерс так считает? Потому что по его многолетнему опыту без тестов обычно трудно узнать всё, что код умеет. Если тестов нет, то для понимания, что код делает, вам нужно внимательно его прочитать, воспроизвести программу в своей голове и представить все возможные сценарии. Потом вы поменяете код и нужно снова представить все сценарии. Или проверить их вручную, но всегда есть шанс что-то сломать.
Это хорошее определение: чаще всего тесты отсутствуют, так что это хорошее начало. Но это ещё не всё — есть нюансы.
Код с тестами также может быть легаси. Если вы читаете тесты, но не можете понять, что должен делать код — они отстой. Плохие тесты только мешают: тестируемый код так же трудно отрефакторить, как если бы у него не было тестов, а может даже и сложнее!
Тестов может и не быть, но код всё ещё легко можно отрефакторить. Возможно, вы поддерживаете небольшую кодовую базу без тестов, которую легко понять и рефакторить. Хотя, по моему опыту, это аномалия. Эту кодовую базу можно было бы проверить, но отсутствие автоматизированных тестов всё же не позволяет квалифицировать его как легаси.
Перейдём к моему определению легаси.
Легаси — это ценный код, который вы боитесь менять.
Например, мы ищем первопричину ошибки или выясняете, куда вставить свою функцию. Мы хотим поменять код, но это трудно, потому что непонятно как не нарушить существующее поведение. Готово — у нас легаси!
Мы переоцениваем сложность незнакомого кода. Поэтому мы думаем, что код, который писали не мы — устаревший. Это работает и с нашими прошлыми проектами, когда мы не можем понять, что закладывали и имели в виду, когда писали эту мешанину на экране.
Хорошие тесты помогают легко менять незнакомый код. А плохие тесты не помогают. Отсюда и определение Физерса.
С легаси помогает время. Парадоксально: обычно время превращает любой код в легаси, но чтобы его понять нам также помогает время. Если вы начали работать над легаси и это трудно — подождите. Да, большая часть кода ужасна, но вы привыкнете и лучше поймете его причуды и особенности.
Легаси не виновато в том, что оно такое. Большая часть кода ужасна, потому что это результат работы многих людей в течение долгого времени с противоречивыми требованиями и под давлением дедлайнов. Это Рецепт Устаревшего Кода™. Когда мало времени и недостаточно знаний — рождаются костыли (ну вы знаете). В конце концов, мы достигнем состояния, когда каждое движение приводит к ошибке, а реализация любой функции занимает целую вечность.
А теперь один из важнейших нюансов.
Легаси — это код, который мы изо всех сил пытаемся понять, чтобы поменять.
Легаси — это личная точка зрения. Устаревший код может стать проблемой для каждого разработчика команды. Какой-то код может показаться сложным, потому что мы его ещё не поняли, а какой-то понимаем, но всё равно чувствуем себя некомфортно, когда рефакторим. Но субъективное ощущение «легаси» зависит от нашего понимания кода, и наших чувств по поводу его изменения. Часто люди этого не понимают.
В итоге мы получаем, что легаси это:
который мы пытаемся понять, чтобы отрефакторить;
Как же эффективно работать с легаси?
Легаси — код, который мы пытаемся понять, чтобы отрефакторить. Задача рефакторинга в том, чтобы сохранить существующее поведение кода. Как без тестов мы будем уверены, что ничего не сломали? Нам нужна обратная связь. Автоматизированная обратная связь — ещё лучше.
Добавить тесты, а затем внести изменения
Логично, что если добавить тесты, они помогут его «прощупать» и он перестанет быть устаревшим. Поэтому первое, что нужно сделать — написать тесты. Только тогда мы будем в безопасности, чтобы рефакторить код.
Но чтобы запустить тесты, мы должны поменять код. Возникает парадокс легаси. Мы обречены? Нет. Поменяем как можно меньше кода для тестов:
Определим точки изменения — «швы».
Первые два пункта самые сложные, а как только доберёмся до тестов, мы знаем, что делать.
Найти «швы» для разрыва зависимостей
Обычно когда мы добавляем тесты к легаси возникает «проблема зависимостей»: код, который мы хотим протестировать, не может работать, потому что ему нужно что-то сложное для тестирования. Иногда это соединение с базой данных, иногда вызов на сторонний сервер, а иногда — параметр, который сложно создать. А чаще всё и сразу.
Чтобы протестировать код, нужно разбить эти зависимости в тестах. Для этого необходимо выявить «швы».
«Шов» — место, где можно изменить поведение программы, не меняя код.
«Швы» бывают разные. Если это объектно-ориентированный ЯП, то обычно это объект, например, в JavaScript.
Допустим, метод connect() вызывает проблемы, когда мы пытаемся поместить код в тесты. Получается, что весь класс — это «шов», который можно поменять. Можно расширить этот класс в тестах, чтобы предотвратить его подключение к реальной БД.
Есть и другие виды швов. Если язык позволяет изменять поведение кода без изменения исходного кода, у нас есть точка входа в написание тестов. Кстати о тестах…
Напишем unit-тесты
Дискуссии о лучших практиках тестирования обычно перерастают в холивары. Применять принцип пирамиды тестов, и писать максимум unit-тестов? Или использовать «Кубок тестирования» и писать в основном интеграционные?
Почему советы такие противоречивые? Потому что у них нет единого определения того, что такое «unit». Одни люди говорят об «интеграционных тестах» и тестируют всю библиотеку, а другие тестируют каждый класс по отдельности.
Чтобы избежать путаницы, Майкл даёт четкое определение того, что такое НЕ unit-тест:
он не работает быстро (< 100ms / test);
он взаимодействует с инфраструктурой, например, базой данных, сетью, файловой системой, переменными;
Напишите максимум тестов, которые обладают этими 2 качествами, при этом неважно, как вы их назовёте.
Иногда трудно написать такие тесты, потому что не понятно, что код должен делать. Тогда используйте специальную технику.
Тесты для определения характеристик
Это тесты, которые формализуют фактическое поведение части кода.
Вместо того чтобы писать комплексные модульные тесты, мы фиксируем текущее поведение кода — делаем снимок того, что он делает. Тест гарантирует, что это поведение не изменится!
Это мощная техника, потому что:
В большинстве систем то, что код делает важнее того, что он должен делать.
Мы можем быстро покрыть легаси с помощью этих тестов. Так мы подстрахуемся для рефакторинга.
Этот метод также называют «Approval Testing» («тестированием одобрения»), «Snapshot Testing» или «Golden Master».
Но обычно на всё это очень мало времени.
Когда совсем нет времени на рефакторинг
Несколько советов, если предыдущие не подходят.
Большие куски кода обладают «гравитацией» и привлекают ещё больше кода. «Теория разбитых окон» в действии: небольшой беспорядок влечёт за собой беспорядок серьёзнее. Если класс уже содержит 2000 строк, то какая разница, что вы добавите еще 3 if оператора и будете поддерживать класс длиной в 2010 строк?
Это всего лишь 3 if: тяжело себя убедить, что нужно потратить на них 2 дня, хотя и должны. Что делать, если действительно нет времени писать тесты для этого класса? Используйте техники Sprout (прорастание), Wrap (обёртывание) и скретч-рефакторинг.
Sprout
Напишите код в другом месте, сделайте один тест, определите, где вы должны вызвать этот код из существующего кода (точка вставки), и вызовите свой код из легаси.
Рассмотрим на примере:
Допустим, нам нужно убрать дубли файла entries, но postEntries() трудно проверить — нет на это времени. Мы можем «прорастить» код где-то ещё, например, в новом методе uniqueEntries(). Этот новый метод легко протестировать, потому что он изолирован. Затем вставим вызов этого метода в существующий, не проверенный код.
Минимальные изменения, минимальный риск. Можете «вырастить» один метод, целый класс или что-то ещё, что изолирует новый код.
Можно «обернуть» изменение, если оно должно произойти до или после существующего кода.
Переименуем старый метод, который хотим обернуть.
Создадим новый с тем же именем и подписью, что и старый.
Вызовем старый метод из нового.
Поместим новую логику до/после вызова другого метода.
Эту новую логику можно проверить, потому что старый метод — это «шов», который можно изменить в тестах. Помните предыдущий код?
Ещё один способ решить эту проблему — это обернуть её, поэтому мы переходим к postEntries(), списку записей, из которых мы удалили дубли.
В тестах мы бы изменили проблему postEntriesThatAreUnique(), чтобы проверить, работает ли логика удаления дубликатов. Разница может быть ещё больше.
Эти методы не идеальны, и у них есть недостатки. Но это полезные инструменты при работе с легаси. А при необходимости можно даже немного нарушить правила.
Скретч-рефакторинг
Сложно работать с кодом, который мы не писали, без тестов и с плохой документацией. Чтобы «выбраться» нам нужно разбить зависимости, написать тесты. Но с чего вообще начинать, когда код непонятен? Хорошая техника — скретч-рефакторинг.
Его цель в том, чтобы ознакомиться с кодом, а не менять. Мы «играем» с кодом столько, сколько захотим: извлекаем функции, упрощаем, переименовываем переменные. Как только сделаем, всё, что нам нужно — откатим всё обратно и начнём с правильных тестов.
Выводы
Легаси будет везде, где бы вы ни работали, в каждой кодовой базе. Можно сопротивляться и чувствовать себя плохо, когда вы застряли в нём. А можно рассматривать это как возможность. Работа со старым кодом это очень ценный навык, его надо изучать теоретически (почитайте книгу «Working Effectively with Legacy Code») и практиковать в ежедневных задачах.
Похожие и интересные статьи:
О том, над чем в целом мы тут работаем: монолит, монолит, опять монолит.
Кратко об истории Open Source — просто развлечься (да и статья хорошая).
Больше новостей про разработку в Додо Пицце я пишу в канале Dodo Pizza Mobile. Также подписывайтесь на чат Dodo Engineering, если хотите обсудить эту и другие наши статьи и подходы, а также на канал Dodo Engineering, где мы постим всё, что с нами интересного происходит.
А если хочешь присоединиться к нам в Dodo Engineering, то будем рады — сейчас у нас открыты вакансии iOS-разработчиков (а ещё для Android, frontend, SRE и других).
Что такое легаси в коде
Иногда программисты на вопрос, почему программа работает именно так, отвечают, что это «легаси» и исправить ничего нельзя. Разберёмся, что это значит, насколько это мешает разработке и что делают с легаси-кодом.
Что такое легаси
С английского legacy переводится как «наследие». Легаси-код — это код, который перешёл «по наследству» от предыдущих разработчиков. Чаще всего это происходит так:
- Команда делает продукт, внутри много разных возможностей.
- Часть функций со временем оптимизируется, а часть остаётся неизменной в виде старого кода, потому что и так работает.
- Некоторое время спустя в команде не остаётся тех, кто писал старый код.
- Текущая команда не знает, почему старый код написан именно так.
- В этих кусках сложно что-то поменять или разобраться, потому что всё остальное написано уже по-другому.
- Этот старый код, который сложно поддерживать и сложно разбираться — это и есть легаси.
Проще говоря, легаси — это код, про который говорят: «Это ещё Михалыч писал 8 лет назад для синхронизации с сервером, он работает, мы его не трогаем, потому что иначе всё сломается». При этом Михалыча в компании давно нет, документации тоже нет, и проще этот код не трогать совсем.
Так как легаси — это старый код, то обычно на него завязаны многие важные вещи в программе. Получается замкнутый круг: отказаться от легаси нельзя, потому что без него всё сломается, но и поддерживать его в рабочем состоянии тоже сложно, потому что никто не хочет разбираться в старом коде.
Откуда берётся легаси
Причин появления легаси может быть несколько:
- команда перешла на другой фреймворк, но части программы остались на старом;
- часть программы написана на старой версии языка;
- старая команда не задокументировала свой код;
- код написан в одном стиле, а команда давно перешла на другой стиль программирования.
Легаси — это не какое-то преступление, а часть жизни любой живой ИТ-компании. Рано или поздно у любого продукта появится легаси. И чем крупнее проект, тем больше его будет. Например, в исходном коде Windows 10 до сих пор остаются фрагменты кода, написанные ещё 20 лет назад для Windows 3.1.
Легаси — это плохо?
Легаси — это просто старый код, который нужно поддерживать наравне с новым. Если он работает — отлично, пусть живёт. Другое дело, что команде, например, было бы удобнее, чтобы код был написан не на старом фреймворке, а на новом, который знают все.
Получается, главный минус легаси-кода не в том, что он работает плохо, а в том, что его неудобно поддерживать.
Что значит «поддерживать старый код»?
Например, в старом коде для запроса к серверу идёт сначала адрес, а потом номер запроса. Спустя 10 лет требования сервера изменились, поэтому сначала должен идти запрос, а потом уже адрес. Значит, нужно изменить порядок полей в коде.
Если старый код понятен и хорошо задокументирован, на эту задачу уйдёт две минуты. Если это старые пыльные легаси-кишки, то это может стать задачей на час.
Что делать с легаси-кодом
Если легаси-код работает и не требует вмешательства и поддержки — то можно пока ничего не делать, пусть работает. Будет время — перепишем на новый фреймворк, а если нет, то и так пока поработает.
Что такое Legacy-код? 2 определения термина. 8 советов по работе с legacy кодом для облегчения жизни.
В этой статье простым языком даны 2 определения Legacy-кода. И 8 советов для упрощенной работы с Legacy кодом.
Если вы так или иначе связаны с программированием, то от ряда к ряду будете сталкиваться с понятием — Legacy-код. Разработчики часто “козыряют” этим терминамов, и обычно с достаточной долей негативной окраски.
Но что конкретно это значит? Просто старый код либо? Или чужой код? Как ни крути, из контекста понятно что этот код точно не хороший….
Если вы имеете достаточный опыт в разработке, то понимаете что не существует единственно верного определения. У каждого свое видение на этот счет, у всех разные представления о хорошем коде, “каждый строчит как он хочет”.
Это сборка из двух статей западного интернета, в частности из статей: “What is Legacy Code? Is it code without tests?” understandlegacycode.com и “What Is Legacy Code: 8 Tips For Working With Legacy Code” perforce.com.
⭕ Мы дадим 2 определения Legacy кода, от которых можно смело отталкиваться. По каждому из них дан небольшой поясняющий комментарий.
1 определение. Legacy-код — это код без тестов.
Возможно вы уже встречали такую трактовку. Майкл Фезерс в своей книге “Working Effectively With Legacy Code” дает четкое определение legacy кода:
Для меня legacy код – это просто код без тестов. To me, legacy code is simply code without tests.
Почему Майкл имеет такую точку зрения? Потому что без тестов, обычно чрезвычайно тяжело узнать всё что может код (или не может). Крепит иногда просто…
Чтобы разобраться в коде вам нужно просмотреть его “ручками”, прокрутить в голове его исполнение и предусмотреть все возможные сценарии поведения. В 8 случаях из 10 код без тестов невозможно подправить без существенных временных издержек на изучение.

И это весьма “рабочее” определение, оно передаёт суть. Чаще всего тестов просто нет. Это хорошая отправная точка. Но есть нюансы, и хотелось бы их обсудить, 2 важные особенности пропущены:
Код с тестами тоже без проблем может быть Legacy. Хреново написанные тесты “идут мимо”. Код даже может быть сложнее изменить если тесты сделаны непрофессионально. Если вы читаете тесты и ничего не понятно по коду — тесты г#вно. Протестированный код так же трудно поддерживать и изменять, как если бы их не было, если не еще хуже.
Код может не иметь тестов и без нареканий может поддерживаться. Мб вы поддерживаете небольшую codebase без тестов, но в которой все понятно, просто, и наглядно. Но обычно это не так.
2 определение. Legacy-код — это код, который проблематично поддерживать.
Более универсальная трактовка. Это личное определение Николаса Карло с сайта understandlegacycode.com.
Legacy код — это код имеющий ценность для продукта, но который вы боитесь менять. Legacy Code is valuable code you’re afraid to change.
Как вариант, вы наблюдаете баг. Или непонятно куда вставить свою функцию органично. Вы хотите изменить код, но вам трудно это сделать, потому что вы не знаете, как не нарушать существующее поведение. Это Legacy код.
Тут важно усвоить пару значимых моментов:
Незнакомый код это значительно. Мы склонны недооценивать сложность незнакомого кода. По этой причине вы думаете, что код перед вами, написанный не вами, legacy. Или смотря на код который вы написали 3-6 месяцев назад, и не можете припомнить всех мелких решений возникших в тот момент в голове.
Хорошие тесты помогут вам изменить незнакомый код. Отсюда и определение №1. Но плохие тесты не в кассу.
Становится проще через несколько месяцев, дорогу осилит идущий. Если вы начали работать над унаследованным проектом и боретесь с ним. Я не говорю, что код отличный – большая часть кода ужасная. Но вы привыкнете к нему и лучше поймете его причуды и особенности.
Большая часть кода ужасна, потому что он является результатом того, что многие люди работают над ним в течение длительного периода времени с противоречивыми требованиями и нехваткой времени. Legacy Code Recipe™. Имеем ограниченные знания и локально используем костыли чтобы вписаться в дедлайны. Это частая практика. Рано или поздно это приведет к достаточно плачевным последствиям — что не изменение, то вылезает баг.
Как работать с Legacy кодом. 8 принципов-советов для упрощения жизни
На бумаге и в теории просто “покумекать” над тем какой код плохой, и как это непрактично. Но на деле часто разработчики сталкиваются с необходимостью (необходимостью!) поддерживать и развивать код совершенно разного качества.
На таком фоне имеет смысл заручиться некоторыми принципами-правилами, которые упростят работу с Legacy кодом и сделают жизнь рядовому разработчику проще:
Совет #1. Тестируйте код
Один из способов понять код – создать characterization tests и модульные тесты. Вы также можете запустить статический анализатор над своим кодом для выявления потенциальных проблем.
Это поможет вам понять, что на самом деле делает код. И это выявит любые потенциально проблемные области. Как только вы поймете код, вы сможете вносить изменения с большей уверенностью.
Совет #2. Пересматривайте документацию
Просмотр документации с оригинальными требованиями поможет вам понять, откуда появился код. Откуда растут ноги.
Наличие этой документации поможет вам улучшить код без ущерба для системы. Без этой информации вы могли бы случайно внести изменения, которые привели бы к нежелательному поведению.
Совет #3. Не переписывайте код без реальной надобности
Переписывание устаревшей кодовой базы может быть заманчивым. Но обычно это ошибка.
Переписывание занимает слишком много времени и слишком много ресурсов программистов. И даже если вы сделаете это, переписывание кода может привести к появлению новых ошибок. Или это может удалить неявный на беглый взгляд функционал.
Совет #4. Пробуйте рефакторить код
Лучше попробовать рефакторинг устаревшей кодовой базы, а не переписывать ее. И лучше делать это постепенно, небольшими порциями.
Рефакторинг – это процесс изменения структуры кода – без изменения его функциональности.
Это делает код чище и облегчает понимание. Это также устраняет потенциальные ошибки. При рефакторинге унаследованного кода лучше всего:
- Рефакторинг кода, в котором уже есть модульные тесты – чтобы вы знали, что к чему;
- Начните код из “самой ямы”, самое узкое звено — его будет легче всего рефакторить (Start with the deepest point of your code — it will be easiest to refactor) ;
- Тесты после рефакторинга — чтобы знать наверняка, что ничего не поломано;
- Имейте запас прочности, готовность к непредвиденным ситуациям. Действуйте по принципам CI — чтобы вы могли условно безболезненно откатить build.
Совет #5. Вносите правки последовательно
Не делайте слишком много изменений одновременно. Плохо проводить рефакторинг параллельно с функциональными изменениями.
Кроме того, это облегчает проверку кода. Отдельные изменения гораздо более очевидны, чем куча изменений разом.
Совет #6. Сотрудничайте с другими разработчиками
Примите, вы можете не очень хорошо знать codebase. Но некоторые из ваших коллег-разработчиков, возможно, знают хорошо. Намного быстрее задавать вопросы тем, кто лучше всех знает код более цельно.
Так что, если это возможно, сотрудничайте с кем-то, кто знает это лучше, чем вы. Второй взгляд на код может помочь вам лучше понять его.
Совет #7. Пишите новый код чисто
Есть способ не сделать код более проблематичным. И это благодаря тому, что новый код чист.
Вы не можете контролировать качество устаревшей кодовой базы. Но вы можете убедиться, что код, который вы добавляете, весьма недурен.
Совет #8. Не стойте на месте
Работа с устаревшей кодовой базой становится легче со временем. Младший разработчик может не понимать, почему кодовая база не подверглась рефакторингу (и может быть заинтересован в ее рефакторинге). Но “твердый” разработчик будет знать, когда лучше все оставить как есть.
Разобравшись детально в Codebase, вы сможете улучшить ее.
Для заминки добавим две книги рекомендации по работе в Legacy кодом: “Working Effectively With Legacy Code” by Michael C. Feathers и “Refactoring: Improving the Design of Existing Code” by Martin Fowler.