Як модернізувати легасі-код. Технічні аспекти рефакторингу
Привіт! Мене звати Дмитро Ханджанов, я фронтенд-розробник із майже
- Health-care проєкт, де на перше місце ставилася автономність. Ми мали працювати офлайн і передавати дані на сервер у короткі проміжки часу, коли з’являвся зв’язок.
- Трекінгові системи, які відстежували переміщення користувача всередині приміщень і організовували дані для аналітики. Не варто й казати, з якими обсягами інформації доводилося працювати :)
- Платформа для кіберспортсменів і гравців у CS2, яка налічує понад 4 мільйони користувачів зі 168 країн світу, де на перше місце ставиться інтерактивність, реактивність і відмовостійкість системи. Мій поточний проєкт.
Тепер, коли я вже представився і з підозрілої особистості став трохи знайомою, гайда без зайвих слів перейдемо до теми нашої розмови, а саме до рефакторингу проєктів.
Як сприймати статтю
Часто при прийнятті рішень складно знайти теоретичне підґрунтя, на яке можна спиратися, бо питання, що виникають перед розробниками під час рефакторингу проєкту, є занадто абстрактними. Відповіді на них не завжди можна нагуглити або знайти на Stack Overflow. У цій статті я зібрав і коротко надав список матеріалів, які особисто мені не раз ставали у пригоді й про які я б волів знати набагато раніше.
Стаття буде розділена на дві частини: технічну та організаційну. У першій, технічній частині, ми розглянемо питання прийняття рішень і основні підходи, які можуть цьому сприяти. А також торкнемося питання складання технічної документації. В другій, організаційній частині, обговоримо варіанти представлення ідеї менеджменту, а також окреслимо супутній обсяг робіт.
Що таке легасі-код. Як утворюється, яким буває
Усі програмісти хоч раз та стикалися з поняттям легасі-коду, а багато хто навіть мав справу з ним особисто. У абсолютній більшості випадків він призводить до болю, перепрацювань і тотальної фрустрації розробників від того, що відбувається.
Однак перш ніж ми перейдемо до обговорення рефакторингу, я б хотів дати визначення легасі-коду. З назви можна зробити висновок, що це код, який був розроблений давно і вже морально застарів. Але це лише частина правди, чи не так?
Для мене легасі-код поділяється на два види:
- Добре написаний код на застарілих версіях мови/фреймворку. Такий код потрібно не стільки «рефакторити» у звичному розумінні цього слова, скільки переписати під нові стандарти, залишивши концепцію незмінною.
- Розрослий MVP. Напевно, саме цей або подібний варіант першим спадає на думку при згадці легасі-коду. Тобто проєкт, який розростався з часом, із застосуванням компромісних рішень і приділенням недостатньої кількості часу на рефакторинг.
Саме такий кейс з другого пункту я і пропоную розібрати. Ми будемо не просто розбирати «погані запахи» коду, підбираючи під них рішення, а подивимося на картину ширше. Оцінимо стан проєкту загалом і розв’яжемо його проблеми на фундаментальному рівні.
Абстрактний проєкт для прикладу
Уявімо найбільш поширений з мого досвіду сценарій. Перед командою поставили завдання написати відносно невеликий додаток (MVP) для перевірки ідеї. Було прийнято рішення використовувати один із фреймворків (нехай це буде React), додати туди Redux для зручності та слідувати канонам фреймворку в розробці застосунку.
Минув час, MVP вистрілив, однак переписувати його під нові фічі замовник відмовився. Вирішили розвивати те, що вже працює. Так потроху з’явився якийсь кастомний middleware-прошарок між Redux і React-компонентами, куди перейшла частина логіки.
Ще частина загубилася десь у нескінченних helper’ах, utils’ах, handler’ах тощо.
У цей час змінювалися люди, хтось йшов, хтось приходив йому на заміну, і ось через декілька років команда утвердилася в думці, що немає нікого, хто б всеосяжно розумів, як проєкт функціонує. Функції з helper’ів і utils’ів у хаотичному порядку переплітаються з Redux’ом і middleware, і ніхто не хоче в них лізти, щоб нічого не зламати. А новий функціонал теж намагаються писати так, «аби нічого не зламати».
Тут знаходимось ми й починаємо його рефакторити.
Рефакторинг проєкту такого масштабу кидає виклики як з технічного боку, так і з погляду менеджменту. У цьому розділі озберемо формулювання ідеї, перевірку ідей, створення технічного дизайну, створення пісочниці та складання документації.
Розуміння проблем або розробка вимог
З чого варто почати? Зрозуміти, чи заважає легасі-код продукту розвиватися. І якщо заважає, то як саме, і що тоді ми хочемо отримати після рефакторингу.
Існує 12 атрибутів якості архітектури проєкту. Оцінюючи їх ми можемо сказати, чи є проблеми, які наслідки з них випливають, і чи є це чимось критичним.
У нашому проєкті ми не маємо закладеної архітектури, тому оцінювати її не зовсім коректно. Однак це не заважає нам використовувати ці атрибути, щоб оцінити якість проєкту загалом, і виділити області, які ми хочемо покращити.
Для нашого абстрактного прикладу будуть актуальними проблеми з багатьма, якщо не з усіма, якостями архітектури. Можемо виділити наступні проблеми:
- Testability (тестованість). Погано спроєктована система неминуче призводить до дублювання коду та нехтування чистими функціями, що робить тестування якщо не неможливим, то вкрай складним завданням.
- Maintainability (підтримуваність). Через високу зв’язаність коду стають дуже ймовірними помилки на кшталт «тут полагодив, а там зламав», на виявлення та виправлення яких витрачається багато часу.
- Flexibility (гнучкість). Точки розширення функціональності проєкту важко виділити, через що для впровадження нової функціональності доводиться частково переписувати стару. Що в умовах відсутності тестів призводить до великої кількості багів.
Серед інших проблем, які також варто зауважити — відсутність структури проєкту, документації, knowledge holder’а. Це впливає на якість прийнятих рішень і залучення в проєкт нових розробників.
А також зростаюча з часом складність. На ілюстрації показано, як буде зростати складність проєкту залежно від обраного архітектурного патерна. Але можна помітити, що вона зростатиме в будь-якому випадку. Рано чи пізно впровадження нової функції або виправлення бага стане настільки непередбачуваним і дорогим, що проєкт перестануть підтримувати зовсім.
Розробка рішень
Існує безліч патернів і підходів до організації коду та побудови архітектури. Однак, як і у виборі фреймворка, не завжди вдається обрати ідеальне рішення, ґрунтуючись виключно на потребах проєкту. Потрібно також враховувати орієнтовні терміни імплементації та компетенції команди.
Наприклад, ви ж не будете переписувати проєкт з React на Angular тільки тому, що він краще підходить проєкту? Це вимагатиме повної заміни команди, інакше процес переписування буде ідеальною ілюстрацією фрази «через терни до зірок».
У нашому абстрактному проєкті, припустимо, є люди, обізнані в ООП, і є достатній запас часу для реалізації рефакторингу будь-якої складності. Пропоную з цієї позиції й в контексті цього проєкту розглянути можливий варіант архітектурного рішення.
Роздiлення на шари
Навіщо розділяти проєкт на шари? Причин багато, але одна буде зрозуміла всім. Знаєте це відчуття, коли приходите на проєкт, де немає жодної архітектури, і виникає потреба створити функцію, яка відправляє кудись запит і обробляє відповідь?
Погодьтеся, в 99.9% випадків ця функція знайде своє місце в черговому хелпері, десь серед сотень інших. Бо просто немає місця, куди її варто покласти. В результаті розробник робить найдешевше рішення, породжуючи технічний борг.
Крім того, завжди легше тримати в голові окремий модуль або шар, розуміючи його призначення та відповідальність, ніж оперувати відразу всім проєктом. Іншими словами, розділення на шари зменшує зв’язаність проєкту, що позитивно позначиться на Testability та Flexibility, з якими у нас, як ми пам’ятаємо, є проблеми.
Звичайно ж, є ряд підходів, які описують можливі варіанти розшарування архітектури та взаємодії між шарами. Для нашого «абстрактного проєкту» ми зупинимося на популярному Clean Architecture, але можна розглянути всі варіанти.
Clean Architecture. Описує розділення на шари таким чином, коли залежності рухаються лише ззовні всередину. Тобто умовний вебінтерфейс може мати доступ тільки до наступного умовного шару «controllers» або «middleware», а ті, своєю чергою, не можуть напряму керувати вебінтерфейсом.
Vertical Slice Architecture. Наступний еволюційний крок Clean Architecture, суть якого в реорганізації коду проєкту таким чином, щоб «шари» описували структуру кожної окремої функції як модуля.
Onion Architecture. Дуже схожа за своєю концепцією з Clean Architecture, але більш гнучка. Дає змогу різним частинам шарів спілкуватися між собою за допомогою обробників команд, подій тощо.
Ймовірно, наведені вище патерни далеко не всі, що описують способи розшарування проєкту. Але основні, з яких я б рекомендував почати знайомство.
Звісно, не обов’язково розділяти проєкт на шари, використовуючи якусь методологію, особливо якщо у вас немає великої кількості коду, який можна туди винести. Однак хоча б подумати над зонами відповідальності та відокремити їх одна від одної — завжди хороша практика.
Патерни та підходи, на які варто звернути увагу
Світ розробки можна описати, перефразовуючи фразу з відомого фільму — все вже придумали до нас. Є безліч патернів і шаблонів, які прийняті спільнотою, протестовані, і дозволяють уникнути помилок, що були зроблені їх авторами до того, як вдалось прийти до цих патернів і шаблонів.
Архітектурні патерни
Якщо шари абстрактно описують зони відповідальності та способи взаємодії між ними, то архітектурні патерни описують компоненти систем і їхню взаємодію. Вони є готовим рішенням для побудови застосунку, що відповідав би описаним вище принципам.
Звісно, докладно розглядати всі патерни ми не будемо, але навести перелік таких шаблонних рішень, безумовно, необхідно:
MVC (Model-View-Controller). Мабуть, один з найбільш поширених патернів. Його суть полягає у створенні контролерів, які приймають команди від клієнта, створюють моделі. Які, своєю чергою, оновлюють уявлення (view).
MVP (Model-View-Provider). Дуже схожий з MVC патерн, відмінність якого полягає в тому, що модель не оновлює уявлення, а провайдер сам організовує дані та оновлює модель.
BLoC (Business Logic Component). Архітектурний патерн, що використовується для управління станом в застосунках, особливо популярних у Flutter. Він відокремлює бізнес-логіку від уявлення, організовуючи дані та події. Події передаються в блок, який обробляє їх і оновлює стан, передаючи його назад до уявлення.
VIPER. Архітектура, що має популярність у Swift. Містить перелік елементів з чітко розділеними зонами відповідальності та способом взаємодії між ними.
Список патернів, звичайно, не обмежується цими прикладами, проте я навів основні, найбільш популярні, щоб було від чого відштовхнутися.
Окремо зауважу, що для фронтенд-розробників буде корисно звернути увагу на рішення для мобільних застосунків. По суті, наші проєкти мають багато спільного, проте в питаннях побудови архітектури для асинхронного, інтерактивного проєкту в них більше рішень.
Модульність
Імплементуючи модульний моноліт, я не міг не помітити, що він на відмінно справляється з типовими проблемами фронтенд-розробки. Виділення модуля як окремого набору керуючих елементів у рамках домену дозволить:
1. Розділити розробників на команди.
2. Знизити зв’язаність проєкту (якщо забезпечити невзаємодію модулів).
3. Просто роздробити проєкт для кращого його розуміння.
На мою думку, єдиною причиною не виділяти модулі може бути відсутність бізнес-логіки як такої. В іншому випадку ми отримуємо безліч плюсів за низької вартості. Наполегливо рекомендую розглянути цю ідею до імплементації.
Також важливо забезпечити правильну інтеграцію модулів у додаток. Адже якщо просто взяти та імпортувати функції напряму, ми отримаємо ті ж утиліти, тільки красиво складені по різних папках. Щоб якісно ізолювати код, можна вдатися до dependency injection. Вона якісно реалізована в Angular, хоча є багато фреймворк-агностик бібліотек, які можна використовувати.
Enterprise-патерни
Окремого згадування заслуговує ця категорія патернів. Частково вони вже були згадані вище, проте все-таки дуже багато залишилося за рамками цієї статті. Дані патерни описують рішення для типових проблем при побудові архітектури, організації потоків даних тощо.
Вони можуть описувати як структуру всього застосунку (MVC, MVVM тощо), так і окремих шарів (Data Mapper, Repository) і навіть окремих класів, призначених для спілкування між цими шарами (DTO, Query Object). Тому якщо виникають сумніви щодо якогось рішення в архітектурі — його, ймовірно, можна знайти в одному з цих патернів.
На відміну від GoF або GRASP, немає визначеного списку enterprise-патернів. Однак є безліч джерел, які надають такі списки в рамках певного контексту. Скажімо, патерни організації бізнес-логіки або зберігання даних. Просто обирайте свою область і майже напевно знайдете рішення своєї, типової проблеми.
Створення технічного дизайну
Як буде сказано далі, технічний дизайн — це результат першого етапу рефакторингу. Він необхідний, щоб на початку дати всім зрозуміти, що ми будемо робити, і колективно оцінити рішення, пропрацювати його слабкі місця.
Ознаками хорошого технічного дизайну будуть логічність, структурованість і вичерпність. Технічний дизайн повинен пояснювати, як і чому ми будемо реструктурувати проєкт. Кожен розробник, прочитавши його, повинен приблизно уявляти, що потрібно зробити.
Є ряд форматів і стандартів, яких дотримуються архітектори для опису своїх рішень. Хочу зупинитись на двох з них:
C4. Формат, який дозволяє описати архітектуру проєкту для різних рівнів і завдань. Структура документа є тривимірною і розділяється від більш абстрактних ідей і тем до деталей окремих класів і їхніх зв’язків, що дає змогу «провалюватися» в деталі за потреби. Наприклад, архітектору буде цікавіше дивитися на архітектурне рішення загалом, тоді як виконавцю — подивитися на передбачувану реалізацію конкретного модуля.
ARC42. Це шаблон, який дозволяє створити зручний для читання технічний документ з мінімумом води. Кожен пункт плану в цьому шаблоні строго регламентований і описаний. Це дає розробнику певний чек-лист, щоб нічого не упустити, і водночас ставить рамки, щоб той не «лив воду», вправляючись у красномовності.
Складений технічний дизайн за описаними вище шаблонами легко може бути використаний як документація по проєкту. Як мінімум частково.
Якщо ж представлені готові шаблони не задовольняють ваші запити, або потрібно щось простіше, і ви хочете описати дизайн у власному, вільному стилі, я все одно рекомендую пройтися декількома важливими пунктами в документації.
А саме:
Діаграма проєкту. Люди — візуали й краще сприймають інформацію в графічному стилі. Не супер детальна, але повна структура проєкту буде давати розуміння, як проєкт має бути побудований і як по ньому ходитимуть дані. Передача цього розуміння і є основною метою документації.
Опис шарів. Важливо описати кожен шар і яскраво виділити його зони відповідальності. Часто ці зони розмиті й потрібно всіх привести до єдиного розуміння, за що конкретно відповідає кожна з них.
Структура проєкту. Як файли проєкту будуть розділені по папках. Звучить, можливо, смішно, але розробники більшу частину часу працюють саме зі структурою проєкту, нишпорячи файлами. Якщо вона буде незручною — це впливатиме на продуктивність й кількість помилок, допущених при роботі з проєктом. Частково структура проєкту, звісно, буде продиктована фреймворком. Я ж кажу про структуру прошарку бізнес-логіки.
Переслідувані цілі та альтернативні рішення. Цей пункт буде корисний лише в тому випадку, якщо ми говоримо про технічний дизайн. Він проллє світло на причини прийнятих рішень, а також на рішення, що розглядалися, але були відкинуті. Це надзвичайно корисно для розуміння «чому», а не «для чого» і дасть можливість розробникам самостійно приймати правильніші тактичні рішення.
Технічне рішення для нашого проєкту
Як я казав раніше, багато в рішеннях залежить від команди. Однак для нашого прикладу ми цей момент упустимо, зосередивши увагу на тому, з чого почати побудову нашого рішення.
Отже, для початку у нас є заявлені проблеми з Maintainability і Flexibility проєкту. Це означає, що насамперед потрібно розв’язати проблему: де зберігати код і як організувати його взаємодію.
Для цього пропоную використовувати вже згадані вище модулі. У них можна буде поступово переносити функції та класи з хелперів і утиліт. Розділяти ці модулі слід на домени, які є в застосунку.
Оскільки це React-додаток, нам не обійтися без Redux. Це де-факто вже стандарт в індустрії, навколо якого умільці налагодили цілий зоопарк технологій, за що я їм особисто не скажу «красно дякую».
Залишилася лише одна нерозв’язана проблема з заявлених — Testability. Для того, щоб досягти гарної тестованості коду, ми повинні забезпечити можливість тестувати не окремі функції, а бізнес-правила. У нас вже є модулі, в які ми будемо виносити код, що обробляє дані у своєму домені, проте можемо для кожного з них зробити один або кілька Facade, за якими приховаємо складність модуля. Та опишемо бізнес-правила обробки даних, які чудово покриються тестами.
Використання ж цих модулів пропоную винести в middleware-прошарок на кшталт Saga або Redux-Effects, щоб повністю позбавити компоненти застосунку необхідності готувати параметри або займатися ще чимось, крім рендерингу елементів.
У результаті ми отримаємо приблизно таку схему поділу шарів і їхньої комунікації: компоненти мають доступ лише до middleware і підписуються на зміни стора, водночас лише middleware має право і можливість викликати модулі, що містять бізнес-логіку.
Звичайно, багато ще потрібно вирішити, але це вже основа, з якої можна починати. Далі потрібно розв’язувати проблеми побудови транспортного шару для API та ізолювання коду за допомогою інверсії залежностей. Але це вже незначні деталі для нашого прикладу.
Висновки
Висновок про складність побудови хорошої архітектури проєкту можна зробити хоча б з кількості букв, використаних для написання цієї статті. І це ми лише поверхнево пройшлися по основних моментах, які потребують глибшого вивчення, якщо ви хочете зайнятися цією справою серйозно.
Однак, як відомо, нічого цінного не дається задарма. Я маю скромну надію, що ця стаття надихне фронтенд-розробників відійти від практик, описаних у нашому «абстрактному проєкті» до чогось, що можна буде назвати своєрідним мистецтвом.
18 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів