Зависьте от абстракций
Принцип инверсии зависимостей говорит: код высокого уровня — ваша бизнес-логика — не должен зависеть от низкоуровневых деталей вроде конкретной базы данных или формата файла. Оба должны зависеть от абстракции: в PHP это интерфейс.
«Инверсия» — это направление стрелки. Вместо того чтобы сервис указывал на одно конкретное хранилище, сервис указывает на интерфейс — и каждое хранилище указывает на тот же интерфейс с другой стороны.
Приварено к деталям
Этот сервис пользователей создаёт и использует конкретный DiskStore напрямую. Он работает — но сервис приварен к одному хранилищу. Другое хранилище, более быстрое хранилище, поддельное хранилище для тестов: каждое из них означает правку самого сервиса.
Сервис, приваренный к своему хранилищу
UserService сам создаёт внутри себя DiskStore. Теперь высокоуровневая логика регистрации зависит от низкоуровневой детали — файла на диске.
Вывод: PHP-скрипт
<?php
class DiskStore {
public function save(string $user): void {
echo "записываем $user в файл users.txt на диске\n";
}
}
// UserService сам создаёт своё хранилище — приварен к деталям.
class UserService {
private DiskStore $store;
public function __construct() {
$this->store = new DiskStore();
}
public function register(string $user): void {
echo "регистрируем пользователя: $user\n";
$this->store->save($user);
}
}
$service = new UserService();
$service->register("ana");
Инвертируйте зависимость
Решение: сервис запрашивает интерфейс Storage в своём конструкторе и никогда не узнаёт, какую реализацию получил. И дисковое хранилище, и хранилище в памяти реализуют этот интерфейс.
Теперь стрелка указывает на абстракцию с обеих сторон — и замена хранилища становится правкой одной строки там, где сервис создаётся, а не внутри него.
Сервис принимает интерфейс
Та же логика регистрации, два разных хранилища. new UserService(new DiskStore()) или new UserService(new MemoryStore()) — сам код сервиса не меняется.
Вывод: PHP-скрипт
<?php
// Абстракция, от которой зависят обе стороны.
interface Storage {
public function save(string $user): void;
}
class DiskStore implements Storage {
public function save(string $user): void {
echo "записываем $user в файл users.txt на диске\n";
}
}
class MemoryStore implements Storage {
public function save(string $user): void {
echo "храним $user в памяти\n";
}
}
class UserService {
public function __construct(private Storage $store) {
}
public function register(string $user): void {
echo "регистрируем пользователя: $user\n";
$this->store->save($user);
}
}
$onDisk = new UserService(new DiskStore());
$onDisk->register("ana");
$inMemory = new UserService(new MemoryStore());
$inMemory->register("ben");
Выигрыш: тестирование без диска
Поскольку сервис зависит от абстракции, тест может передать ему хранилище в памяти и мгновенно проверить результат — никаких файлов, никакой уборки. Именно инверсия зависимостей делает код удобным для модульного тестирования.
Вывод: PHP-скрипт
<?php
interface Storage {
public function save(string $user): void;
}
// MemoryStore запоминает то, что было сохранено — идеально для проверок.
class MemoryStore implements Storage {
public array $users = [];
public function save(string $user): void {
$this->users[] = $user;
}
}
class UserService {
public function __construct(private Storage $store) {
}
public function register(string $user): void {
$this->store->save($user);
}
}
$store = new MemoryStore();
$service = new UserService($store);
$service->register("ana");
$service->register("ben");
if (count($store->users) === 2) {
echo "проверка пройдена: сохранено 2 пользователя\n";
}
echo "сохранённые пользователи: ", implode(", ", $store->users), "\n";