OOP: SOLID
Glossary overview

OOP: SOLID

SOLID – это не магическое заклинание, а набор из пяти принципов, которые помогают не превратить код в липкий спагетти-комбайн через год после старта проекта.

Это аббревиатура:

S – Single Responsibility Principle
O – Open/Closed Principle
L – Liskov Substitution Principle
I – Interface Segregation Principle
D – Dependency Inversion Principle

Single Responsibility.

Класс должен иметь одну причину для изменения. Не “делать одну функцию”, а иметь одну ответственность.

Плохой пример:

class UserManager
{
    public function createUser() {}
    public function sendEmail() {}
    public function logToFile() {}
}

Этот класс управляет пользователями, отправляет письма и пишет логи. Три причины для изменения. Три разные области.

Хороший вариант – разделить: UserService, EmailService, Logger. Каждый отвечает за своё.

Open/Closed.

Класс должен быть открыт для расширения, но закрыт для изменения.

Это означает: добавлять новое поведение через новые классы, а не переписывать старые.

Если ты добавляешь новый способ оплаты и лезешь переписывать старый код с кучей if-ов – ты нарушаешь принцип.

Если ты просто добавил новый класс, реализующий интерфейс PaymentMethod – ты соблюдаешь его.

Допустим, у тебя есть сервис оплаты:

class PaymentService
{
    public function pay(string $type, float $amount): void
    {
        if ($type === 'card') {
            echo "Оплата картой: $amount\n";
        } elseif ($type === 'paypal') {
            echo "Оплата через PayPal: $amount\n";
        }
    }
}

Работает? Да.

Но теперь бизнес говорит: добавляем крипту.

Что ты делаешь? Лезешь в этот же класс и дописываешь:

elseif ($type === 'crypto') {
    echo "Оплата криптой: $amount\n";
}

Каждый новый способ оплаты заставляет тебя менять существующий код.

Класс не закрыт для изменения. Ты постоянно его трогаешь. Растет лес if-ов. Растет риск сломать старое.

Теперь делаем правильно.

Шаг 1. Вводим абстракцию.

interface PaymentMethod
{
    public function pay(float $amount): void;
}

Шаг 2. Конкретные реализации.

class CardPayment implements PaymentMethod
{
    public function pay(float $amount): void
    {
        echo "Оплата картой: $amount\n";
    }
}

class PaypalPayment implements PaymentMethod
{
    public function pay(float $amount): void
    {
        echo "Оплата через PayPal: $amount\n";
    }
}

Шаг 3. Сервис работает с абстракцией.

class PaymentService
{
    private PaymentMethod $paymentMethod;

    public function __construct(PaymentMethod $paymentMethod)
    {
        $this->paymentMethod = $paymentMethod;
    }

    public function pay(float $amount): void
    {
        $this->paymentMethod->pay($amount);
    }
}

Теперь добавляем крипту.

Мы НЕ трогаем PaymentService вообще.

Просто создаем новый класс:

class CryptoPayment implements PaymentMethod
{
    public function pay(float $amount): void
    {
        echo "Оплата криптой: $amount\n";
    }
}

И используем:

$payment = new CryptoPayment();
$service = new PaymentService($payment);
$service->pay(100);

Вот это и есть Open/Closed в действии.

PaymentService:

  • открыт для расширения, потому что можно передать новую реализацию
  • закрыт для изменения, потому что его код менять не нужно

И вот тут начинается архитектурная чистота. Старый код стабилен. Новое поведение добавляется через новые классы. Риск минимальный.

Если говорить глубже – Open/Closed почти всегда достигается через:

  • абстракции
  • полиморфизм
  • композицию

Без них принцип не работает.

Liskov Substitution

Любой дочерний класс должен полностью заменять родительский без поломки логики.

Если наследник не ведет себя как родитель по контракту – это плохое наследование.

Сначала нормальный пример.

class Bird
{
    public function fly(): void
    {
        echo "Flying\n";
    }
}

Теперь:

class Sparrow extends Bird
{
}

Все ок. Везде, где ожидается Bird, можно передать Sparrow.

Теперь плохой пример.

class Penguin extends Bird
{
    public function fly(): void
    {
        throw new Exception("Penguins can't fly");
    }
}

Формально Penguin – это Bird.
Но по факту он ломает поведение.

Если где-то есть код:

function makeBirdFly(Bird $bird): void
{
    $bird->fly();
}

И ты передаешь Penguin – программа падает.

Вот это нарушение Liskov. Потомок не соответствует ожиданиям родителя.

Ключевая мысль: наследник не должен усиливать ограничения или менять контракт.

Если родитель говорит: “я умею летать”, потомок не может сказать: “я не умею”.

Значит модель неправильная.

Правильнее было бы сделать так:

abstract class Bird
{
}

А способность летать вынести в отдельный интерфейс:

interface Flyable
{
    public function fly(): void;
}

И тогда:

class Sparrow extends Bird implements Flyable
{
    public function fly(): void
    {
        echo "Flying\n";
    }
}

