О Legacy-коде без максимализма: что делать

fcb8e796da2c45da2ead4512fdb0185e.png

Представьте, что вам дали задачу поправить часть кода. В голове возникнет много мыслей. Кто его написал? Когда? А может он — legacy? Где документация? Давайте попробуем разобраться с «наследием» основательно и со всех сторон. Поможет нам в этом вопросе Андрей Солнцев @asolntsev (http://asolntsev.github.io/), разработчик из таллинской компании Codeborne. Начнём.

252a71f8da0a3df1ac93c938a5391fce.jpg— Андрей, вы знакомы с трудами Michael Feathers, например, «Working Effectively with Legacy Code»? В книге акцентируется внимание на важности тестирования и выделяется одно из ключевых отличий legacy от не legacy-кода — это наличие тестов. Вы согласны с этим мнением?

Абсолютно согласен! Скажу больше: юнит-тесты — необходимое, но недостаточное условие. И с юнит-тестами можно навалить так, что сам Геракл не разгребёт.
Что для настоящего джедая мастхав, так это:

  1. TDD — то есть тесты ДО кода.
  2. Чистый код (и чистые тесты).

Я очень люблю книгу Robert C. Martin «Clean Code» («Чистый код»). Это для меня настольная библия. Категорически всем советую. Кстати, его блог тоже великолепен.


— С чистым кодом понятно. А при чём тут TDD? Код без багов — это ведь не то же самое, что хороший код?

Все думают, что TDD — это про тесты, а значит, это скучно.
Ерунда!
TDD — это про разработку (test driven DEVELOPMENT). Это способ создания кода таким, чтобы он был НЕ legacy. Таким, чтобы его было возможно поддерживать: отлаживать, исправлять, рефакторить, дорабатывать. На данный момент это единственный известный человечеству способ. Всё остальное — от лукавого, не дайте себя обмануть.

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

TDD нужен вовсе не для того, чтобы находить баги.
Тест — это первое использование твоего кода. Шерлок Холмс говорил: «Сначала собираешь факты, а уж потом выстраиваешь теорию. Иначе ты подсознательно начнёшь подтасовывать факты под свою теорию». Ты даже сам не заметишь, как это случится! Так же и с кодом: когда ты пишешь тест до кода, ты вынужден подумать о том, как его удобнее использовать, как назвать класс, метод; какие параметры передать. А если сначала написать код, то начнёшь подтасовывать тесты под него. В итоге доля тестов окажется слишком сложна, другая — трудна для исправления, а часть останется ненаписанной. И вот перед нами legacy-код собственной персоной!

— В продолжение разговора о тестировании — на ваш взгляд, какие тесты корректны и в каких ситуациях, например, юнит-тесты?

Начнём с юнит-тестов. Они необходимы в каждом проекте, тут без вариантов.

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

— А каким должно быть покрытие кода тестами? Достаточно ли 70%? Как не переборщить с «диагностикой» проекта?

Безусловно, нужно стремиться к максимальному покрытию. Все эти разговоры про 30% или 70% покрытие — от непонимания. Надо стремиться к 100% (хоть это и недостижимый предел).

Всем известно, что 100% покрытие невозможно, но не все правильно понимают, почему. Вовсе не потому, что «надо тестировать только критичное», «незачем тестировать геттеры» и «у нас есть дела поважнее».

Полное покрытие невозможно потому, что в любой системе код общается с внешним миром. Отрисовка пикселей на экране, управление устройствами и так далее — для этого кода юнит-тесты невозможны, это нужно проверять своими глазами. Наш профессионализм состоит в том, чтобы этот слой «общения с внешним миром» держать максимально тонким, и всю логику выносить из него. И вот её-то нужно тестировать.

Грубо говоря, если просроченные кредиты нужно подсвечивать красным, то логику «какие просроченные» нужно выносить в отдельный метод с юнит-тестами, а «подсвечивание» — отдельно. Ну там, css или как там ещё. И TDD стимулирует разделять эти слои, вот что замечательно. В этом его сила! А тесты — это лишь побочный эффект.

— Сколько времени нужно уделить созданию юнит-тестов, и на каком этапе проекта они играют ключевую роль?

Юнит-тесты играют ключевую роль всегда, на всех этапах. Нет такого этапа, где «пора начать писать юнит-тесты» или «можно больше не писать юнит-тесты».

Если делать всё правильно, вы никогда не сможете ответить на вопрос, сколько времени занимает написание юнит-тестов. Это часть разработки. Никто же не спрашивает, сколько времени занимает намазывание кирпича раствором, а сколько — укладка. Это всё необходимые шаги, и всё тут. Нельзя выкинуть один из них, иначе твоя стена развалиться на второй день, и ты получишь кучу legacy-кирпичей.

— Как у вас выглядит процесс написания тестов?

Ты пишешь красный тест 10 минут, делаешь его зелёным 10 минут, рефакторишь 10 минут. Никто, конечно, не замеряет это время точно — иногда это 5:30:0, а иногда 180:5:60. Неважно. Важно, что ты с таким же темпом, с предсказуемой скоростью сможешь менять код и через месяц, и через год. Постоянная скорость в долгосрочной перспективе гораздо важнее высокой мгновенной скорости на старте.

Советую вот это видео, где понятно и весело показано TDD: «Пацан накодил — пацан протестил!» Не пугайтесь длины, там про юнит-тесты только первые 30 минут.

242815f1e4b468a386c52cc881cb57c4.png

— Андрей, если TDD так полезен, почему его так мало используют?

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

Забавная разница в том, что люди, лежащие на диванах, не называют себя спортсменами. Тут всё честно. А кодеры не пишут тестов и при этом называют себя разработчиками. Невероятно, правда? Но заканчивается это всегда одинаково. Спортсмен, пропускающий тренировки, быстро проигрывает соревнования. Так же и код, написанный не через тесты, моментально становится legacy. Тут всё честно.

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

TDD — это теорема, которую надо доказывать каждый день.

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

Я вам расскажу одну байку. Мой брат работал в химической лаборатории тартуского университета. К ним приехала делегация откуда-то из Европы. Им всё показали: новые помещения, новое оборудование, все дела. И тут они обратили внимание на старый советский агрегат где-то в углу. Делегаты сделали круглые глаза и спросили, мол, а что ж вы его не выкинете? На что последовал ответ: «Не поверите! Потому, что он… РАБОТАЕТ!»

Тут нет единого ответа. Можно ежедневно обновлять все зависимости и относиться к этому как к необходимой гигиене. Можно даже попросить Maven или Gradle делать это автоматически. А можно сделать fork оригинальной библиотеки и сидеть двадцать лет на старой версии. Пожалуй, это зависит от зависимостей. Если я сильно завишу от Selenium, использую новые фичи, то я его часто обновляю. Если я никак не завишу от новых фич log4j, я сижу на древней версии. Всё ж работает.

Чем старше я становлюсь, тем больше склоняюсь к тому, что не стоит гнаться за обновлениями. Новый Play framework оказался не лучше, чем Struts2. Maven не лучше (и даже хуже), чем Ant. TestNG однозначно хуже JUnit (а ведь «new generation» в названии!). Hibernate3 был сложнее, чем Hibernate 2. Spring boot сложнее, чем Spring MVC. А ведь лет 10 прошло! Про новые JavaScript библиотеки я уж молчу.

Так что не всё то новьё, что обмазано солидолом.

— В вашей практике встречались подобные ситуации (дополнительно к предыдущему вопросу)? Как вы выходили из подобных ситуаций? Какой стратегии вы придерживаетесь, переписать всё (часто используется термин — greenfield) или сделать рефакторинг?

Конечно, встречались.
Да это просто вопрос организации. Если приспичило обновить какую-то зависимость, которая обратно несовместима, надо просто выделить для этого время, договориться с командой, чтобы ни у кого ничего срочного в этот момент не было, да и отрефакторить. Многие начнут плакаться: «Нам не дают на это времени». Ну не знаю, может, не так уж нужно обновляться? Было бы критично — дали бы время.

Насчёт greenfield я довольно скептично настроен.
Конечно, плох тот солдат, который не мечтает всё переписать с нуля.
Но я за всю свою жизнь ни разу не видел, чтобы систему переписали с нуля и стало лучше. Обычно получается так же или хуже. Переписывальщик начинает с большим азартом, потом сталкивается с одной проблемой, с другой; понимает, что уже не успевает… Оставляет технический долг в одном месте, в другом — и вот у вас уже legacy-код в новом проекте, хотя ещё и половины-то не написано.

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

— Может быть, существует проверенный «гибридный» вариант?

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

— Как вы отличаете плохой, попадающий под определение legacy-код, от максимально «разогнанного»?  Стоит ли гнаться за производительностью в ущерб будущей поддержке?

Разогнанный код — это типа десяток строк заумных операций с байтиками, чтобы работало быстрее? Я думаю, что такой код в реальной жизни из разряда баек. Все думают, что только крутые перцы так умеют, и тоже хотят научиться. В прикладном программировании это почти никогда не нужно.
А если всё-таки нужно, то никто не мешает сделать этот код и «разогнанным», и хорошо читаемым, и протестированным. Давай, ты же профессионал. Сначала напиши юнит-тесты, а потом разгоняй. Не осилил юнит-тесты? Нечего и разгонять — нос не дорос.

f12e82058f9b6b5bb12c7608cf130a28.png

— Как вы думаете, legacy-код может тормозить развитие всей системы?

Конечно, может! И тормозит!
Собственно, это самое главное, что обычно тормозит. Каждое следующее изменение вносить всё сложнее, риск что-то сломать всё больше.

 — Вы можете дать совет о необходимости избавления от ужасного и медленного «монстра» в проекте, если его увидите? Как на это реагировали (если такие ситуации происходили)? Изменения провели к положительно эффекту (если такие ситуации происходили)?

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

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

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

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

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

— Вы придерживаетесь мнения, что красота кода — это ещё не показатель legacy-статуса?

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

— Можно ли утверждать, что нарушение стандартного соглашения по оформлению кода — это legacy?

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

И не стоит ради них тратить свои силы. Настройте один раз IDE всех участников проекта, чтобы она сама форматировала код как надо, и больше не думайте об этом.

— В рамках legacy мы обсудили вопросы тестирования, производительности, зависимостей в проектах, красоты кода и затронули тему развития. Ваше мнение о legacy-коде и причинах его возникновения не менялось с течением времени?

Конечно, менялось. Вначале я, как и все, думал, что плохой код пишут индусы и тупые коллеги. Или «джуниоры». Но всё не так просто.

Я не люблю слишком простые объяснения. Слишком легко свалить всё на «тех придурков». А legacy-код возникает и у самых умных людей, и в самых лучших проектах.

— И у тебя?

О да. О да!
Вот я тут вещаю, такой умный, про legacy-код, а у меня самого есть open-source проект Selenide, который legacy-кодом зарос по самое не моргну. Нет, он отлично работает, пользователи довольны, но мне его менять со временем всё сложнее. Есть тикеты, которые висят уже пару лет, потому что мне реально сложно их сделать. Поменяешь в одном месте — ломается в другом. Да, там есть интеграционные тесты, благодаря которым я не боюсь что-то сломать. Но от них нет помощи в том смысле, что менять-то код не получается.

Однажды я понял: ключевая проблема в том, что в этом проекте я изначально не писал юнит-тесты. Казалось, зачем тесты, если эта библиотека сама предназначены для написания тестов? Она и так будет протестирована вдоль и поперёк. А оказалось — вон оно что, Михалыч! Юнит-тесты нужны для избежания legacy-кода.

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

—Как вам кажется, в каких оттенках стоит воспринимать legacy-код? В каких случаях оправдано его наличие?

У меня есть простой рецепт. Отвлекитесь на секунду от лямбд и скобочек и представьте себя на месте клиента. Это здорово прочищает мозги.

Как клиент, привозите вы свою машину в автосервис, просите посмотреть, что там стучит.
А вам механик говорит: «Я поменял то, что стучало, но у вас ещё и топливный насос дырявый, — будем менять?» Это хороший механик. Плохой механик поменял бы насос сразу. А хороший механик сообщил о проблеме и расписал варианты: можно заменить сейчас — будет стоить столько-то. Можно не менять, тогда зиму ещё доездите, но потом и все шланги полетят — выйдет втрое дороже. И вы сами как клиент решаете. Один скажет: чего два раза ходить, меняй всё! Другой скажет: «Сейчас денег нет, а мне ещё зимнюю резину нужно. Давай до весны подождём».

А знаете, что сделает самый худший механик? Промолчит. Сделает только то, что просили.

А как ведём себя мы все с вами, дорогие программисты?
Те самые горячие головы, что норовят тут же всё порефакторить — они как кто? Правильно, как плохой механик. А те «повзрослевшие», что решают оставить всё как есть? Увы, как самый худший. А что должен сделать с legacy-кодом хороший механик? А как часто мы так делаем? Вот и думайте.

— Андрей, TDD, юнит-тесты — всё это звучит хорошо, но вы же понимаете, что для 99% наших читателей это неподъёмно в силу самых разных причин, часто от них не зависящих. А можете напоследок дать простой рецепт, как бороться с legacy-кодом?

Вы хотите рецепт, но когда я даю рецепт — проверенный, надёжный, вы говорите, что у вас нет на это времени. И продолжаете жаловаться на legacy-код. Вы что же от меня ждёте, красной пилюли — выпил, и ты уже в матрице? Не будет пилюли.

Люди как люди. Любят торопиться, но ведь это всегда было… Человечество любит простые рецепты… В общем, напоминают прежних… микросервисный вопрос только испортил их…

В общем, волшебного избавления от legacy-кода не будет. Но вы держитесь там, удачи, хорошего настроения!

— Огромное спасибо за откровенную и позитивную беседу. Будем надеяться, что читатели откроют для себя, пусть и не простые, но все-таки решения в битве с legacy. Одна из недавних статей  — прямое подтверждение важности тестирования. Лично меня вы убедили.

Спасибо и вам за приглашение.
А я воспользуюсь положением и приглашу всех читателей в гости к нам в Таллинн. У нас красивый средневековый город, хорошее крафтовое пиво и отличные мероприятия для айтишников, куда любой желающий может прийти бесплатно: devclub.eu

Спасибо за внимание и за терпение!


Больше интересных докладов, технических, хардкорных, вы найдете в программе Joker 2016. Если вы работаете с легаси, вам стоит обратить внимание на следующие доклады:
  • Andres Almiray, Griffon: What’s new and what’s coming
  • Барух Садогурский: Мавен против Грейдла — на заре автоматизации
  • Дмитрий Александров: JBatch, или далеко не самые большие данные

А также еще неопубликованный доклад Вячеслава Лапина «Как я переводил большой legacy enterprise проект на Java 8 — приёмы, трюки и «подводные ками».

Комментарии (0)

© Habrahabr.ru