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