У меня самые умные подписчики в IT 💙 — нашли ошибку за секунды. Ну а для меня
этот комментарий стал поводом разложить всё по полочкам.
DI — Dependency InjectionПодход, при котором код явно декларирует необходимые зависимости и получает их снаружи, а не создаёт, не хардкодит и не извлекает их сам.
function logToFile(string $message): void
{
// не DI
file_put_contents(__DIR__ . '/../app.log', $message, FILE_APPEND);
}
final class StreamLogger extends Psr\Log\AbstractLogger
{
public function __construct(
// DI
private string $stream,
) {}
// ...
}
Как видите, зависимостью может быть что угодно, не только интерфейс: строка, число, инстанс финального класса.
Здесь часто проводят параллель с принципом Голливуда («Don’t call us, we’ll call you»): не ищите зависимости, мы их для вас соберём и передадим.
Мне ещё нравится на пальцах объяснять так:
Привет, я A, моя компетенция — α. Чтобы выполнить α, мне потребуются: x, опционально y и кто-то, кто умеет β. Предоставите — всё сделаю. Нет — сорян.
Зависимости можно передавать через конструктор, параметр метода, путём клонирования with-методом (поддерживается в
Symfony и
Thesis DIC) или через сеттер (не рекомендую, так как мутабельно и можно получить неполный стейт или проблемы похуже в асинхронном сетапе).
DIC — Dependency Injection ContainerСугубо опциональная вещь! Контейнер призван облегчить сборку графа зависимостей в крупном проекте, но никто не мешает собирать приложение руками:
// код в стиле DI, но контейнера нет
$app = new App(
logger: new StreamLogger(__DIR__ . '/../app.log'),
);
Большинство современных библиотек написано в стиле DI, но не требует контейнера. В README как раз обычно и показывают, как всё собрать "на коленке".
DIP — Dependency Inversion PrincipleПоследний принцип из SOLI
D. Он призван снизить каплинг и повысить переиспользуемость кода за счёт опоры на абстракции.
Обратите внимание, что в оригинальной формулировке принципа не используется понятие «интерфейс»:
A. High level modules should not depend upon low level modules. Both should depend upon abstractions.
B. Abstractions should not depend upon details. Details should depend upon abstractions.
Robert C. Martin, «The Dependency Inversion Principle», C++ Report, 1996В C++, о котором идёт речь в статье, не было отдельной языковой конструкции
interface: Мартин выражал объектные интерфейсы чисто абстрактными классами. Но в той же статье он отдельно приводит
stdio.h как пример Dependency Inversion без всяких классов и виртуальных методов.
В наш
StreamLogger тоже можно передать не только путь к локальному файлу:
$stdOutLogger = new StreamLogger('php://stdout');
$compressedLogger = new StreamLogger('compress.zlib:///var/log/app.log.gz');
$remoteLogger = new StreamLogger('ftp://logger:secret@logs.example.com/app.log');
Внезапно для работы с разными потоками нам не потребовался
interface! Строковый URI выбирает конкретную реализацию, а
StreamLogger работает через общую абстракцию PHP-стримов.
Инверсия зависимостей здесь реализована не в нашем коде, а в PHP: контракт —
PHP Streams API, конкретные врапперы —
file,
php,
compress.zlib,
ftp и пользовательские протоколы.
StreamLogger пользуется этим контрактом, не зная, какая реализация скрывается за URI.
Получается, «абстракция» не тождественна интерфейсу или абстрактному классу. Даже финальный класс может абстрагировать вызывающий код от деталей реализации, пока сохраняет публичный контракт.
И вот тут-то мы и нащупали причину, по которой интерфейс может быть избыточным:
Если тип сам выражает требуемую клиенту стабильную абстракцию и не протекает низкоуровневыми деталями — на него можно опереться напрямую.⸻
🌿
Анонсировали доклады Данилы Щуцкого и Саши Макарова на Пыхнике!