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

ООП, SOLID і патерни

20 питань · ~20 хв · Версія v3.0

Увійдіть, щоб продовжити

Принципи ООП і SOLID на прикладах PHP, патерни GoF у Laravel, запахи коду й рефакторинг, зв'язність, архітектура й тестованість.

За спробу
20
У пулі
100
Проходжень
0
Середній бал
-
Пройшли на 70%+
-

Питання для підготовки

100 питань

Strategy - кілька взаємозамінних алгоритмів за спільним інтерфейсом. Код, що їх використовує, не знає, який саме алгоритм працює, і не містить розгалужень за типом.

interface ShippingCalculator
{
    public function cost(Order $order): Money;
}

final class NovaPoshtaShipping implements ShippingCalculator { /* ... */ }
final class UkrposhtaShipping implements ShippingCalculator { /* ... */ }
final class PickupShipping implements ShippingCalculator { /* ... */ }

final class Checkout
{
    public function __construct(private ShippingCalculator $shipping) {}

    public function total(Order $order): Money
    {
        return $order->subtotal()->add($this->shipping->cost($order));
    }
}

Що це дає:

  • новий спосіб доставки - новий клас, без зміни Checkout (принцип відкритості/закритості);
  • кожну стратегію легко протестувати окремо;
  • немає switch ($order->shipping_method), що розростається з кожним новим варіантом.

У Laravel цей патерн скрізь - під назвою «драйвери»:

  • кеш: file, redis, database за контрактом Store;
  • файлові диски: локальний, S3, FTP за спільним API Storage;
  • черги, пошта, логи, сесії, хешування - той самий принцип.

Конкретна стратегія обирається конфігурацією (CACHE_STORE=redis), а код працює з контрактом.

Як обирати стратегію під час виконання: через контейнер (прив'язати інтерфейс до реалізації), фабрику чи мапу [метод => клас]. Якщо варіантів два й вони ніколи не множитимуться, простий if часто чесніший за патерн.

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

Принцип відкритості/закритості (Open/Closed Principle, «O» в SOLID): модуль має бути відкритим для розширення і закритим для змін. Нову поведінку додають новим кодом, а не переписуванням того, що вже працює й протестоване.

Порушення - кожна нова можливість змінює той самий код:

class ShippingCalculator
{
    public function cost(Order $order, string $carrier): int
    {
        return match ($carrier) {
            'nova_poshta' => 70 + $order->weight * 5,
            'ukrposhta' => 45 + $order->weight * 3,
            // новий перевізник - правка цього класу, ризик зламати наявних
        };
    }
}

Відповідно до OCP - розширення через новий клас:

interface ShippingMethod
{
    public function cost(Order $order): int;
}

final class NovaPoshta implements ShippingMethod { /* ... */ }
final class Ukrposhta implements ShippingMethod { /* ... */ }
final class Meest implements ShippingMethod { /* ... */ }   // новий - без змін в інших

class ShippingCalculator
{
    public function cost(Order $order, ShippingMethod $method): int
    {
        return $method->cost($order);
    }
}

Типові механізми розширення:

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

Як це виглядає в Laravel: нові драйвери кешу, черг, файлових систем (Storage::extend()), канали сповіщень, правила валідації, макроси - фреймворк закритий для змін, але відкритий для розширення.

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

Корисний тест: чи доводилося останні кілька разів змінювати той самий match/switch, додаючи варіант? Якщо так - це місце, де OCP окупиться.

Докладніше в документації: Принцип відкритості/закритості

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

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

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