class Penguin extends Bird
{
}

Теперь логика честная. Мы не заставляем пингвина притворяться тем, кем он не является.

Interface Segregation Principle

Клиенты не должны зависеть от методов, которые они не используют.

Проще: лучше несколько маленьких интерфейсов, чем один жирный универсальный комбайн.

Сейчас покажу, где обычно ломаются.

Допустим, кто-то придумал такой интерфейс:

interface Worker
{
    public function work(): void;
    public function eat(): void;
    public function sleep(): void;
}

Звучит логично? Типа “рабочий организм”.

Теперь появляется человек:

class Human implements Worker
{
    public function work(): void {}
    public function eat(): void {}
    public function sleep(): void {}
}

Все ок.

А теперь появляется робот.

class Robot implements Worker
{
    public function work(): void {}

    public function eat(): void
    {
        // роботы не едят
    }

    public function sleep(): void
    {
        // роботы не спят
    }
}

Вот тут проблема. Robot вынужден реализовывать методы, которые ему не нужны.

Интерфейс слишком жирный. Он заставляет классы притворяться.

Теперь делаем правильно.

Разделяем интерфейсы:

interface Workable
{
    public function work(): void;
}

interface Eatable
{
    public function eat(): void;
}

interface Sleepable
{
    public function sleep(): void;
}

Теперь:

class Human implements Workable, Eatable, Sleepable
{
    public function work(): void {}
    public function eat(): void {}
    public function sleep(): void {}
}

class Robot implements Workable
{
    public function work(): void {}
}

Чисто. Никто не реализует лишнего.

Вот это и есть Interface Segregation.

Почему это важно?

Потому что большие интерфейсы:

  • создают лишние зависимости
  • увеличивают связанность
  • заставляют менять код в местах, где не нужно

Если ты добавишь новый метод в жирный интерфейс, ты обязан изменить ВСЕ классы, которые его реализуют. Даже если им этот метод не нужен.

Этот принцип особенно важен в больших системах, где один интерфейс может использоваться десятками классов. Один лишний метод – и ты запускаешь цепную реакцию правок.

Dependency Inversion.

Dependency Inversion Principle (DIP), или принцип инверсии зависимостей, — это принцип SOLID, требующий, чтобы высокоуровневые модули (логика приложения) не зависели от низкоуровневых (базы данных, API), а оба зависели от абстракций (интерфейсов). В PHP это достигается через передачу интерфейсов в классы, что обеспечивает гибкость, тестируемость и слабую связанность кода.

Сначала плохой пример.

class MySqlDatabase
{
    public function save(string $data): void
    {
        echo "Saving to MySQL: $data\n";
    }
}

class ReportService
{
    private MySqlDatabase $database;

    public function __construct()
    {
        $this->database = new MySqlDatabase();
    }

    public function generate(): void
    {
        $this->database->save("Report data");
    }
}

Что тут не так?

ReportService – это логика верхнего уровня.
Он жестко привязан к MySqlDatabase
.

Если завтра нужно сохранять в PostgreSQL или в файл – придется лезть в ReportService и менять код.

Он зависит от детали.

Теперь делаем правильно.

Шаг 1. Вводим абстракцию.

interface Storage
{
    public function save(string $data): void;
}

Шаг 2. Реализация.

class MySqlStorage implements Storage
{
    public function save(string $data): void
    {
        echo "Saving to MySQL: $data\n";
    }
}

Шаг 3. Сервис зависит от абстракции.

class ReportService
{
    private Storage $storage;

    public function __construct(Storage $storage)
    {
        $this->storage = $storage;
    }

    public function generate(): void
    {
        $this->storage->save("Report data");
    }
}

Шаг 4. Сборка.

$storage = new MySqlStorage();
$service = new ReportService($storage);
$service->generate();

Теперь ReportService не знает, что там внутри. MySQL, Redis, файл, API – ему все равно.

Он зависит от интерфейса Storage.

Вот это и есть инверсия зависимостей.

Раньше было так:
ReportService → MySqlDatabase

Теперь:
ReportService → Storage
MySqlStorage → Storage

Оба зависят от абстракции.

Почему это называется “инверсия”?

Потому что в наивной архитектуре высокоуровневый код зависит от низкоуровневого.
Здесь мы переворачиваем направление зависимости. Детали подстраиваются под контракт, а не наоборот.

И вот где начинается магия.

Этот принцип делает возможным:

  • легкое тестирование (можно подставить mock)
  • замену инфраструктуры без изменения бизнес-логики
  • расширяемость

Это фундамент dependency injection контейнеров, которые ты видишь в Laravel, Symfony и т.д.

И важно понимать одну вещь. Dependency Injection – это способ реализации принципа. А Dependency Inversion – это сам принцип.

Можно внедрять зависимости через конструктор, через сеттер, через контейнер – это детали. Суть в том, чтобы верхний уровень не знал о конкретных классах нижнего уровня.