PHP Code Avengers: Structuring, Refactoring, and Superhero Practices [ukr]

Talk presentation

In the world of PHP development, we often find ourselves facing the challenge of dealing with bloated, monolithic classes that hinder testability, extensibility, and maintainability. This talk aims to guide PHP developers through the process of overcoming the problem of unwieldy classes by employing effective structuring, refactoring techniques, and industry best practices.

Vladyslav Sikailo
run_as_root
  • Senior Backend Developer at run_as_root
  • Skier, cyber sportsman, car enthusiast, climber of the Carpathians
  • Certified Magento 2 developer, contributor, active community member
  • Software architecture and design are my passion
  • LinkedIn

Talk transcription

Привіт усім! Сьогодні ми поговоримо про код, а саме про той код, який існує насправді. Не в книжках, не в абстракціях, не на відео на YouTube, а саме той код, з яким ми зіткнемося. Ми розглянемо питання структурування, рефакторингу і якісних практик в цьому коді. Часто люди вважають, що писати код на PHP легко, або навіть простіше, ніж на інших мовах програмування. Така думка пов'язана з історією мови, яка колись починалася як інструмент для рендерінгу фраз на веб-сторінках. Зараз у нас етап еволюції, де обговорюється, коли Java "вмирає", а PHP залишається виживаючим. І часто код на PHP виглядає приблизно так.

Спробуйте згадати, хто з вас стикався з класами, що складаються майже з трьох тисяч рядків. Це не є винятком, це є стандартом. Але чому це трапляється? Це пов'язано з історією PHP та тим, що більшість веб-простору написано саме на цій мові. Ми маємо велику кількість "спадкового коду", з яким нам доводиться працювати щодня. Сьогодні ми розглянемо, як боротися з таким кодом, як його рефакторити та зробити більш якісним і структурованим. План наступний: усвідомити проблеми, зрозуміти, що їх дійсно існує і як їх вирішувати, а потім святкувати успіх.Розглянемо найтиповіші проблеми в PHP-коді. Перше — це "годові" класи, які виконують занадто багато функцій і зберігають занадто багато інформації. Такі класи важко читати, утримувати та розширювати. Наслідує довгий код, де функції роблять занадто багато різних речей, з великою кількістю умовних операторів, що робить код важким для розуміння. І ще одна проблема — дублікація коду, коли схожі чи ті самі шматки коду розкидані по різних частинах системи.

Стандартів для структури файлів не існує, що часто веде до хаотичної організації. Таким чином, дефініція структури файлів залишається на розсуд розробника. Усі ці проблеми не тільки ускладнюють роботу з кодом, але і призводять до появи помилок. Тому сьогодні ми розглянемо, як можна вирішити ці проблеми і зробити PHP-код більш організованим та якісним. Вона існує, вона існує залежно від фреймворків. Наприклад, в Laravel є свої практики, в Magento – свої, в Symfony – свої, навіть в WordPress теж є свої практики. Але фактично не існує стандарту, і це призводить до проблем з розумінням коду. Коли ми приходимо на новий проект, нам доводиться витрачати значний час і зусилля на розуміння коду та знаходження необхідної функціональності. Це ускладнює підтримку проекту.

Погана структура проекту робить невідомим місце, куди слід звертатися для змін чи виправлення помилок. Це особливо актуально, коли проект складається з декількох субпроєктів чи модулів. Документація також часто стає проблемою – або вона відсутня, застаріла, або невалідна. Розробники часто забувають чи уникають оновлення документації, що ускладнює розуміння системи. Недостатня модульність також є проблемою, особливо коли немає чіткої структури модулів. Це ускладнює розширення та підтримку системи, особливо великої.

Ці проблеми призводять до того, що розробники витрачають багато часу на "велосипедобудування" та реінвентив розробку. Це також призводить до великої кількості багів, особливо на етапах QA та продакшену, що вартує бізнесу додаткові витрати. Немає єдиного правильного шляху вирішення цих проблем, але існують базові підходи, які можна застосовувати на більшій кількості проєктів. Такі підходи можуть покривати близько 80% потреб багатьох проєктів, особливо тих, що працюють із легасі-кодом.

