Skip to content

Опыт применения SOLID ​

Использую SOLID как набор практических правил для прикладного кода: сервисов, handler-ов, репозиториев, интеграций и domain-слоя.

SRP — Single Responsibility Principle (принцип единственной ответственности) ​

У класса должна быть одна причина для изменения.

Корректный подход:

php
final class Report {}
final class ReportRenderer { public function render(Report $report): string {} }
final class ReportSaver { public function save(string $contents): void {} }

Контрпример:

php
final class ReportService
{
    public function generateAndSend(array $data, string $email): void
    {
        $report = $this->buildReport($data);
        $html = $this->renderHtml($report);
        file_put_contents('/tmp/report.html', $html);
        mail($email, 'Report', $html);
    }
}

Такой класс одновременно отвечает за сбор данных, рендеринг, сохранение и отправку.

OCP — Open / Closed Principle (принцип открытости / закрытости) ​

Поведение лучше расширять новыми реализациями, а не переписывать один центральный switch.

Корректный подход:

php
interface Discount
{
    public function apply(int $amount): int;
}

final class NoDiscount implements Discount
{
    public function apply(int $amount): int
    {
        return $amount;
    }
}

final class Checkout
{
    public function __construct(private Discount $discount) {}
}

Контрпример:

php
final class CheckoutService
{
    public function applyDiscount(string $discountType, int $amount): int
    {
        if ($discountType === 'none') {
            return $amount;
        }

        if ($discountType === 'vip') {
            return (int) ($amount * 0.9);
        }

        if ($discountType === 'employee') {
            return (int) ($amount * 0.8);
        }

        throw new InvalidArgumentException('Unknown discount type.');
    }
}

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

LSP — Liskov Substitution Principle (принцип подстановки Барбары Лисков) ​

Классы-наследники не должны ломать поведение базового класса. Они не должны ужесточать входные параметры (предусловия), и ослаблять возвращаемые значения (постусловия).

  • Входные параметры метода подкласса – совпадают или более абстрактные;

  • возвращаемое значение – совпадает либо подтип возвращаемого значения базового класса;

  • Метод не должен выбрасывать исключения, которые не свойственны базовому методу;

  • Инварианты класса должны остаться без изменений;

  • Подкласс не должен изменять значения приватных полей базового класса.

  • Контрпример:

php
class Animal {}
class Dog extends Animal {}

class Service
{
    public function handle(Animal $animal): Animal
    {
        return $animal;
    }
}

Нарушение по входному параметру: подкласс ужесточает предусловие: вместо Animal требует только Dog.

php
class BadDogService extends Service
{
    public function handle(Dog $animal): Animal
    {
        return $animal;
    }
}

Нарушение по возвращаемому значению: подкласс ослабляет постусловие: базовый метод обещает вернуть Dog, а наследник возвращает более общий Animal.

php
class DogFactory
{
    public function make(): Dog
    {
        return new Dog();
    }
}

class BadAnimalFactory extends DogFactory
{
    public function make(): Animal
    {
        return new Animal();
    }
}

ISP — Interface Segregation Principle (принцип разделения интерфейсов) ​

Интерфейсы лучше делать узкими и прикладными, чтобы клиент зависел только от нужных ему операций.

Корректный подход:

php
interface UserReader
{
    public function find(int $id): ?array;
}

interface UserWriter
{
    public function save(array $user): void;
}

Контрпример:

php
interface UserManagerInterface
{
    public function find(int $id): ?array;
    public function save(array $user): void;
    public function delete(int $id): void;
    public function import(array $rows): void;
    public function export(): array;
    public function sendWelcomeEmail(int $id): void;
}

Клиенту, которому нужно только чтение, всё равно приходится зависеть от методов записи, импорта и уведомлений.

DIP — Dependency Inversion Principle (принцип инверсии зависимостей) ​

Классы верхних уровней не должны зависеть от классов нижних уровней. Оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Корректный подход:

php
interface UserRepository
{
    public function findByEmail(string $email): ?User;
}

final class RegisterUser
{
    public function __construct(private UserRepository $repo) {}
}

Контрпример:

php
final class RegisterUser
{
    public function handle(string $email): void
    {
        $repo = new EloquentUserRepository();

        if ($repo->findByEmail($email) !== null) {
            return;
        }

        DB::table('users')->insert(['email' => $email]);
    }
}

Такой use-case жёстко привязан к конкретной ORM и способу записи в базу.

Что это даёт на практике ​

  • изменения чаще локализуются в одном слое или одной реализации;
  • новые варианты поведения проще добавлять через отдельные классы;
  • тесты проще писать на уровне интерфейсов и use-case классов;
  • границы между Domain, Application и Infrastructure остаются чище.

DRY, KISS, YAGNI ​

Дополнительно ориентируюсь на DRY, KISS и YAGNI, но без догматизма: если дублирование или простая реализация дешевле лишней абстракции, не усложняю код заранее.

См. также ​

Сайт обновлен и проверен: