Рефакторинг, зв'язність і архітектура
20 питань · ~20 хв · Версія v3.0
Увійдіть, щоб продовжити
Запахи коду й безпечний рефакторинг, зв'язність і зачеплення, тестованість, DDD і поступова заміна легасі - питання від middle до lead.
- За спробу
- 20
- У пулі
- 54
- Проходжень
- 0
- Середній бал
- -
- Пройшли на 70%+
- -
Питання для підготовки
34 питання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.
Рефакторинг - зміна внутрішньої структури коду без зміни його поведінки. «Виділити метод» (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
SRP - перша літера SOLID: у класу має бути одна причина для змін. Роберт Мартін формулює це так: модуль має відповідати перед одним «актором» - однією групою людей, чиї вимоги можуть його змінити.
Порушення:
class InvoiceService
{
public function create(Order $order): Invoice { /* розрахунок сум, податків */ }
public function renderPdf(Invoice $invoice): string { /* верстка */ }
public function send(Invoice $invoice): void { /* SMTP, шаблон листа */ }
}
У класу три причини змінюватися: бухгалтерія змінює правила податків, дизайнер - вигляд PDF, маркетинг - текст листа. Зміна одного може зламати інше, а тест розрахунку змушує налаштовувати пошту.
Після розділення:
class InvoiceCalculator { public function create(Order $order): Invoice {} }
class InvoicePdf { public function render(Invoice $invoice): string {} }
class InvoiceMailer { public function send(Invoice $invoice): void {} }
Як розпізнати порушення:
- назва класу з «And», «Manager», «Helper», «Util»;
- клас важко назвати одним іменником;
- методи класу використовують різні, непересічні групи властивостей;
- зміна в одній функції регулярно ламає тести іншої.
Пастка - надмірне дроблення. «Одна відповідальність» не означає «один метод». Десять класів по три рядки, які завжди змінюються разом, - теж погано: логіку однієї зміни доводиться збирати по багатьох файлах. Розділяють те, що змінюється з різних причин і в різний час.
Рефакторинг - зміна структури коду без зміни поведінки. Без тестів неможливо переконатися, що поведінка справді не змінилася. Тому перший крок - створити страховку.
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»), старий код видаляється, коли на нього вже ніхто не посилається.
Зв'язність (coupling) - наскільки модулі залежать один від одного. Чим вона сильніша, тим більше зміна в одному модулі тягне зміни в інших. До неї прагнуть низької.
Зчеплення (cohesion) - наскільки елементи всередині модуля пов'язані спільною метою. До нього прагнуть високого: усе в класі працює на одну задачу.
Девіз: low coupling, high cohesion.
Ознаки сильної зв'язності:
- клас знає внутрішню будову іншого:
$order->customer->address->city->region->name(порушення «закону Деметри»); - спільний змінюваний глобальний стан;
- зміна формату даних в одному модулі ламає кілька інших;
- неможливо протестувати клас, не піднявши половину застосунку;
- модулі імпортують один одного по колу.
Ознаки слабкого зчеплення:
- клас
Utils/Helperз не пов'язаними функціями; - методи класу використовують різні, непересічні набори полів;
- щоб зробити одну зміну, доводиться правити п'ять різних місць - логіку однієї задачі розмазано.
Як зменшують зв'язність:
- залежати від інтерфейсів, а не конкретних класів;
- спілкуватися через події, коли модулю-джерелу не потрібен результат;
- передавати дані (DTO), а не цілі об'єкти з усіма зв'язками;
- приховувати внутрішню будову:
$order->shippingRegion()замість ланцюжка.
Баланс: нульової зв'язності не буває - модулі мусять взаємодіяти. Мета - щоб залежності були явними, спрямованими в один бік (від деталей до бізнес-логіки) і стабільними. Надмірне розщеплення (абстракції й події скрізь) теж має ціну: логіку однієї дії важко простежити.
Прочитати - ще не значить знати
20 питань, по одному на екран, ~20 хв. Після завершення - розбір кожної помилки з посиланням на питання.