Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Senior: питання на співбесіді з теми «Рефакторинг і зв'язність»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

5 питань

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

Борг буває різним (квадрант Мартіна Фаулера):

  • Свідомий і обачний: «випускаємо зараз, бо дедлайн, і знаємо, що треба буде переробити». Нормальне бізнес-рішення.
  • Свідомий і необачний: «на дизайн немає часу» - постійно.
  • Несвідомий і обачний: «тепер ми розуміємо, як це треба було зробити» - природний наслідок навчання.
  • Несвідомий і необачний: команда просто не знає, як краще.

Як керувати:

  • Робити видимим. Задачі в трекері з описом наслідків, а не «відрефакторити X»: «додавання нового способу оплати займає 3 дні замість пів дня через Y».
  • Говорити мовою бізнесу. «Рефакторинг» не продається; «зменшимо кількість інцидентів у платежах» і «пришвидшимо релізи нових інтеграцій» - продаються.
  • Гасити постійно, а не «колись». Частина ємності кожного спринту, правило бойскаута при роботі в коді, а не окремий «спринт рефакторингу» раз на рік.
  • Пріоритезувати за відсотками. Найдорожчий борг - у коді, який часто змінюється. Заплутаний модуль, якого ніхто не чіпав три роки, може почекати. Аналіз «hotspots» (частота змін × складність файлів з історії Git) показує, де борг коштує найбільше.
  • Не накопичувати нового непомітно: код-рев'ю, статичний аналіз у CI, тести як умова злиття.

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

Докладніше в документації: Technical Debt

Bounded context (обмежений контекст) - поняття з DDD: межа, всередині якої терміни й модель мають одне чітке значення. У великій системі одне слово означає різне в різних частинах.

«Товар» у каталозі - опис, фото, характеристики. На складі - залишки, комірка, вага. У бухгалтерії - собівартість і податкова група. Спроба зробити одну модель Product для всіх трьох дає клас на сотні полів, який змінюють усі команди й ніхто не розуміє повністю.

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

Як ділити моноліт на модулі (модульний моноліт):

  1. Знайти межі за мовою й бізнес-процесами: де змінюється значення термінів, які частини змінюються разом, які команди відповідають за що.
  2. Модуль = каталог з публічним API: app/Billing, app/Catalog, app/Shipping. Інші модулі звертаються лише до публічного інтерфейсу (сервіси, події), а не до внутрішніх моделей і таблиць.
  3. Зв'язок між модулями - через події (OrderPlaced → Billing створює рахунок) або явні виклики фасаду модуля.
  4. Свої таблиці для модуля. Запити JOIN через межу модуля - сигнал, що межа неправильна або потрібна копія даних.
  5. Автоматична перевірка меж: архітектурні тести (Pest arch(), Deptrac) забороняють Catalog імпортувати внутрішні класи Billing.

Чому модульний моноліт, а не одразу мікросервіси: межі спершу майже завжди визначають неточно. Перенести код між модулями - рефакторинг; між мікросервісами - міграція даних, нові API й розподілені транзакції. Добре відокремлений модуль легко винести в сервіс пізніше, коли для цього з'явиться справжня причина: незалежне масштабування, окрема команда, інший цикл релізів.

Докладніше в документації: Bounded Context

Великі зміни (заміна ORM-шару, платіжного провайдера, пошукового рушія, переписування ключового модуля) в окремій гілці Git на тижні - класична пастка: гілка відстає від main, злиття стає болісним, а результат не перевірений у продакшені до самого кінця.

Branch by Abstraction - «гілкування» всередині коду замість гілки в Git:

  1. ввести абстракцію перед частиною, що змінюється, і перевести на неї всіх клієнтів - поки з єдиною реалізацією, старою:
interface SearchEngine
{
    public function search(SearchQuery $query): SearchResults;
}

final class DatabaseSearch implements SearchEngine { /* наявний код */ }
  1. поступово писати нову реалізацію поруч - у main, маленькими комітами, з тестами:
final class MeilisearchSearch implements SearchEngine { /* нова */ }
  1. перемикати клієнтів на нову реалізацію - через конфігурацію чи feature flag, частинами:
$this->app->bind(SearchEngine::class, fn () => Feature::active('new-search')
    ? app(MeilisearchSearch::class)
    : app(DatabaseSearch::class));
  1. видалити стару реалізацію, а за потреби - і саму абстракцію, якщо вона більше не потрібна.

Переваги:

  • код завжди в робочому стані і постійно інтегрується - немає великого злиття;
  • зміни потрапляють у продакшен поступово і можуть бути вимкнені миттєво;
  • паралельна робота: інша команда продовжує розробку, не чекаючи на завершення рефакторингу;
  • перевірка на реальних даних: нову реалізацію можна ввімкнути для частини користувачів чи запускати «в тіні» й порівнювати результати.

Інструменти:

  • feature flags - у Laravel - Pennant (Feature::active()), з поступовим ввімкненням для відсотка користувачів;
  • контейнер - перемикання реалізацій без змін у клієнтах;
  • тести на контракт абстракції - однаковий набір тестів для старої й нової реалізацій.

