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

Рефакторинг, зв'язність і архітектура

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.

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

Рефакторинг - зміна внутрішньої структури коду без зміни його поведінки. «Виділити метод» (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);
  • блок потрібно тестувати окремо.

Як робити безпечно:

  1. переконатися, що є тести (або написати їх для поточної поведінки);
  2. виділити метод - автоматично в IDE (PhpStorm: Extract Method), щоб не помилитися з параметрами й поверненим значенням;
  3. дати назву за наміром («чи в наявності», «сума зі знижкою»), а не за реалізацією («цикл по товарах»);
  4. запустити тести.

Коли виділення не допомагає: якщо виділеному фрагменту потрібно передати шість параметрів і повернути три значення - це ознака, що код хоче стати окремим класом (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»;
  • клас важко назвати одним іменником;
  • методи класу використовують різні, непересічні групи властивостей;
  • зміна в одній функції регулярно ламає тести іншої.

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

Докладніше в документації: Single-responsibility principle

Рефакторинг - зміна структури коду без зміни поведінки. Без тестів неможливо переконатися, що поведінка справді не змінилася. Тому перший крок - створити страховку.

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() замість ланцюжка.

Баланс: нульової зв'язності не буває - модулі мусять взаємодіяти. Мета - щоб залежності були явними, спрямованими в один бік (від деталей до бізнес-логіки) і стабільними. Надмірне розщеплення (абстракції й події скрізь) теж має ціну: логіку однієї дії важко простежити.

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

Прочитати - ще не значить знати

20 питань, по одному на екран, ~20 хв. Після завершення - розбір кожної помилки з посиланням на питання.