І так, наші супергеройські підходи — це свого роду мікс досвіду: досвід компаній, де я працював, та теорії, такі як Domain-Driven Design, чиста архітектура, чистий код, рефакторинг Мартина Фаулера. Загалом це сукупність цих речей. Коротко про кожен з підходів, і далі розглянемо детально кожен з них. Перший простий і банальний підхід — дотримання кодинг-стандартів. Наступний — розуміння відповідальності коду та його компонентів, розбивання коду на компоненти за цілями або типами, написання платформ-агностікоду, використання патернів, де це потрібно, та написання документації. І, звісно, тести, без них нікуди.

Детальніше про дотримання кодинг-стандартів. При виборі стандартів слідування, коли встановлюємо PHP-CS та налаштовуємо ruleset, ми виглядаємо, як в команді. Важливо вирішити, як писати код та як жити. Є речі, які повинні бути: PHP-CS для PSR-стандартів 1, 2, 12. Використання аналізаторів коду, таких як PHPStan та Psalm для типів. Також використовуємо копіпаст-детектор, який допомагає виявляти код-дублікати. Ще один корисний інструмент — PHPMND, який допомагає виявляти неймовірно складне для зрозуміння мовою кількість.

Щодо автоматизації, використовуємо різні інструменти, такі як Rector та PHP-CSFixer, які допомагають фіксувати проблеми. Ось деякі інші інструменти, такі як variable pint для змінних та coding стандарти для кожного фреймворку або платформи, які важливо використовувати. Нехай це стане доброю практикою враховувати відповідальність при написанні коду: розуміти, що робить клас, яка його зона відповідальності, з ким він взаємодіє та яка його роль в системі.

Слідувати принципу KISS — keep it simple, stupid. Я б ще додав би — keep it focused. Тому що насправді класи, які прості і сфокусовані на вирішенні конкретної задачі, яку вони вирішують, якщо вся система побудована на таких класах — у нас не буде проблем. Слідування принципу low-coupling, high-cohesion — це про принципи з ГРАСПу, про те, що клас має бути слабо зв'язаний з зовнішньою системою і щільно зв'язаний всередині себе. Тобто якась логіка всередині має пересікатися дуже щільно, а взаємодія з зовнішнім світом має бути якомога простішою, простішою і слабшою.

Які переваги ми отримуємо з таких сфокусованих компонентів, сфокусованих класів або елементів коду? Їх просто читати, легко підтримувати, просто використовувати і легко тестувати. Розбиття за ціллю або за типом. У цьому списку я зібрав найчастіше зустрічаються типи, які покривають 80% проблем, 80% велосипедів, створених. Перший тип – це Entity, звісно, куди без Entity. Це тип, який відображає унікальний предмет предметної області, унікальний об'єкт предметної області. Value Object – це також з DDD-підходу, це тип, який відображає атрибути Entity зазвичай, і він не є унікальним. Його не можна ідентифікувати, наприклад, як money або якась адреса. Репозиторії.

Усі ми знаємо паттерни на репозиторії. І це класи, це тип, який відповідає за зв'язок з базою даних, за обмін даними з базою даних. І це як про банальні CRUD-операції, так і про якесь більш складну логіку, як, наприклад, видалити неактивних користувачів, або щось подібне. Data Transfer Object – це об'єкти, які класно працюють, коли нам необхідно передати дані з одного шару в інший, наприклад, з якогось інфраструктурного шару, там, де база даних розташована. Ми передаємо інформацію в якийсь, наприклад, презентаційний шар, ближче до фронтенду. Ми їх, ці дані, пакуємо в Data Transfer Object і передаємо вище.

І виходить так, що у нас система, різні свої системи, залежать не одна від одної, а всі вони залежать від Data Transfer Object. Валідатори, банально, це типи класів, які можуть валідувати якийсь User Input і валідувати стан системи. Data Providers – це тип, який працює для провайдінга якихось повторно використовуваних даних, таких як якісь текстові блоки для фронтенду, або навіть шматки HTML для фронтенду, якщо таке потрібно. Mapei – це тип, який є створений для того, щоб мапити з одного типу в інший дані. Це, як такий, знаєте, спрощений білдер, насправді. Воно класно працює, якщо нам потрібно з User Input замапити дані в Entity, наприклад, або замапити дані в якийсь тип, який підходить для якихось нижчих шарів системи, для інфраструктурного шару.

