SOLID — це не чекліст, а набір спостережень про те, де код починає опиратись змінам. На практиці найбільше працюють два з пʼяти. Єдина відповідальність: клас має одну причину для зміни. Не один метод, а один привід, з яким до нього приходять. Симптоми порушення видно без теорії: конструктор на десять залежностей, тести з величезним setup, клас, який правлять і бухгалтерія, і маркетинг. Інверсія залежностей: код залежить від абстракції там, де реалізація може змінитись або її треба підміняти в тестах, тобто на межах із базою, зовнішніми API, часом, файлами.
Решта принципів здебільшого наслідки. Відкритість до розширення означає точки розширення там, де варіанти справді додаються: способи оплати, формати експорту. Підстановка Лісков — це про контракти: підклас, який кидає виняток у методі батька або мовчки змінює його семантику, ламає весь код, написаний під батька. Розділення інтерфейсів — про те, щоб споживач не залежав від методів, які не використовує, і саме воно зазвичай виправляє порушення LSP.
Формальне дотримання всіх пʼяти без потреби дає інтерфейс на кожен клас з однією реалізацією, десятки файлів на один сценарій і код, у якому важко знайти, де щось відбувається. Для CRUD-адмінки це шкідливо. Принципи застосовують там, де є біль: клас, який страшно міняти, тест, який неможливо написати, зміна, яка тягне правки в десяти місцях.
// До: одна причина для зміни? Ні: платежі, лист, PDF, склад в одному класі
final class OrderService
{
public function checkout(Order $order): void
{
$charge = $this->stripe->charge($order->total(), $order->card()); // зовнішній API
$this->mailer->send(new ReceiptMail($order, $charge)); // формат листа
file_put_contents("/invoices/{$order->id}.pdf", $this->pdf($order)); // інфраструктура
$this->db->table('stock')->decrement('qty', $order->qty()); // складський облік
}
}
// Після: SRP + DIP там, де є межа з зовнішнім світом. Усередині домену конкретні класи.
interface PaymentGateway { public function charge(Money $amount, CardToken $card): Charge; }
final class Checkout
{
public function __construct(
private PaymentGateway $payments, // абстракція: буде FakeGateway у тестах
private EventDispatcher $events,
) {}
public function handle(Order $order): void
{
$charge = $this->payments->charge($order->total(), $order->card());
$order->markPaid($charge->id());
$this->events->dispatch(new OrderPaid($order->id)); // лист, PDF, склад — окремі слухачі
}
}
// LSP порушено: підклас звужує контракт батька
class ReadOnlyOrders extends Orders
{
public function save(Order $o): void { throw new LogicException('read only'); }
}
// Правильно: окремі інтерфейси OrdersReader і OrdersWriter (ISP), без наслідування
Не переказуйте абревіатуру. Наведіть приклад класу, який ви розділили, і що це дало в тестах. Скажіть, який принцип порушуєте свідомо і чому.