Що може піти не так:

  • абстракція, «зліплена» зі старої реалізації, - нова не вкладається в неї. Абстракцію варто проєктувати від потреб клієнтів, а не від методів старого класу;
  • прапорці, які ніхто не прибирає - після завершення переходу їх треба видалити разом зі старим кодом;
  • дані: якщо реалізації по-різному зберігають дані, потрібна міграція чи подвійний запис на перехідний період.

Пов'язане: Strangler Fig - та сама ідея на рівні систем і маршрутизації запитів, Branch by Abstraction - на рівні коду всередині застосунку.

Докладніше в документації: Мартін Фаулер: Branch By Abstraction

Ручний рефакторинг великої кодової бази повільний і схильний до помилок. Інструменти беруть на себе механічну частину.

Rector - автоматичні перетворення коду за правилами:

// rector.php
return RectorConfig::configure()
    ->withPaths([__DIR__.'/app', __DIR__.'/tests'])
    ->withPhpSets()                       // сучасний синтаксис для версії PHP з composer.json
    ->withPreparedSets(deadCode: true, codeQuality: true, typeDeclarations: true);
vendor/bin/rector process --dry-run   # показати зміни
vendor/bin/rector process             # застосувати

Що він робить: оновлює синтаксис (властивості конструктора, match, readonly, енуми), додає типи, прибирає мертвий код, переводить між версіями фреймворків (набори для Laravel - пакет driftingly/rector-laravel). Кожне правило - детерміноване перетворення AST, тож результат передбачуваний на тисячах файлів.

PHPStan / Larastan - статичний аналіз: знаходить помилки типів, виклики неіснуючих методів, неправильні аргументи без запуску коду. Larastan додає розуміння Laravel (магія Eloquent, фасади, контейнер).

Baseline - як впровадити аналіз у старий проєкт:

vendor/bin/phpstan analyse --generate-baseline

Усі наявні помилки записуються у phpstan-baseline.neon і ігноруються. Новий код перевіряється за повними правилами - нові помилки не додаються, а базову лінію поступово зменшують. Те саме вміють Psalm і Rector (пропуск окремих правил чи шляхів).

Як це вбудувати в процес:

  • CI: PHPStan, Pint (стиль), Rector в режимі --dry-run і тести на кожен pull request - злиття блокується при помилках;
  • рівень аналізу піднімати поступово: рівень PHPStan 0 → 5 → 8 → max, з baseline на кожному кроці;
  • рефакторинг окремими комітами: механічні зміни Rector не змішувати з бізнес-змінами - рев'ю тоді простіше;
  • тести перед масовими змінами - статичний аналіз не ловить зміну поведінки.

Чого інструменти не роблять: не вирішують, як розділити відповідальності, які абстракції потрібні, як назвати поняття. Вони прибирають механічну роботу, звільняючи час на архітектурні рішення.

Пастка baseline: він може перетворитися на «смітник», куди складають нові помилки, щоб пройти CI. Варто стежити, щоб кількість записів лише зменшувалася.

Докладніше в документації: Документація Rector

У будь-якому великому проєкті поганого коду більше, ніж часу на його виправлення. Рефакторити «все погане» неможливо - і не потрібно: складний модуль, який ніхто не змінює роками, нікому не заважає.

Аналіз гарячих точок (hotspots, ідея Адама Торнгілла «Код як місце злочину») поєднує два виміри:

  • складність коду - довжина, вкладеність, цикломатична складність файлу чи методу;
  • частота змін - скільки разів файл змінювали за останні місяці (з історії Git).

Гаряча точка = складний код, який часто змінюють. Саме там зосереджені витрати часу команди й помилки: кожна зміна складного файлу повільна й ризикована, і робиться вона часто.

# найчастіше змінювані файли за рік
git log --since="1 year ago" --name-only --format="" | sort | uniq -c | sort -rn | head -20

Перетин цього списку з найскладнішими файлами (за даними PHPStan, phpmetrics, PhpStorm) дає короткий список кандидатів.

Чому це краще за інтуїцію:

  • пріоритет за впливом: покращення файлу, який змінюють щотижня, окупається швидко;
  • аргумент для бізнесу: «80% змін за квартал зачіпали ці три файли, і в них 60% багів» - зрозуміліше, ніж «код поганий»;
  • виявляє приховані проблеми: часто гаряча точка - «божественний» клас (OrderService на 3000 рядків), через який проходить половина змін.

Додаткові сигнали з історії Git:

  • зв'язок змін (change coupling): файли, які майже завжди змінюються разом, - прихована залежність чи неправильно розділена відповідальність;
  • знання авторів: файли, які змінювала одна людина, - ризик для команди («bus factor»);
  • частота виправлень: коміти з «fix» у тих самих файлах.

Як діяти з гарячою точкою:

  • рефакторити поступово, разом з функціональними змінами - правило бойскаута: залишити файл трохи кращим після кожної зміни;
  • спершу тести для поведінки, яку змінюють найчастіше;
  • розділяти великий клас на менші за відповідальностями - щоб зміни розподілилися;
  • виміряти після: чи зменшилися складність і кількість змін, що зачіпають файл.

Інструменти: CodeScene (комерційний), git log з простими скриптами, phpmetrics, SonarQube - для оцінки складності.

Докладніше в документації: CodeScene: гарячі точки