Про Legacy-коді без максималізму: що робити



Уявіть, що вам дали завдання поправити частина коду. В голові виникне багато думок. Хто його написав? Коли? А може він — legacy? Де документація? Давайте спробуємо розібратися з «спадщиною» ґрунтовно і з усіх сторін. Допоможе нам в цьому питанні Андрій Солнцев @asolntsev http://asolntsev.github.io/), розробник з таллінській компанії Codeborne. Почнемо.

— Андрій, ви знайомі з працями 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 хвилин.



— Андрію, якщо 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-код, від максимально «розігнаного»? Варто гнатися за продуктивністю на шкоду майбутньої підтримки?

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



— Як ви думаєте, 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. Якщо ви працюєте з легасі, вам варто звернути увагу на наступні доповіді:

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

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.