І сервіси. Якщо коротко, то сервіси – це про повторне використання бізнес-логіки, але вони заслуговують на більше деталей, детальне розкриття. І так, сервіси. Сервіси – це про сфокусовану логіку бізнес-домену, бізнес-правил. І вони, навіть в назвах сервісу, необхідно використовувати слова, які говорять бізнес-правилами, типу видалити застарілих користувачів або відіслати якісь емейли конкретним користувачам. Сервіси – це про stateless операції. Це забезпечує їм повторне використання, якщо у нас сервіс містить якийсь State, це убиває просто повторне використання. Ми не завжди можемо використовувати його в іншому місці, тому що щось може піти не так. Сервіси – це про охоплення одного доменного use case. Знову ж таки, це про принцип Single Responsibility.

І сервіси не повинні знати нічого про контекст, з якого вони викликаються і з яким вони працюють. І одразу швидкі приклади про неправильний сервіс-контракт, який знає про контекст. Тобто у нас на слайді є якийсь клас абстрактний, якийсь сервіс, і сервіс цей клас приймає на вхід HTTP-реквест і прямо передає сервісу. І що виходить? Виходить, що у нас сервіс залежить від HTTP-контексту, і в етазі він не буде працювати в CLI-команді, тому що у нас там просто банально немає HTTP-реквесту. Або з якогось крон-джоба він не буде працювати. І як це вирішувати? Ми просто беремо, створюємо мапер, створюємо Data Transfer Object, мапимо ці всі речі і передаємо їх в сервіс. В такому вигляді, про який, ну в гнучкому вигляді, так? В роз'єднаному навіть вигляді. І все працює прекрасно.

Платформа Gnostic Code — це про те, що я тільки що сказав, у нас бізнес-логіки, шар бізнес-логіки, має бути максимально відрізаний від шару системи, від шару платформи, в якому ми знаходимося. І відразу приклад поганого коду. У нас є якийсь плагін, який наслідується від плагіна фреймворка, або якоїсь платформи. У нас є публічний метод, який викликається цією платформою. І є вся бізнес-логіка у нас зберігається, виконується в цих приватних функціях, які десь там знизу є уявома. В чому тут проблема? Проблема в тому, що нам буде важко зробити, що це все, що ми маємо, важко замінити фреймворки, що таке станеться, така потреба з'явиться. Тому що у нас шари змішані, логіка змішана, і очевидно нам пройдеться, швидше за все буде простіше просто переписати ці всі речі. Важко замінити environment, такі речі, як зміна бази даних, заміна якогось, не знаю, Redis для кешу чи Elasticsearch. Ці всі речі робити буде важко. Важко розбивати за модулями, тому що бізнес-логіка змішана, у нас коїться месиво насправді, і код живе настільки довго, як і фреймворк. Тобто якщо фреймворк помирає, код також помре, тому що швидше за все прийдеться його банально переписати. Це, до речі, до JavaScript прикольно відноситься, тому що там отак от фреймворк родився, а отак от і помер.

Приклад правильного коду, платформи-ності коду. У нас є той самий клас, той самий плагін, той самий публічний метод, який викликає, який викликається системою, і є якийсь сервіс, який робить бізнес-логіку, інкапсулює в собі бізнес-логіку. Просто лаконічно, банально, але чомусь так не роблять. І так, патерни, наступний підхід. Патерни — це круто, вони вирішують велику кількість проблем. Лаконічно, правильно вирішують, але тільки тоді, коли вони є доречними. Є прикольна стаття на цю тему. З таким от графіком, коли якийсь розробник набирається свого досвіду, починає знайомитись з патернами, кількість патернів, які він використовує у своєму коді, росте моментально вверх.

І згодом, коли він набирається досвіду, він починає розуміти, коли це є доречно, коли можна використати патерн, а коли він просто вносить непотрібну комплексіті до коду. Ось. Детальніше про патерни. Будьте обережні з ними, це те, про що я сказав. Вони не є завжди доречними. Подумайте двічі перед тим, як використовувати якийсь патерн. Чи він приносить більше комплексіті і вирішує вашу проблему, так? А може не варто писати 40 класів, наприклад? Може там можна якийсь один написати, і це вирішить вашу проблему. Варто сфокусуватися на таких дефолтних звичайних патернах, які широко використовуються. Таких як, наприклад, патерн, господи, factory, як репозиторій, як strategy. Тому що, на мою думку, 20% патернів з книжки «Банди чотирьох» вирішують 80% проблем. Тобто не варто все пробувати і тулить все у всі місця. І використовувати правильне найменування. Це важливо, тому що патерни — це насправді про найменування. Тобто якщо розробник приходить в код, і бачить назву strategy, він відразу розуміє, що буде коїться в цьому коді. Він розуміє, як приблизно strategy працює, як з нею взаємодіяти, як її розширяти. Але якщо найменування неправильне, то буде мясиво, з'явиться десь знизу паралельний слой ще одного strategy, і це буде не добре.

