Что лучше: передавать зависимости через параметры конструктора или создавать их внутри конструктора?

Ответ

Я строго придерживаюсь принципа внедрения зависимостей (Dependency Injection, DI) через конструктор. Это краеугольный камень тестируемого и поддерживаемого кода.

Почему передача через конструктор — лучшая практика:

  • Явность зависимостей: Класс честно декларирует, что ему нужно для работы. Нет скрытых вызовов new или глобальных состояний.
  • Тестируемость: Я могу легко подменить реальный репозиторий или сервис мок-объектом в юнит-тестах.
  • Гибкость: Конфигурация объекта выносится наружу, что позволяет использовать разные реализации (например, FileLogger и DatabaseLogger).

Пример из моего кода:

class OrderProcessor {
    private PaymentGateway $gateway;
    private OrderRepository $repository;
    private LoggerInterface $logger;

    // Все зависимости передаются явно
    public function __construct(
        PaymentGateway $gateway,
        OrderRepository $repository,
        LoggerInterface $logger
    ) {
        $this->gateway = $gateway;
        $this->repository = $repository;
        $this->logger = $logger;
    }

    public function process(Order $order): void {
        $this->logger->info('Processing order', ['id' => $order->id]);
        // ... логика с использованием $this->gateway и $this->repository
    }
}

Исключения, когда создание внутри допустимо:

  • Value Objects (например, new DateTimeImmutable() или new Uuid()).
  • Внутренние вспомогательные объекты, которые не влияют на поведение класса с точки зрения внешнего контракта и не требуют мокирования в тестах.

Для управления сложными графами зависимостей я использую DI-контейнеры (например, из Laravel или Symfony).

Ответ 18+ 🔞

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

А почему это так, блядь, важно:

  • Всё на виду, как в бане: Класс сразу ясно показывает, без чего он нихуя не работает. Никаких скрытых new где-то в углу и глобальных переменных, от которых потом волосы дыбом.
  • Тесты пишутся за пять минут: Хочешь потестить? Подсовываешь в конструктор мок вместо реальной базы или платежки — и всё, ебушки-воробушки. Никакой магии.
  • Гибкость овердохуища: Можешь в разных местах прокидывать разные реализации. Скажем, логгер в файл для прода и логгер в /dev/null для тестов. Красота.

Смотри, как у меня обычно выглядит:

class OrderProcessor {
    private PaymentGateway $gateway;
    private OrderRepository $repository;
    private LoggerInterface $logger;

    // Всё засовываешь сюда, и спишь спокойно
    public function __construct(
        PaymentGateway $gateway,
        OrderRepository $repository,
        LoggerInterface $logger
    ) {
        $this->gateway = $gateway;
        $this->repository = $repository;
        $this->logger = $logger;
    }

    public function process(Order $order): void {
        $this->logger->info('Processing order', ['id' => $order->id]);
        // ... тут уже работаешь с $this->gateway и $this->repository
    }
}

Ну а когда можно на это правило забить хуй?

  • Разные Value Objects: Типа new DateTimeImmutable() или new Uuid(). Это просто данные в красивой обёртке.
  • Внутренние подручные объекты: Которые сам класс создаёт для своих мелких делишек, они на общее поведение не влияют и мокировать их в тестах — только время терять.

А когда зависимостей становится, блядь, как грязи, и руками их собирать — терпения ноль ебать, тут на помощь приходят DI-контейнеры. Из Laravel или Symfony, например. Они эту всю хуйню сами аккуратно соберут, как конструктор.