Урок 05

SOLID: DIP — инверсия зависимостей

DIP

Dependency Inversion Principle — два правила:

  1. Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций.
  2. Абстракции не должны зависеть от деталей. Детали зависят от абстракций.

Проще: завись от интерфейсов, не от классов.

Проблема

Класс зависит от конкретной реализации:

class UserService
{
    public function __construct(
        private MySQLUserRepository $repo,
    ) {}

    public function register(string $email): void
    {
        $user = new User($email);
        $this->repo->save($user);
    }
}

Что плохо:

  • Жёсткая связь с MySQL.
  • Нельзя подменить (в тестах — in-memory).
  • Изменение MySQLUserRepository требует правки UserService.
  • Модуль верхнего уровня зависит от конкретики.

Решение

Интерфейс:

interface UserRepository
{
    public function save(User $user): void;
}

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

    public function register(string $email): void
    {
        $user = new User($email);
        $this->repo->save($user);
    }
}

final class MySQLUserRepository implements UserRepository
{
    public function save(User $user): void
    {
        // SQL
    }
}

final class InMemoryUserRepository implements UserRepository
{
    private array $users = [];

    public function save(User $user): void
    {
        $this->users[] = $user;
    }
}

UserService зависит от интерфейса. Можно подменить в тестах.

Инверсия

Было:

UserService ──▶ MySQLUserRepository

Стало:

UserService ──▶ UserRepository ◀── MySQLUserRepository
                    ▲
                    │
              InMemoryUserRepository

Оба зависят от абстракции. Стрелка «инвертировалась».

Уровни

Модуль верхнего уровня — бизнес-логика, use cases. Модуль нижнего уровня — детали (БД, HTTP, файлы).

Бизнес-логика не должна зависеть от деталей.

Пример: уведомления

Плохо:

class OrderService
{
    public function complete(Order $order): void
    {
        // бизнес-логика
        $order->markAsCompleted();

        // деталь: отправка email через конкретную библиотеку
        $mailer = new \PHPMailer\PHPMailer();
        $mailer->addAddress($order->userEmail);
        $mailer->Subject = 'Заказ завершён';
        $mailer->Body = '...';
        $mailer->send();
    }
}

OrderService знает про PHPMailer. Смена библиотеки — правка сервиса.

Хорошо:

interface Notifier
{
    public function notify(string $to, string $subject, string $body): void;
}

final class OrderService
{
    public function __construct(
        private Notifier $notifier,
    ) {}

    public function complete(Order $order): void
    {
        $order->markAsCompleted();

        $this->notifier->notify(
            $order->userEmail,
            'Заказ завершён',
            '...',
        );
    }
}

OrderService не знает, как отправляется. Может быть email, SMS, push.

Инъекция зависимостей (DI)

Dependency Injection — передача зависимостей через конструктор/сеттер.

Через конструктор:

final class UserService
{
    public function __construct(
        private UserRepository $repo,
        private Logger $logger,
    ) {}
}

Через сеттер:

final class UserService
{
    private ?Logger $logger = null;

    public function setLogger(Logger $logger): void
    {
        $this->logger = $logger;
    }
}

Правило: конструктор для обязательных, сеттер для опциональных.

DI-контейнер

Автоматически создаёт объекты:

$container = new Container();

$container->bind(UserRepository::class, MySQLUserRepository::class);
$container->bind(Logger::class, FileLogger::class);

$userService = $container->make(UserService::class);
// автоматически создаст UserService с MySQLUserRepository и FileLogger

Laravel Service Container — встроенный.

Laravel пример

interface PaymentGateway
{
    public function charge(int $amount): bool;
}

final class StripeGateway implements PaymentGateway
{
    public function charge(int $amount): bool
    {
        // Stripe API
        return true;
    }
}

AppServiceProvider:

public function register(): void
{
    $this->app->bind(PaymentGateway::class, StripeGateway::class);
}

Контроллер:

final class OrderController
{
    public function __construct(
        private PaymentGateway $gateway,
    ) {}

    public function pay(Order $order): Response
    {
        $this->gateway->charge($order->total);
        return response()->noContent();
    }
}

Контейнер сам подставит StripeGateway.

Группы и теги

Несколько реализаций:

$this->app->tag([
    EmailNotifier::class,
    SmsNotifier::class,
], 'notifiers');

Получение:

final class NotifierChain
{
    public function __construct(
        #[Tag('notifiers')] iterable $notifiers,
    ) {}
}

Проблема — сервис локатор

⚠️ Антипаттерн:

class UserService
{
    public function register(): void
    {
        $repo = app(UserRepository::class);   // Service Locator
        $repo->save(...);
    }
}

Проблема: зависимость скрыта. Тесты сложнее.

Правило: constructor injection, не app() внутри.

DIP vs DI

  • DIP — принцип: завись от абстракций.
  • DI — техника: передавай зависимости.

DI — инструмент, DIP — цель.

Практика: репозиторий

<?php

declare(strict_types=1);

final readonly class User
{
    public function __construct(
        public int $id,
        public string $name,
        public string $email,
    ) {}
}

interface UserRepository
{
    public function find(int $id): ?User;
    public function save(User $user): void;
}

final class InMemoryUserRepository implements UserRepository
{
    private array $users = [];

    public function find(int $id): ?User
    {
        return $this->users[$id] ?? null;
    }

    public function save(User $user): void
    {
        $this->users[$user->id] = $user;
    }
}

interface Logger
{
    public function log(string $message): void;
}

final class NullLogger implements Logger
{
    public function log(string $message): void { }
}

final class RegisterUserUseCase
{
    public function __construct(
        private UserRepository $repo,
        private Logger $logger,
    ) {}

    public function execute(string $name, string $email): User
    {
        $user = new User(
            id: random_int(1, 1000000),
            name: $name,
            email: $email,
        );

        $this->repo->save($user);
        $this->logger->log("User registered: {$user->id}");

        return $user;
    }
}

// Прод
$useCase = new RegisterUserUseCase(
    new InMemoryUserRepository(),
    new NullLogger(),
);

$user = $useCase->execute('Иван', 'ivan@example.com');
echo $user->name;

В тестах — те же интерфейсы, другие реализации.

Тестирование

Мок:

$repo = new class implements UserRepository {
    public array $saved = [];

    public function find(int $id): ?User { return null; }

    public function save(User $user): void
    {
        $this->saved[] = $user;
    }
};

$useCase = new RegisterUserUseCase($repo, new NullLogger());
$useCase->execute('Иван', 'ivan@example.com');

assert(count($repo->saved) === 1);

Тест не нуждается в БД.

Итог

  • DIP — завись от абстракций, не от классов.
  • Верхние уровни не зависят от нижних.
  • DI — техника внедрения зависимостей.
  • Constructor injection — основной способ.
  • DI-контейнер — автоматическая сборка.
  • Service Locator — антипаттерн.
  • Тестируемость — главный плюс.
  • Laravel — App::bind, автоинъекция.

Дальше — порождающие паттерны: Factory, Builder.