Існування антипатернів — це також необхідно пам'ятати. Я думаю, у кожного була така історія, коли ти починаєш займатися розробкою, відкриваєш книжку, бачиш strategy, і такий — щасливий, коротше, все на... Ой, не strategy, а singleton. І думаєш, коротше, все на singletonі побудую. Насправді так не працює. Треба це пам'ятати, пам'ятати про список цих антипатернів, і розуміти, чому той чи інший патерн є антипатерном, коли він доречний, а коли ні. Документація. Не варто дублювати типи PHP. Тому що в PHP 7, здається, у нас з'явилися стріктайпи для методів, для аргументів методів, для return-тайпів, і чомусь всі продовжують дублювати це в PHP-доках. В цьому немає ніякого сенсу. Ну, окрім тих випадків, коли нам треба указати типізацію масиву, наприклад.

Є класна філософія Docs as Code. Вона говорить про те, що документація повинна лежати поруч з кодом, і документація повинна бути під системою контролю версій. Це про ту проблему, про яку я казав на початку, якщо у нас документація знаходиться десь далеко в Confluence, типу піти її заапдейтити, ну це така задача, що я ще подумаю, чи це робити, тому що я можу про це забути, можу не хотіти, ну різні причини бувають. Але коли у нас в коді, в якому-небудь модулі, лежить папка «документація» або лежить якийсь README-файл, це банально простіше, зручніше, пішов, передивився, і ти завжди знаєш, де цю документацію шукати, ти майже впевнений в тому, що вона там актуальна, ти можеш подивитися історію комітів і все таке.

Не варто документувати якісь маленькі речі, які можна зрозуміти з коду, які можна зрозуміти з тестів. Необхідно документувати такі речі, які важкі для розуміння в цілому. Такі, як, наприклад, взаємодія з сторонніми системами, якими-небудь сервісами, коли їх багато, і на проєкті є якась людина. Одна є, інша є. І якщо є людина, яка знає, як це все працює, то до добра це не доводить. Документація потрібна для таких речей. І ще є цікавий підхід для документації архітектурних рішень, але таких, якщо ви не архітектори, а просто розробники звичайні, смертні. Можна використовувати такий підхід, як architectural decision record. Це штука, яка говорить про те, що необхідно документувати архітектурні рішення, те, як ви до цих рішень прийшли, які були аналоги рішень, які були плюси і мінуси, навіть дискусію можна задокументувати так, щоб через п'ять років людина прийшла і така, а чого тут так воно працює. Пішла, подивилася цей документик і зрозуміла, чого воно так працює. І можливо переробила, тому що деякі речі вже є неактуальними.

Приклад гарної документації. У нас є якийсь MD-файл, у нас є короткий опис, у нас є діаграма того, як система працює в цілому. Круто. Правильно, лаконічно, коротко. Наступний підхід — це покриття тестами. Тести — це важливо, ми всі це знаємо. Тести вміють знаходити проблему дуже швидко. Тести є частиною документації насправді. Якщо ми хочемо зрозуміти, як працює та чи інша частина коду, ми йдемо, дивимося, яким чином воно тестується, і отримуємо розуміння. Ну, в ідеалі — повне розуміння. Тести дають нам змогу безпечно рефакторити систему. Сьогодні на панельній дискусії говорили про це.

Якщо у нас все покрите тестами, ми виносимо якісь компоненти, рефакторимо, робимо що завгодно, запускаємо тести, бачимо, чого воно впало, чи тому, що ми щось поламали, чи тому, що просто треба якийсь мокновий підкинути, наприклад. І це дає нам змогу навіть не йти і не дивитися на фронтенд, чи воно там працює нормально чи ні. Ми впевнені, ну, майже впевнені з юніт-тестів, що все працює так, як треба. Ну, не юніт-тестів, а в цілому тестів. І тести — це річ, про яку не говорять в книжках. Тести — це річ, яка змушує тебе думати як тест. Коли ти пишеш якийсь код, відразу десь в голові в тебе проскакує думка про те, чи зможу я покрити цей код тестом. І якщо ні, то це вже такий кавикольчик, такий дзвіночок, що треба йти і якось переписувати, щось міняти у своєму житті.

