Питання на співбесіді: Рефакторинг і зв'язність
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
15 питань
Code smell («запах коду») - ознака, що в коді, ймовірно, є проблема дизайну. Сам по собі не баг - код працює, - але його важко читати, змінювати й тестувати.
Найпоширеніші:
- Довгий метод. Метод на 100+ рядків, який робить кілька речей. Лікується виділенням методів з іменами, що пояснюють намір.
- Великий клас («God object»): тисячі рядків, десятки залежностей. Розділити за відповідальностями.
- Дублювання: однакова логіка в кількох місцях - зміну доведеться робити скрізь, і одне місце точно забудуть.
- Довгий список параметрів:
createUser($name, $email, $phone, $role, $team, $sendEmail, $isAdmin). Об'єкт-параметр (DTO) чи кілька методів. - Прапорець-параметр:
render(true)- незрозуміло без заглядання всередину. Два методи чи іменований аргумент. - Одержимість примітивами: гроші як
float, email якstring, статус як магічний рядок. Value objects і enum. - Заздрість до функцій (feature envy): метод постійно звертається до даних іншого класу - можливо, він має жити там.
- Розгалуження за типом: однаковий
switch ($type)у кількох місцях - кандидат на поліморфізм. - Магічні числа:
if ($status === 3),sleep(86400). - Коментар, що пояснює заплутаний код, - часто краще переписати код так, щоб пояснення стало зайвим.
Як з ними працювати: запах - привід придивитися, а не вимога негайно переписувати. Рефакторять тоді, коли код доводиться змінювати: «правило бойскаута» - залишити код трохи кращим, ніж знайшли.
Частину запахів знаходять інструменти: PHPStan/Larastan, PHP Mess Detector, метрики складності в IDE.
Рефакторинг - зміна структури коду без зміни поведінки. Без тестів неможливо переконатися, що поведінка справді не змінилася. Тому перший крок - створити страховку.
1. Тести-характеристики (characterization tests). Не «як має працювати», а «як працює зараз», включно з дивацтвами. Викликати код з різними входами, записати результати й зафіксувати їх у тестах.
it('рахує знижку так само, як до рефакторингу', function (array $order, int $expected) {
expect(DiscountCalculator::calculate($order))->toBe($expected);
})->with([
[['total' => 1000, 'vip' => false], 0],
[['total' => 1000, 'vip' => true], 100],
[['total' => 5000, 'vip' => false], 250],
]);
2. Тести на верхньому рівні, якщо код важко тестувати окремо: feature-тест HTTP-ендпоінту, що перевіряє відповідь і зміни в базі. Грубо, але надійно.
3. Маленькі кроки. Перейменувати змінну, виділити метод, перенести метод - по одній зміні, з прогоном тестів після кожної. Невдалий крок легко відкотити.
4. Автоматичні рефакторинги IDE та Rector. «Rename», «Extract Method», «Move» у PhpStorm змінюють усі посилання коректніше, ніж ручні правки. Rector автоматизує масові зміни.
5. Окремо рефакторинг, окремо нові функції. Не змішувати в одному коміті: якщо щось зламалося, видно, що саме.
6. Статичний аналіз (PHPStan/Larastan) ловить зламані виклики й типи, яких тести можуть не покрити.
Чого не робити: «великого переписування» без тестів - найризикованішого сценарію. Краще поступово: нова структура поруч зі старою, поступове перенесення (патерн «Strangler Fig»), старий код видаляється, коли на нього вже ніхто не посилається.
Рефакторинг - зміна внутрішньої структури коду без зміни його поведінки. «Виділити метод» (Extract Method / Extract Function) - найчастіший з них: фрагмент коду переноситься в окремий метод з назвою, що пояснює навіщо він.
До:
public function checkout(Cart $cart, User $user): Order
{
// перевіряємо, що всі товари в наявності
foreach ($cart->items as $item) {
if ($item->product->stock < $item->qty) {
throw new OutOfStock($item->product);
}
}
// рахуємо суму зі знижкою
$total = $cart->items->sum(fn ($i) => $i->product->price * $i->qty);
if ($user->isVip()) {
$total = (int) round($total * 0.9);
}
return Order::create(['user_id' => $user->id, 'total' => $total]);
}
Після:
public function checkout(Cart $cart, User $user): Order
{
$this->ensureInStock($cart);
return Order::create(['user_id' => $user->id, 'total' => $this->totalFor($cart, $user)]);
}
private function ensureInStock(Cart $cart): void { /* ... */ }
private function totalFor(Cart $cart, User $user): int { /* ... */ }
Коли виділяти:
- коментар пояснює, що робить блок коду - назва методу може замінити коментар;
- метод не вміщується на екран чи має кілька рівнів вкладеності;
- той самий фрагмент повторюється в кількох місцях;
- змішано рівні абстракції: бізнес-кроки поруч із деталями (цикли, форматування, SQL);
- блок потрібно тестувати окремо.
Як робити безпечно:
- переконатися, що є тести (або написати їх для поточної поведінки);
- виділити метод - автоматично в IDE (PhpStorm: Extract Method), щоб не помилитися з параметрами й поверненим значенням;
- дати назву за наміром («чи в наявності», «сума зі знижкою»), а не за реалізацією («цикл по товарах»);
- запустити тести.
Коли виділення не допомагає: якщо виділеному фрагменту потрібно передати шість параметрів і повернути три значення - це ознака, що код хоче стати окремим класом (Extract Class), а не методом.
Зворотний рефакторинг - Inline Method - коли метод-обгортка нічого не пояснює і лише ускладнює читання.
Докладніше в документації: Каталог рефакторингів: Extract Function
Код читають набагато частіше, ніж пишуть. Перейменування - найдешевший рефакторинг з найбільшим ефектом на читабельність: назва, що точно описує сутність, позбавляє потреби читати реалізацію.
Ознаки поганих назв:
$data = $this->get($id); // які дані? що «отримати»?
$flag = true; // який прапорець?
$tmp, $res, $arr, $obj; // назви за типом, а не за змістом
function process($x) {} // «обробити» - що саме?
$d = 30; // 30 чого? днів? хвилин?
Як краще:
$invoice = $this->findInvoice($invoiceId);
$isRegisteredForDiscounts = true;
function sendOverdueReminders(Collection $invoices): void {}
$gracePeriodInDays = 30;
Правила, що допомагають:
- за наміром, а не за реалізацією:
activeSubscribers(), а неgetUsersWhereStatusIsOneAndPaidIsTrue(); - булеві значення - питанням:
isPaid,hasAccess,canPublish,shouldRetry; - методи - дієсловом:
calculateTotal(),sendInvoice(); класи й змінні - іменником; - одиниці виміру в назві, якщо тип їх не передає:
timeoutSeconds,priceCents,sizeInBytes; - мова предметної області: якщо бізнес каже «замовлення», «відвантаження», «повернення» - у коді
Order,Shipment,Refund, а неItem,Process,Operation; - послідовність: не
fetch/get/retrieve/loadдля того самого типу дії в різних місцях; - довжина пропорційна області видимості:
$iу короткому циклі - нормально; змінна, що живе в усьому класі, - описова назва.
Перейменування безпечне з інструментами: IDE (Rename у PhpStorm) змінює всі використання, включно з рядковими посиланнями й документацією; статичний аналіз (PHPStan) і тести ловлять пропущене.
Пастки:
- публічні API (маршрути, поля JSON, назви колонок, події) - перейменування ламає клієнтів і потребує міграції чи періоду сумісності;
- рядкові посилання (
config('services.old_name'), назви в Blade,$model->getAttribute('field')) IDE може не знайти; - магічні методи й динамічні виклики - перевіряти тестами.
Корисне правило: якщо назву важко придумати, часто проблема не в назві, а в коді - метод чи клас робить забагато різного.
Докладніше в документації: Каталог рефакторингів: Rename Variable
DRY (Don't Repeat Yourself) - кожне знання в системі має мати одне джерело. Дублювання шкодить, бо зміну доводиться вносити в кількох місцях, і рано чи пізно одне з них пропускають.
Але не всяке схоже код - дублювання знання. Два фрагменти можуть виглядати однаково випадково, а змінюватися з різних причин:
// валідація реєстрації
'name' => ['required', 'string', 'max:255'],
// валідація назви товару
'name' => ['required', 'string', 'max:255'],
Це не одне знання: правила для імені користувача й назви товару завтра розійдуться. Спільна функція nameRules() їх штучно зв'яже.
«Неправильна абстракція» (Сенді Метц) - типовий сценарій:
- програміст бачить повтор і виносить код у спільну функцію чи базовий клас;
- з'являється новий випадок, що «майже підходить» - додають параметр;
- ще один випадок - ще параметр, умова всередині;
- через рік абстракція має прапорці
$isAdmin,$skipValidation,$legacyMode, і ніхто не розуміє, як вона працює, а змінювати страшно, бо вона використовується скрізь.
Висновок Метц: «Дублювання значно дешевше за неправильну абстракцію». Якщо абстракція обросла параметрами й умовами, вигідніше повернути код назад (вбудувати в місця використання) і заново подивитися, що справді спільне.
Практичні правила:
- правило трьох: перший раз - пишемо, другий - терпимо дублювання, третій - виносимо абстракцію. До третього разу видно, що справді спільне;
- дублюється знання чи текст? Якщо обидва місця змінюватимуться разом з тієї самої причини - це знання, його варто об'єднати;
- ознаки поганої абстракції: булеві параметри, що вмикають гілки;
if ($type === ...)усередині «спільного» коду; коментарі «для X не викликати»; - дублювання в тестах часто корисне: тест, який читається сам по собі без переходів у допоміжні функції, - кращий тест.
WET («Write Everything Twice») і AHA («Avoid Hasty Abstractions») - жартівливі назви того самого принципу: не поспішати з абстракціями, доки не стане зрозуміло, яка вона має бути.
Докладніше в документації: Сенді Метц: The Wrong Abstraction
Зв'язність (coupling) - наскільки модулі залежать один від одного. Чим вона сильніша, тим більше зміна в одному модулі тягне зміни в інших. До неї прагнуть низької.
Зчеплення (cohesion) - наскільки елементи всередині модуля пов'язані спільною метою. До нього прагнуть високого: усе в класі працює на одну задачу.
Девіз: low coupling, high cohesion.
Ознаки сильної зв'язності:
- клас знає внутрішню будову іншого:
$order->customer->address->city->region->name(порушення «закону Деметри»); - спільний змінюваний глобальний стан;
- зміна формату даних в одному модулі ламає кілька інших;
- неможливо протестувати клас, не піднявши половину застосунку;
- модулі імпортують один одного по колу.
Ознаки слабкого зчеплення:
- клас
Utils/Helperз не пов'язаними функціями; - методи класу використовують різні, непересічні набори полів;
- щоб зробити одну зміну, доводиться правити п'ять різних місць - логіку однієї задачі розмазано.
Як зменшують зв'язність:
- залежати від інтерфейсів, а не конкретних класів;
- спілкуватися через події, коли модулю-джерелу не потрібен результат;
- передавати дані (DTO), а не цілі об'єкти з усіма зв'язками;
- приховувати внутрішню будову:
$order->shippingRegion()замість ланцюжка.
Баланс: нульової зв'язності не буває - модулі мусять взаємодіяти. Мета - щоб залежності були явними, спрямованими в один бік (від деталей до бізнес-логіки) і стабільними. Надмірне розщеплення (абстракції й події скрізь) теж має ціну: логіку однієї дії важко простежити.
Проблема: однаковий switch за типом повторюється в багатьох місцях. Новий тип - зміни в кожному з них, і одне точно забудуть.
function fee(Payment $p): int
{
return match ($p->method) {
'card' => (int) ($p->amount * 0.025),
'bank_transfer' => 500,
'cash' => 0,
};
}
// і ще схожі match для label(), isRefundable(), icon()...
Варіант 1 - enum з методами (PHP 8.1+). Добре, коли поведінка невелика й залежить лише від типу:
enum PaymentMethod: string
{
case Card = 'card';
case BankTransfer = 'bank_transfer';
case Cash = 'cash';
public function fee(int $amount): int
{
return match ($this) {
self::Card => (int) ($amount * 0.025),
self::BankTransfer => 500,
self::Cash => 0,
};
}
}
Знання про тип зібране в одному місці, а match без default змусить обробити новий випадок - інакше UnhandledMatchError, а статичний аналізатор попередить ще раніше.
Варіант 2 - поліморфізм (класи за спільним інтерфейсом). Коли в кожного типу своя складна логіка чи залежності:
interface PaymentProcessor
{
public function fee(int $amount): int;
public function refund(Payment $payment): void;
}
final class CardProcessor implements PaymentProcessor { /* ... */ }
Вибір реалізації - один раз, у фабриці чи контейнері. Далі код викликає методи без розгалужень.
Коли switch нормальний: він трапляється в одному місці (наприклад, у фабриці, що створює потрібний об'єкт), варіантів мало і їхній список стабільний. Поліморфізм заради одного if - зайве ускладнення.
Докладніше в документації: Replace Conditional with Polymorphism
Довгий список параметрів - code smell: виклик важко читати, легко переплутати порядок аргументів однакового типу, а кожна зміна сигнатури зачіпає всі місця виклику.
$this->createInvoice($customerId, $amount, 'UAH', $dueDate, true, false, null, 'uk');
// що означають true, false і null?
Варіанти лікування:
1. Об'єкт параметрів (Introduce Parameter Object) - параметри, що завжди йдуть разом, об'єднуються в клас:
final readonly class InvoiceData
{
public function __construct(
public int $customerId,
public Money $amount,
public DateTimeImmutable $dueDate,
public bool $sendByEmail = true,
public bool $isDraft = false,
public ?string $note = null,
public string $locale = 'uk',
) {}
}
$this->createInvoice(new InvoiceData(
customerId: $customer->id,
amount: Money::uah(125050),
dueDate: now()->addDays(14)->toImmutable(),
));
Іменовані аргументи PHP 8 роблять створення читабельним, значення за замовчуванням прибирають шум, а readonly гарантує незмінність. Часто в такий об'єкт перетікає й логіка (валідація, обчислення), - він стає об'єктом-значенням, а не просто контейнером.
2. Іменовані аргументи без нового класу - для рідкісних викликів з кількома необов'язковими параметрами:
$this->createInvoice($customerId, $amount, dueDate: $date, isDraft: true);
3. Передавати весь об'єкт замість його частин (Preserve Whole Object): calculateShipping($order) замість calculateShipping($order->weight, $order->country, $order->city, $order->express).
4. Прибрати булеві прапорці (Remove Flag Argument): createInvoice(..., isDraft: true) часто означає два різні методи - createDraftInvoice() і issueInvoice().
5. Перенести параметри в конструктор: залежності (сервіси, конфігурація), що передаються в кожен виклик, - у конструктор класу через впровадження залежностей.
Ознака, що проблема глибша: метод приймає багато параметрів, бо робить забагато. Тоді спершу розділити метод (Extract Method/Class), а вже потім дивитися на параметри.
У Laravel-проєктах такі об'єкти часто будують з Form Request ($request->toDto()) чи використовують spatie/laravel-data, що поєднує DTO з валідацією й перетворенням з/у масив.
Докладніше в документації: Каталог рефакторингів: Introduce Parameter Object
Одержимість примітивами (Primitive Obsession) - доменні поняття представлені «голими» рядками, числами й масивами: email - string, гроші - float, телефон - string, діапазон дат - два окремі DateTime.
function transfer(string $fromIban, string $toIban, float $amount, string $currency): void
Проблеми:
- валідація розкидана - кожне місце, що приймає IBAN, мусить перевіряти формат (і одне колись забуде);
- легко переплутати аргументи однакового типу (
$toIban, $fromIban) - компілятор не допоможе; - неявні правила (гроші не можна додавати в різних валютах, сума не від'ємна) повторюються чи пропускаються;
- поведінка розповзається по допоміжних функціях (
formatPhone(),normalizeEmail()).
Ліки - об'єкт-значення (Value Object):
final readonly class Money
{
private function __construct(public int $cents, public Currency $currency) {}
public static function of(int $cents, Currency $currency): self
{
if ($cents < 0) {
throw new InvalidArgumentException('Сума не може бути від\'ємною');
}
return new self($cents, $currency);
}
public function add(self $other): self
{
if ($other->currency !== $this->currency) {
throw new CurrencyMismatch();
}
return new self($this->cents + $other->cents, $this->currency);
}
public function equals(self $other): bool
{
return $this->cents === $other->cents && $this->currency === $other->currency;
}
}
function transfer(Iban $from, Iban $to, Money $amount): void
Властивості об'єкта-значення:
- валідний за побудовою: створити некоректний
Ibanнеможливо - перевірка в одному місці; - незмінний (
readonly): операції повертають новий об'єкт; - рівність за значенням, а не за посиланням;
- поведінка поруч з даними:
$money->add(),$email->domain(),$phone->formatted().
У Laravel: власні касти (CastsAttributes) перетворюють колонки бази на об'єкти-значення й назад:
protected function casts(): array
{
return ['price' => MoneyCast::class, 'email' => EmailCast::class];
}
Енуми PHP - теж ліки від одержимості примітивами для фіксованих наборів значень (статуси, ролі, валюти).
Де зупинитися: не кожен рядок потребує класу. Об'єкт-значення окупається, коли з поняттям пов'язані правила (валідація, операції, форматування) і воно передається між шарами застосунку.
Докладніше в документації: Refactoring.Guru: Primitive Obsession
Повне переписування з нуля («великий вибух») - один з найризикованіших проєктів: старою системою треба користуватися, поки пишеться нова; нова довго не дає жодної цінності; функції старої системи, про які всі забули, виявляються в останній момент; а дата перемикання постійно зсувається.
Strangler Fig (фікус-«душитель», що поступово обвиває й замінює дерево) - поступова заміна: нова система росте навколо старої, забираючи функціональність частинами, доки стара не стане непотрібною.
Як це виглядає:
- фасад/маршрутизатор перед системами - проксі, балансувальник, маршрути застосунку - вирішує, яка система обробляє запит;
- обрати шматок - бажано цінний і відносно відокремлений (наприклад, каталог чи звіти);
- реалізувати його в новій системі й перенаправити відповідні запити;
- повторювати, доки старої системи не лишиться;
- вимкнути стару систему.
┌──────────────► нова система (каталог, кошик)
запити ──► маршрутизатор
└──────────────► стара система (решта)
Варіант у межах одного застосунку (наприклад, міграція з самописного PHP на Laravel): Laravel приймає всі запити, нові маршрути обробляє сам, а для невідомих - передає старому коду (fallback-маршрут, що підключає старий фронт-контролер). Модуль за модулем код переїжджає в Laravel.
Що важливо:
- спільні дані - найскладніше. Варіанти: обидві системи працюють з однією базою (простіше, але зв'язує їх), синхронізація подіями, поступова міграція таблиць з шаром сумісності;
- кожен крок дає цінність і може йти в продакшен - немає «року без релізів»;
- можливість відкату: якщо нова частина має проблеми, маршрутизатор повертає трафік на стару;
- порівняння результатів: для критичних частин - запуск обох реалізацій паралельно й порівняння відповідей (shadow traffic) перед перемиканням;
- доводити до кінця: найчастіша проблема - «тимчасово» дві системи на роки. Потрібен план вимкнення старої.
Пов'язані прийоми: Branch by Abstraction - та сама ідея всередині коду (абстракція перед старою реалізацією, нова реалізація за прапорцем), feature flags для поступового перемикання.
Докладніше в документації: Мартін Фаулер: Strangler Fig Application
Технічний борг - метафора Ворда Каннінгема: швидке, неідеальне рішення - як позика. Ви отримуєте швидкість зараз, але платите відсотки: кожна наступна зміна в цьому коді коштує дорожче. Якщо борг не гасити, відсотки з'їдають усю швидкість команди.
Борг буває різним (квадрант Мартіна Фаулера):
- Свідомий і обачний: «випускаємо зараз, бо дедлайн, і знаємо, що треба буде переробити». Нормальне бізнес-рішення.
- Свідомий і необачний: «на дизайн немає часу» - постійно.
- Несвідомий і обачний: «тепер ми розуміємо, як це треба було зробити» - природний наслідок навчання.
- Несвідомий і необачний: команда просто не знає, як краще.
Як керувати:
- Робити видимим. Задачі в трекері з описом наслідків, а не «відрефакторити X»: «додавання нового способу оплати займає 3 дні замість пів дня через Y».
- Говорити мовою бізнесу. «Рефакторинг» не продається; «зменшимо кількість інцидентів у платежах» і «пришвидшимо релізи нових інтеграцій» - продаються.
- Гасити постійно, а не «колись». Частина ємності кожного спринту, правило бойскаута при роботі в коді, а не окремий «спринт рефакторингу» раз на рік.
- Пріоритезувати за відсотками. Найдорожчий борг - у коді, який часто змінюється. Заплутаний модуль, якого ніхто не чіпав три роки, може почекати. Аналіз «hotspots» (частота змін × складність файлів з історії Git) показує, де борг коштує найбільше.
- Не накопичувати нового непомітно: код-рев'ю, статичний аналіз у CI, тести як умова злиття.
Найнебезпечніше - коли борг не усвідомлюють: команда сповільнюється, оцінки ростуть, баги множаться, а причину шукають у людях, а не в коді.
Bounded context (обмежений контекст) - поняття з DDD: межа, всередині якої терміни й модель мають одне чітке значення. У великій системі одне слово означає різне в різних частинах.
«Товар» у каталозі - опис, фото, характеристики. На складі - залишки, комірка, вага. У бухгалтерії - собівартість і податкова група. Спроба зробити одну модель Product для всіх трьох дає клас на сотні полів, який змінюють усі команди й ніхто не розуміє повністю.
Ідея: кожен контекст має свою модель того, що йому потрібно. Контексти пов'язані через ідентифікатори й явні контракти, а не через спільні таблиці й класи.
Як ділити моноліт на модулі (модульний моноліт):
- Знайти межі за мовою й бізнес-процесами: де змінюється значення термінів, які частини змінюються разом, які команди відповідають за що.
- Модуль = каталог з публічним API:
app/Billing,app/Catalog,app/Shipping. Інші модулі звертаються лише до публічного інтерфейсу (сервіси, події), а не до внутрішніх моделей і таблиць. - Зв'язок між модулями - через події (
OrderPlaced→ Billing створює рахунок) або явні виклики фасаду модуля. - Свої таблиці для модуля. Запити
JOINчерез межу модуля - сигнал, що межа неправильна або потрібна копія даних. - Автоматична перевірка меж: архітектурні тести (Pest
arch(), Deptrac) забороняютьCatalogімпортувати внутрішні класиBilling.
Чому модульний моноліт, а не одразу мікросервіси: межі спершу майже завжди визначають неточно. Перенести код між модулями - рефакторинг; між мікросервісами - міграція даних, нові API й розподілені транзакції. Добре відокремлений модуль легко винести в сервіс пізніше, коли для цього з'явиться справжня причина: незалежне масштабування, окрема команда, інший цикл релізів.
Великі зміни (заміна ORM-шару, платіжного провайдера, пошукового рушія, переписування ключового модуля) в окремій гілці Git на тижні - класична пастка: гілка відстає від main, злиття стає болісним, а результат не перевірений у продакшені до самого кінця.
Branch by Abstraction - «гілкування» всередині коду замість гілки в Git:
- ввести абстракцію перед частиною, що змінюється, і перевести на неї всіх клієнтів - поки з єдиною реалізацією, старою:
interface SearchEngine
{
public function search(SearchQuery $query): SearchResults;
}
final class DatabaseSearch implements SearchEngine { /* наявний код */ }
- поступово писати нову реалізацію поруч - у
main, маленькими комітами, з тестами:
final class MeilisearchSearch implements SearchEngine { /* нова */ }
- перемикати клієнтів на нову реалізацію - через конфігурацію чи feature flag, частинами:
$this->app->bind(SearchEngine::class, fn () => Feature::active('new-search')
? app(MeilisearchSearch::class)
: app(DatabaseSearch::class));
- видалити стару реалізацію, а за потреби - і саму абстракцію, якщо вона більше не потрібна.
Переваги:
- код завжди в робочому стані і постійно інтегрується - немає великого злиття;
- зміни потрапляють у продакшен поступово і можуть бути вимкнені миттєво;
- паралельна робота: інша команда продовжує розробку, не чекаючи на завершення рефакторингу;
- перевірка на реальних даних: нову реалізацію можна ввімкнути для частини користувачів чи запускати «в тіні» й порівнювати результати.
Інструменти:
- 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. Варто стежити, щоб кількість записів лише зменшувалася.
У будь-якому великому проєкті поганого коду більше, ніж часу на його виправлення. Рефакторити «все погане» неможливо - і не потрібно: складний модуль, який ніхто не змінює роками, нікому не заважає.
Аналіз гарячих точок (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 - для оцінки складності.