Senior: питання на співбесіді з теми «Чиста архітектура й тестованість»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Позиція «фреймворк - деталь» (чиста архітектура): бізнес-логіка не повинна знати про Laravel. Ніяких моделей Eloquent, фасадів, хелперів у ядрі - лише чисті PHP-класи, інтерфейси й DTO, а фреймворк лише доставляє запити й зберігає дані.
Позиція Laravel-спільноти: фреймворк - не деталь, а інструмент продуктивності. Eloquent, фасади, черги, події дають швидкість розробки саме завдяки тісній інтеграції. Абстрагування від них - це подвоєння коду заради заміни, яка ніколи не станеться.
Що коштує повна ізоляція:
- окремі доменні сутності й Eloquent-моделі + відображення між ними;
- репозиторії-обгортки над Eloquent з інтерфейсами;
- DTO на кожній межі;
- втрата зручностей (зв'язки, ліниве завантаження, події моделей,
toResource()); - новим людям складніше: «Laravel-проєкт» не схожий на Laravel.
Що вона дає:
- бізнес-правила тестуються миттєво, без бази й контейнера;
- складна предметна область (фінанси, страхування, логістика) не тоне в інфраструктурному коді;
- довговічність: ядро переживає великі оновлення фреймворку.
Прагматичний середній шлях, що працює в більшості проєктів:
- дії (actions) чи сервіси для сценаріїв - бізнес-логіка не в контролерах, моделях чи Livewire-компонентах;
- Eloquent лишається доменною моделлю - Active Record з методами домену (
$order->cancel()), скоупами й кастами; - ізолювати зовнішні системи, а не власну базу: інтерфейси для платежів, SMS, сторонніх API - тут заміна й фейки реально потрібні;
- value objects і енуми для доменних понять (гроші, статуси, діапазони дат);
- arch-тести на напрям залежностей (домен не імпортує
Illuminate\Http); - фасади - за потреби, з розумінням, що в тестах вони підміняються (
Mail::fake()).
Критерій вибору: складність предметної області порівняно з технічною. CRUD-адмінка, каталог, блог - Laravel-шлях. Складні правила з багатьма інваріантами й довгим життям - більше ізоляції, хоча б для ядра цієї частини (модульний моноліт, де лише складний модуль має «чисту» архітектуру).
Найгірший варіант - половинчастий: інтерфейси репозиторіїв, що повертають моделі Eloquent, і «сервіси», що лише проксюють виклики. Складність є, а користі немає.
Еволюційна архітектура (Ніл Форд, Ребекка Парсонс, Патрік Куа) - підхід, за яким архітектуру не проєктують раз і назавжди, а постійно змінюють разом із системою. Ключове питання - як змінювати, не втрачаючи важливих властивостей (продуктивності, безпеки, модульності).
Функція придатності (fitness function) - автоматична перевірка конкретної архітектурної властивості. Термін запозичено з еволюційних алгоритмів, де функція придатності оцінює, наскільки рішення близьке до мети.
Приклади функцій придатності:
- модульність: arch-тести Pest - «модуль Billing не залежить від внутрішніх класів Catalog», «домен не імпортує HTTP-шар»;
- продуктивність: тест, що головна сторінка робить не більше 15 SQL-запитів (
expectsDatabaseQueryCount); бюджет часу відповіді в навантажувальних тестах; - розмір збірки фронтенду: CI падає, якщо JavaScript-бандл перевищив 250 КБ;
- безпека:
composer auditбез вразливостей високого рівня, заборонені функції черезarch()->preset()->security(); - якість: рівень PHPStan, покриття критичних модулів тестами;
- операційні: моніторинг SLO в продакшені - «функція придатності», що працює постійно.
Види:
- атомарні (одна властивість) і цілісні (поєднання - продуктивність разом із безпекою);
- тригерні (запускаються в CI на кожну зміну) і безперервні (моніторинг у продакшені);
- статичні (правила коду) і динамічні (вимірювання під навантаженням).
Чому це важливо: архітектура деградує поступово - кожен окремий pull request «трохи» порушує межі, і через рік модульний моноліт стає великою грудкою бруду. Функції придатності роблять архітектурні рішення виконуваними: порушення помітне в момент внесення, а не на ретроспективі.
Інші принципи еволюційної архітектури:
- останній відповідальний момент для рішень - вирішувати, коли знань достатньо, але до того, як відкладання стане дорожчим;
- зменшення зв'язності - чим менше залежностей, тим легше змінювати частини незалежно;
- малі інкрементальні зміни з можливістю відкату - замість великих переписувань.
Як почати: обрати 2-3 властивості, порушення яких найболючіші для проєкту (часто - межі модулів і N+1), і автоматизувати їх перевірку в CI.
Докладніше в документації: Building Evolutionary Architectures
Роберт Мартін запропонував метрики, що перетворюють розмови про «зв'язність модулів» на числа.
Аферентна зв'язність (Ca) - скільки інших модулів залежать від цього. Висока Ca - модуль важливий для багатьох; його зміна зачіпає багатьох.
Еферентна зв'язність (Ce) - від скількох модулів залежить цей. Висока Ce - модуль залежить від багатьох; зміни в будь-якому з них можуть його зламати.
Нестабільність:
I = Ce / (Ca + Ce) від 0 до 1
- I ≈ 0 - стабільний модуль: від нього багато залежать, він сам ні від кого. Змінювати його дорого (багато споживачів). Приклад: спільні value objects, базові інтерфейси, ядро предметної області;
- I ≈ 1 - нестабільний: ні від нього ніхто, а він від багатьох. Змінювати легко й безпечно. Приклад: контролери, консольні команди, UI.
Принцип стабільних залежностей: залежності мають іти в напрямку стабільності - нестабільні модулі залежать від стабільних, а не навпаки. Якщо стабільний модуль (від якого залежить половина системи) залежить від нестабільного, кожна зміна нестабільного «трусить» усю систему.
Абстрактність (A) - частка абстрактних класів та інтерфейсів у модулі. Принцип стабільних абстракцій: стабільні модулі мають бути абстрактними (щоб їх можна було розширювати без зміни), нестабільні - конкретними.
Головна послідовність: ідеально A + I ≈ 1. Відхилення показує проблемні зони:
- «зона болю» (стабільний і конкретний: I ≈ 0, A ≈ 0) - від модуля всі залежать, але його неможливо розширити без зміни. Типово для «утилітних» класів і перевантажених базових моделей;
- «зона марності» (нестабільний і абстрактний) - інтерфейси, які ніхто не використовує.
Як застосувати в PHP-проєкті:
- інструменти: PhpMetrics, Deptrac (ще й перевіряє дозволені залежності між шарами), pdepend;
- дивитися на тенденції між релізами, а не на абсолютні значення;
- для модульного моноліту - головні метрики на рівні модулів (
Billing,Catalog), а не окремих класів.
Застереження: метрики - сигнал для розмови, а не мета. Оптимізувати «I» заради числа - створити штучні інтерфейси. Корисне питання: «чому від цього модуля залежить усе?» - і чи має так бути.
У великій команді один архітектор, через якого проходять усі рішення, стає вузьким місцем: рішення чекають тижнями, а ухвалюються далеко від людей, що знають деталі. У маленькій - навпаки, рішення ухвалюються «мимохідь», і через рік ніхто не пам'ятає чому.
Поділ рішень за вартістю зміни:
- оборотні («двосторонні двері») - легко змінити: структура класів усередині модуля, вибір бібліотеки для внутрішньої задачі. Ухвалюються швидко, тими, хто робить роботу;
- необоротні («односторонні двері») - дорого змінити: формат публічного API, основна база, розбиття на сервіси, модель даних, автентифікація. Тут варто зупинитися, зібрати думки й записати рішення.
Процес порад (advice process) - підхід, який описує Андрю Гармел-Ло в статті на сайті Мартіна Фаулера:
- будь-хто може ухвалити архітектурне рішення, але спершу мусить порадитися з тими, кого воно зачепить, і з тими, хто має досвід у цій галузі;
- порада не є вето - відповідальність за рішення лишається на тому, хто його ухвалює;
- рішення фіксується в ADR - разом із контекстом, альтернативами й отриманими порадами;
- архітектурний форум (регулярна зустріч) - місце, де обговорюють поточні рішення й діляться ними, а не орган затвердження.
Інструменти, що підтримують такий підхід:
- ADR - пам'ять про рішення і їхні причини;
- принципи архітектури - кілька загальноприйнятих правил («межі модулів не перетинаємо напряму», «нові інтеграції - через черги»), щоб більшість рішень можна було ухвалити самостійно, звіряючись із ними;
- технологічний радар - перелік технологій зі статусами «використовуємо», «пробуємо», «не використовуємо»;
- функції придатності - автоматична перевірка того, що вже вирішено.
Чого уникати:
- рішення без обговорення з тими, хто житиме з наслідками (експлуатація, безпека, інші команди);
- комітет, що затверджує все - повільно й знімає відповідальність з авторів;
- рішення без запису - через рік їх «виправляють» люди, які не знають контексту.
На співбесіді важливо показати, що архітектура - не лише технічні знання, а й процес: як збирати інформацію, зважувати компроміси, документувати й поширювати рішення.
Докладніше в документації: Martin Fowler: Scaling the Practice of Architecture, Conversationally