І знову ж таки, ця діаграма, яка говорить про те, що баг, який знайдений на етапі юніт-тестування, він є досить дешевим до фіксу. Найрозповсюдженіші типи тестів, юніт-тести, ми всі про них знаємо, це маленькі лаконічні прості тести, які швидко виконуються, тестують якийсь один юніт, якусь одну функцію, один клас. Компонентні тести. До речі, ці типи наступні можуть називатися в кожній системі, в кожній платформі по-різному, але фактично вони роблять одне і те саме. І так компонентні тести, це тести, які тестують якусь частинку логіки, якусь прослойку логіки в ізоляції від інших.

До прикладу, у нас є якийсь фасад в коді, ми піднімаємо якусь базу даних, проганяємо ці методи нашого фасаду і розуміємо, що цей модуль, ця частина коду працює правильно або неправильно. Інтеграційні тести, це тести, які піднімають систему в цілому, навіть фронтенд піднімають, базу даних, кеші, всякі search engine, що завгодно. І тестують систему в цілому, з усім піднятим, і тестують якийсь конкретний flow навіть. Ось. І крута штука, яка в системах контролю версій, це те, що в нас є. Який ряд коду покриті тестами, а який ні, називається кубертюра test coverage. Воно класно працює з GitLab, наприклад, прям парсить. Ти GitLab скармлюєш якусь XML, якусь output test coverage репорта, і він тобі показує, який ряд коду покритий тестами, а який ні, віддає тобі відсоток покриття, і ти вже розумієш, чи треба тобі щось вчинити, дописувати тести, чи переписувати якимось чином.

В принципі, це всі практики, які покривають 80% наших потреб і фіксують 80% наших проблем. Є сенс поговорити про плюси та мінуси таких підходів, так? Виконання, слідування таким підходом. Плюси. У нас виростає maintainability, тобто якщо проект створений для того, щоб жити 5 років, якщо ми слідуємо таким практикам, швидше за все він буде жити 5 років, його можна буде підтримувати, фіксувати якісь проблеми, і це не буде дуже важко. Проект легко читати і розуміти. Це важливо, як і для нових розробників, які приходять, тому що, скажімо, садить якийсь сеньор 10 років на проєкті, він звільнився, вся інформація була в нього в голові, але у нас є документація, у нас правильно написаний код, тому немає проблем привести когось нового в систему.

Зменшення кількості багів та помилок. Це те, що я переговорив про бізнес. Тобто, якщо ми своїми намаганнями, своїм чистим кодом фіксуємо проблеми до того, як вони з'являться, ми економимо гроші, а гроші — це прикольно. Проект може розширятися, може існувати, може якось обростати новим функціоналом, і в цьому не буде ніяких проблем. Мінуси. Оверхед. Таке буває, щоб почати слідувати правильним практикам, треба витратити якийсь час. Більше часу, ніж зазвичай. Навчання. Штука в тому, щоб вся команда писала такий код правильний, грамотний. Треба навчити всіх членів команди це робити. І так, треба витратити якийсь час на це. Оверінженіринг. Це те, що я говорив про патерни, коли ти починаєш слідувати цим всім практикам, читаєш розумні книжки. Іноді таке стається, що ти будуєш велосипеди знову і знову.

І тут дуже важливо знайти цей баланс між велосипедобудуванням і тим, які проблеми ти вирішуєш. І невеличкі витрати є на те, щоб внести тулзи, всі ICD (Integrated Code Development). Це вся автоматизована перевірка коду. Але в принципі це не дуже складно. Ось в принципі і все. Після всіх цих практик ми починаємо писати крутий, успішний код. І хочу додати, що фактично ми тут з вами знаєте, чим займаємося. Ми граємо в конструктор LEGO, тому що програмування це як LEGO з блоками. І наша задача в тому, щоб ці блочки маленькі ставити в потрібні місця. Дякую.

Sign in
Or by mail
Sign in
Or by mail
Register with email
Register with email
Forgot password?