Чому рефакторинг — це постійний процес
Мені здається, що про рефакторинг ми чуємо дуже часто, але кількість питань з цієї теми не зменшується, а навіть збільшується. Тому про нього й розповім на основі власного досвіду.
В IT я років 15 і за цей час бачив багато різних проєктів. Та нещодавно я стикнувся з яскравим прикладом системи, яка має великий технічний борг і повний набір помилок, яких тільки можна було припуститися, працюючи над застосунком.
Майже рік тому мене запросили на проєкт тімлідом — зібрати команду і очолити фронт-розробку (оскільки останнім часом я спеціалізуюся на React-застосунках).
Проєкт розробляли майже 10 років. Коли ми сформували команду і почали працювати над ним, то постали перед вибором: або переписувати все з нуля (те, що часто люблять програмісти, але те, що складно зробити у великому застосунку, який уже працює), або робити глибокий рефакторинг.
Але про все за порядком. Спочатку трошки поговоримо про те, що ж таке рефакторинг, коли він і навіщо потрібен і чи завжди його доцільно проводити.
Поганий код вбиває. Хто відповідальний за код і його якість
Важливість якісного коду (який ще називають чистим кодом, clean code за Робертом Мартіном, Uncle Bob) часто пояснюють на прикладі жарту про Джона — «серійного програміста». Суть жарту в тому, що коли програміст робить помилку в програмному забезпеченні, то користувачі витрачають час на те, щоб обійти її. І якщо користувачів дуже багато, то навіть кілька зайвих витрачених хвилин у кожного з них у сумі це сотні років. Також поганий код краде час в інших програмістів.
Але хто ж відповідає за якість коду? Роберт Мартін вважає, що лише програмісти. Одним з прикладів відповідальності він називає доволі свіжу історію з компанією Volkswagen. Великий скандал, який стався через маніпуляції з результатами показів викидів вихлопних газів автомобілів, щоб обдурити тестувальні апарати. На суді СТО компанії, коли його запитали, як це могло статися, відповів, що це зробили кілька розробників з якоїсь причини. Звичайно, ця причина нам зрозуміла — менеджмент попросив про це програмістів. Але вони могли відмовитися виконувати таке завдання. Нині ці спеціалісти в тюрмі (як зазначив Роберт Мартін у своєму виступі), а компанія втратила десятки мільярдів євро.
Що таке рефакторинг і навіщо його виконувати
Рефакторинг (англ. refactoring) — перетворення програмного коду, зміна внутрішньої структури програмного забезпечення для полегшення розуміння коду і внесення подальших змін без зміни зовнішньої поведінки самої системи.
Навіщо той код переписувати? «Щоб робити його кращим» — скаже будь-який програміст. Але кращим для кого? Відповідь проста, але часто не зовсім очевидна — для нас, програмістів. Відомий факт, що код набагато частіше читають, аніж пишуть. Читають код зазвичай програмісти.
Якби розуміння коду спеціалістами не було таким важливим, то не було б потреби в існуванні великої кількості мов програмування. Адже всі мови програмування, що були розроблені і розробляються сьогодні, потрібні саме програмістам, а не комп’ютерам (або процесорам). Бо процесор розуміє лише двійковий код, а спеціалістам для ефективної роботи потрібні більш високорівневі інструменти. І одним з найголовніших аспектів мови програмування є саме її читабельність (наприклад, вважають, що Python завоював свою популярність завдяки тому, що легко читається, тому ця мова підходить новачкам).
Також важливо не забувати, що код, який ми написали, ми самі ж і читаємо. Мій син полюбляє жарти про програмістів. Один з них звучить так: «Хто це такий код написав? О, так це був я...» :) Це до того, що з часом ми забуваємо, що писали й навіщо. І якщо код був не ідеальним, то потім виникають питання до нього.
Технічні аспекти та принципи оцінювання якості коду
Тож якщо якісний код — це код, який легко читати, ми можемо описати деякі характеристики коду, які впливають на його читабельність: іменування змінних, функцій та класів; розмір коду (довжина рядка, розмір класу чи методу); форматування.
Іншою важливою особливістю коду є можливість його розширювати та масштабувати. Наприклад, відомою є проблема, коли клас має багато обов’язків, слабко пов’язаних між собою, що порушує принцип єдиного обов’язку (single responsibility principle). У такому разі краще розділити клас на кілька атомарних елементів.
Деякі аспекти оцінювання якості коду є суперечливими. Тож команда має просто обрати набір стилів та найкращих практик, яких планує дотримуватися. Більшість проблем із якістю ми можемо знайти в коді за допомогою програмного забезпечення, що вже давно існує. Його можна внести в пайплайн CI/CD.
Що таке технічний борг і що з ним робити
Вперше метафору «технічний борг» щодо «брудного» коду запропонував програміст Ворд Каннінг.
Наприклад, якщо ви візьмете кредит у банку, то можете пришвидшити купівлю чогось. Однак повернути вам потрібно буде не тільки основну суму кредиту, а й відсотки, які будуть нараховуватися доти, доки ви повністю не розрахуєтеся з банком.
Також ви можете взяти декілька кредитів одночасно. Ба більше — набрати їх стільки, що сума відсотків переважить ваш сукупний дохід і зробить повне погашення неможливим.
Те саме з кодом. Простий приклад: сьогодні ви тимчасово прискорюєтеся, не написавши тести для нового функціоналу, але тепер це буде потроху сповільнювати прогрес у майбутньому. Доти, доки ви не погасите борг через написання цих тестів.
Піком такої ситуації стає момент, коли ви не можете змінити щось у коді, бо не знаєте, чи щось зламається. З цим ми зіткнулися на проєкті, після чого нам потрібно було спланувати «сплату боргу», який назбирало ціле покоління розробників до нас.
Поширені причини появи технічного боргу (всі вони яскраво проявилися на нашому проєкті, за кожним пунктом можна написати окремий розділ або навіть статтю):
- Тиск з боку бізнесу.
- Відсутність розуміння наслідків технічного боргу.
- Відсутність боротьби з жорсткою обмеженістю компонентів.
- Відсутність автотестів.
- Відсутність документації.
- Відсутність взаємодії між членами команди.
- Довготривала одночасна розробка в кількох гілках.
- Відкладений рефакторинг.
- Відсутність контролю за дотриманням стандартів.
- Відсутність компетенції.
Головне, що нам потрібно розуміти, що технічний борг потребує рефакторингу, водночас відкладений рефакторинг збільшує технічний борг. Тому це взаємопов’язані речі.
Чи є причини не робити рефакторинг
Ми вже трохи розібралися, що таке рефакторинг і навіщо він потрібен. Але чи завжди доцільно його робити? Розгляньмо ситуації, коли рефакторинг може нашкодити.
Перечитуючи статтю на «Хабрі» «Цена рефакторинга», мені пригадалася одна ситуація. Коли до нашої команди приєднався Senior-розробник, у пул-реквестах з’явилося багато змін, не пов’язаних з тасками. Понад те, коли він робив код-рев’ю іншим розробникам, то додавав усе більше й більше коментарів на зміну коду (рефакторинг). І начебто це правильно, відповідно до підходу, який описав Роберт Мартін («ми повинні залишати код кращим за той, який отримали»), але щось було не так, щось, що затримувало merge request на дні, а іноді й тижні. І зміни часто починали виходити за межі методів чи класів, які зачіпалися початково. Менеджерам довелося втручатися і пропонувати створювати рефакторинг-таски в беклозі з технічним боргом.
Чому рефакторинг, який має приносити користь, приносив проблеми? Однією з причин була відсутність юніт-тестів, тому рефакторинг часто спричиняв появу нових помилок, а розробники боялися щось змінювати. Тобто юніт-тести — це те, що треба робити передусім. Цим ми і зайнялися, але натрапили на супротив системи з великим технічним боргом — більшу частину коду практично не можна було протестувати (що і було однією з причин, чому розробники, що вже працювали з продуктом, не змогли додати юніт-тести. Як ми виявили пізніше, вони кілька разів намагалися почати писати юніт-тести).
Друга причина полягала в тому, що не було загального розуміння стилів коду та підходів у розробці. Частина команди працювала з кодом багато років, і ці фахівці просто звикли так писати. Перед тим як почати вимагати від людей нової якості коду, потрібно пояснити, які проблеми з цим уже є, як їх краще виправляти. Також запропонували обговорити і затвердити більш докладний DoD (Definition of Done), за яким можна було аргументувати, що не так у коді.
Ну й проблеми в архітектурі. Це дуже важливий момент: розрізняти рефакторинг коду та виправлення (рефакторинг?) архітектури застосунку. Іноді це непросто, тому що вони часто перетинаються, але зазвичай рефакторинг коду не виходить за межі одного модуля і коли ми його проводимо, то робимо код простішим для розуміння. А рефакторимо архітектуру ми здебільшого для полегшення зміни коду в майбутньому.
Чому б не писати код, який не потрібно рефакторити
Якщо ми знаємо правила написання чистого коду та чистої архітектури, то чому не писати чистий і правильний код одразу, а чекати моменту, коли його треба переписувати? Чи могла команда на нашому проєкті написати правильний код відразу?
Більшість людей вважає, що це неможливо. І я з цим погоджуюся, тому що є багато факторів, які впливають на появу технічного боргу та брудного коду. Частину з них ми розглянули раніше. Але що точно можна зробити, так це підтримувати архітектуру застосунку, що дає змогу вносити швидкі та безболісні зміни. Тоді й жоден рефакторинг нам не страшний. А сам процес стає невіддільною частиною роботи з кодом.
Чи справді якісний код потрібен лише програмістам
Поширена думка, що якісний код потрібен лише програмістам (є навіть окремий термін DX — developer experience, або досвід розробника). Але роботи Роберта Мартіна та мій досвід з легасі-системами показують, що якісний код і чиста архітектура (clean architecture) потрібні насамперед самому бізнесу. В книжці Clean Architecture автор описує проєкт зі свого досвіду, в якому збільшення кількості програмістів практично не підвищило продуктивність роботи відділу, а видатки на розробку значно зросли.
Те саме я помітив на одному зі своїх проєктів. Нам довелося допрацьовувати фічу, яку інша команда готувала понад рік і не змогла зарелізити. Помилки під час роботи з тією фічею, а також проблеми з кодом та архітектурою в застосунку, призвели до того, що реліз функціоналу затримався на півтора-два роки.
Тому я зовсім не погоджуюся з тезою, що чистий код та чиста архітектура потрібні лише розробникам. Бізнес у цьому першочергово зацікавлений, просто іноді він цього не розуміє.
Знаю, що багато хто не погодиться зі схожою думкою, бо бувають ситуації, коли продукт треба зарелізити якнайшвидше, щоб протестувати бізнес-модель, а потім вже братися за покращення коду. Але для того, щоб перевірити бізнес-модель, застосунок не потрібен, можна написати PoC (Proof of Concept), який потім викинути. І це доволі поширена проблема, коли пишуть PoC і після релізу беруть його за основу продукту (адже воно ж працює і вже є багато коду), але то вже інша історія.
Передумови для успішного рефакторингу
Що потрібно мати або треба зробити до початку рефакторингу, щоб зменшити ризики порушення логіки коду? Важливо розуміти, який рівень рефакторингу потрібен — коду чи архітектури. І взагалі, мабуть, спершу варто пересвідчитись у потребі підтримувати код та архітектуру чистими.
Потім треба провести аудит коду та архітектури. Він допомагає виявити поширені проблеми і скласти план робіт. Також можна описати їх і зазначити найкращі практики для уникнення схожих труднощів у майбутньому (навчити команду).
Як я вже писав, важливо мати юніт-тести. Завдяки написанню юніт-тестів можна рефакторити модулі, не хвилюючись про те, що код буде зламано. Під час підготування тесту програміст краще розуміє функціонал, який потрібно протестувати. А ще так можна виявити код, який складно покрити тестами. Це зазвичай є ознакою поганого коду, який потрібно рефакторити. Але тут дилема: ми не можемо рефакторити, поки немає юніт-тестів, але буває, що ми не можемо написати юніт-тести без рефакторингу. Переважно це проблема вищого рівня — в архітектурі. Схожі питання мають вирішувати досвідчені інженери і не в межах «фіксу багу».
Крім того, корисними можуть бути інтеграційні тести. Коли ми зрозуміли, що потрібно рефакторити REST API, у якого не було юніт-тестів, то запропонували спочатку закінчити роботу над інтеграційними тестами, щоб упевнитися, що API працює без проблем після змін архітектури.
Ну і, звичайно, потрібна команда, яка зможе це реалізувати. І що дуже важливо — команда має не лише втілити рефакторинг, а й зробити його постійним процесом.
Чи обов’язково задачі на рефакторинг мають бути в беклозі
Чи потрібно додавати задачі на рефакторинг в трекінг-систему, чи його можна проводити в межах наявних задач? Це питання важливе і з погляду того, що часто ми чуємо: «Менеджмент не любить задачі на рефакторинг».
Як на мене, то і так і ні. Є рефакторинг, що має відбуватися завжди, за згаданим принципом американських бойскаутів: «Бачимо сміття — прибираємо». Але якщо виконується маленький баг-фікс і при цьому переписано купа методів та класів, то це ознака, що щось не так з архітектурою. І, найімовірніше, потрібно створити задачу в беклозі на виправлення проблеми.
Існування окремої задачі дає змогу попрацювати над нею більш сконцентровано, а також оцінити вплив змін на роботу застосунку. Уявіть собі здивування тестувальника, який, перевіряючи задачу на зміну тексту на кнопці, помітить, що кнопка тепер має червоний колір, а не зелений (прибрали зайвий клас), або падіння E2E-тестів (видалили зайвий div, за яким орієнтувався тест).
Також можна краще оцінити розмір задачі. Нерідко додаткові зміни під час виправлення помилок затримують її виконання, адже збільшується час не лише на сам фікс та рефакторинг, а й на код-рев’ю, який може відбуватися в кілька циклів.
Тож рефакторинг — це постійний процес, який має бути невіддільною частиною щоденної роботи програміста. Щоб покращувати код не потрібні окремі тікети, не треба чекати дозволу на це — покращувати код можна (і потрібно) завжди. Але значні зміни (і особливо зміни архітектури системи) краще робити прозоро, тобто в окремих задачах — відповідно до процесу розробки, якого дотримуються на проєкті. Не забуваймо: ми відповідаємо за код, який написали.