Menu
Coddy logo textTech

Внедрение зависимостей

Часть раздела Объектно-ориентированное программирование путешествия по PHP на Coddy — урок 57 из 91.

В предыдущем уроке вы узнали, как композиция позволяет Car содержать объект Engine. Но создание зависимостей внутри класса сопряжено с проблемой — это делает код жестким и труднотестируемым. Внедрение зависимостей решает эту проблему, передавая зависимости извне, а не создавая их внутри.

Сравните эти два подхода:

<?php
// Без DI — зависимость создается внутри
class Car {
    private Engine $engine;
    
    public function __construct() {
        $this->engine = new Engine(); // Жесткая связь
    }
}

// С DI — зависимость передается извне
class Car {
    public function __construct(private Engine $engine) {}
}

$engine = new Engine();
$car = new Car($engine); // Внедряется извне

Второй подход — это внедрение зависимостей. Класс Car не создает свой собственный двигатель — он получает его извне. Это простое изменение дает мощные преимущества: вы можете передавать различные типы двигателей, заменять реализации для тестирования, а сам класс становится более гибким.

Для еще большей гибкости внедряйте интерфейсы вместо конкретных классов:

<?php
interface EngineInterface {
    public function start(): string;
}

class Car {
    public function __construct(private EngineInterface $engine) {}
    
    public function start(): string {
        return $this->engine->start();
    }
}

Теперь Car работает с любым классом, реализующим EngineInterface — бензиновым двигателем, электродвигателем или мок-объектом для тестирования. Такая развязка — причина, по которой внедрение зависимостей является основополагающим для написания поддерживаемых и тестируемых PHP-приложений.

challenge icon

Задание

Легко

Давайте создадим систему уведомлений, которая демонстрирует мощь внедрения зависимостей (dependency injection). Вместо жесткого кодирования способа отправки сообщений, вы создадите гибкую систему, в которой метод доставки можно легко заменить.

Вы организуете свой код в четырех файлах:

  • MessageSenderInterface.php — Определите интерфейс с именем MessageSenderInterface с единственным методом send(string $recipient, string $message): string. Этот интерфейс устанавливает контракт, которому должен следовать любой отправитель сообщений.
  • EmailSender.php — Создайте класс EmailSender, который реализует MessageSenderInterface. Подключите файл интерфейса. Метод send() должен возвращать "Email to $recipient: $message".
  • NotificationService.php — Создайте класс NotificationService, который получает свою зависимость через конструктор. Подключите файл интерфейса. Класс должен:
    • Принимать MessageSenderInterface в своем конструкторе, используя продвижение свойств в конструкторе (constructor promotion)
    • Иметь метод notify(string $recipient, string $message), который делегирует задачу внедренному отправителю и возвращает его результат
    Обратите внимание, как NotificationService зависит от интерфейса, а не от конкретного класса. Это означает, что вы можете внедрить любого отправителя, реализующего этот интерфейс, не меняя сам сервис.
  • main.php — Подключите файлы EmailSender и NotificationService. Вы получите два входных значения: получателя и сообщение. Создайте экземпляр EmailSender, внедрите его в NotificationService, затем вызовите notify() и выведите результат.

Этот паттерн делает ваш NotificationService гибким — он работает с любым отправителем, которого вы внедрите, будь то электронная почта, SMS или что-то, что вы еще не создали. Сервис не создает свои собственные зависимости; он получает их извне.

Шпаргалка

Внедрение зависимостей (Dependency Injection, DI) — это метод, при котором зависимости передаются в класс извне, а не создаются внутри него.

Без DI (жесткая связь):

<?php
class Car {
    private Engine $engine;
    
    public function __construct() {
        $this->engine = new Engine(); // Создается внутри
    }
}

С DI (слабая связь):

<?php
class Car {
    public function __construct(private Engine $engine) {}
}

$engine = new Engine();
$car = new Car($engine); // Внедряется извне

Для максимальной гибкости внедряйте интерфейсы вместо конкретных классов:

<?php
interface EngineInterface {
    public function start(): string;
}

class Car {
    public function __construct(private EngineInterface $engine) {}
    
    public function start(): string {
        return $this->engine->start();
    }
}

Это позволяет Car работать с любым классом, реализующим EngineInterface, что делает код более удобным в сопровождении и тестировании.

Попробуйте сами

<?php
require_once 'EmailSender.php';
require_once 'NotificationService.php';

// Чтение входных данных
$recipient = trim(fgets(STDIN));
$message = trim(fgets(STDIN));

// TODO: Создать экземпляр EmailSender
// TODO: Внедрить его в NotificationService
// TODO: Вызвать notify() и вывести результат

?>
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование