SOLID: DIP — инверсия зависимостей
DIP
Dependency Inversion Principle — два правила:
- Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали зависят от абстракций.
Проще: завись от интерфейсов, не от классов.
Проблема
Класс зависит от конкретной реализации:
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.