Dependency Injection в C++: полное руководство с примерами

Онлайн-тренажер Python для начинающих

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

Начать курс

Введение в Dependency Injection в C++

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

В этой статье мы разберем, что такое Dependency Injection в C++, какие проблемы он решает, рассмотрим различные способы его реализации на практике и приведем примеры кода, которые помогут вам внедрить этот подход в свои проекты.

Проблема прямой зависимости

Прежде чем говорить о решении, давайте посмотрим на типичную проблему. Представьте, что у нас есть класс UserService, который использует Logger для записи логов:

class Logger {
public:
    void log(const std::string& message) {
        std::cout << "[LOG]: " << message << std::endl;
    }
};

class UserService {
private:
    Logger logger; // Жесткая зависимость
public:
    void createUser(const std::string& name) {
        logger.log("Creating user: " + name);
        // ... логика создания пользователя
    }
};

Здесь UserService самостоятельно создает экземпляр Logger. Это создает несколько проблем:

  • Сложность тестирования: невозможно подменить логгер на mock-объект во время юнит-тестов.
  • Жесткая связанность: изменение конструктора Logger потребует изменения всех классов, которые его используют.
  • Нарушение SRP (Single Responsibility Principle): класс отвечает не только за свою логику, но и за создание зависимостей.

Что такое Dependency Injection?

Dependency Injection — это паттерн, при котором объект получает свои зависимости извне, а не создает их самостоятельно. Основная идея: "не спрашивай, а получи".

В C++ существует три основных способа внедрения зависимостей:

  • Constructor Injection — через конструктор.
  • Setter Injection — через setter-методы.
  • Interface Injection — через интерфейс.

1. Constructor Injection (Внедрение через конструктор)

Это самый распространенный и предпочтительный способ. Зависимости передаются в конструктор класса:

#include <memory>
#include <iostream>

class ILogger {
public:
    virtual ~ILogger() = default;
    virtual void log(const std::string& message) = 0;
};

class ConsoleLogger : public ILogger {
public:
    void log(const std::string& message) override {
        std::cout << "[CONSOLE]: " << message << std::endl;
    }
};

class FileLogger : public ILogger {
public:
    void log(const std::string& message) override {
        // Логика записи в файл
        std::cout << "[FILE]: " << message << std::endl;
    }
};

class UserService {
private:
    std::unique_ptr<ILogger> logger_; // Зависимость через интерфейс
public:
    // Constructor Injection
    explicit UserService(std::unique_ptr<ILogger> logger) 
        : logger_(std::move(logger)) {}

    void createUser(const std::string& name) {
        logger_->log("Creating user: " + name);
        // ... логика
    }
};

int main() {
    auto consoleLogger = std::make_unique<ConsoleLogger>();
    UserService service(std::move(consoleLogger));
    service.createUser("Alice");
    return 0;
}

Преимущества:

  • Зависимость явно видна в сигнатуре конструктора.
  • Объект всегда находится в валидном состоянии после создания.
  • Легко тестировать — можно подставить mock.

2. Setter Injection (Внедрение через сеттеры)

Используется, когда зависимость не является обязательной или может меняться в рантайме:

class UserServiceWithSetter {
private:
    std::shared_ptr<ILogger> logger_; // shared_ptr для гибкости
public:
    // Зависимость не обязательна
    void setLogger(std::shared_ptr<ILogger> logger) {
        logger_ = std::move(logger);
    }

    void createUser(const std::string& name) {
        if (logger_) {
            logger_->log("Creating user: " + name);
        }
        // ... логика
    }
};

Минусы: объект может быть в неполном состоянии, если забыть вызвать сеттер.

3. Interface Injection (Внедрение через интерфейс)

Это более редкий подход, при котором зависимость передается через отдельный метод, объявленный в интерфейсе. В C++ часто реализуется через шаблоны:

template <typename TLogger>
class UserServiceTemplate {
private:
    TLogger logger_;
public:
    explicit UserServiceTemplate(TLogger logger) : logger_(std::move(logger)) {}

    void createUser(const std::string& name) {
        logger_.log("Creating user: " + name);
    }
};

// Использование
UserServiceTemplate<ConsoleLogger> service(ConsoleLogger{});
service.createUser("Bob");

DI-контейнеры в C++

Для управления большим количеством зависимостей часто используются DI-контейнеры (или IoC-контейнеры). В C++ нет встроенного контейнера, но его можно реализовать самостоятельно или использовать сторонние библиотеки (например, Boost.DI).

Простой пример DI-контейнера:

#include <unordered_map>
#include <functional>
#include <memory>
#include <typeindex>

class DIContainer {
private:
    std::unordered_map<std::type_index, std::function<std::shared_ptr<void>()>> factories_;
public:
    template <typename T>
    void registerFactory(std::function<std::shared_ptr<T>()> factory) {
        factories_[std::type_index(typeid(T))] = factory;
    }

    template <typename T>
    std::shared_ptr<T> resolve() {
        auto it = factories_.find(std::type_index(typeid(T)));
        if (it != factories_.end()) {
            return std::static_pointer_cast<T>(it->second());
        }
        throw std::runtime_error("Dependency not registered");
    }
};

// Пример использования
int main() {
    DIContainer container;
    container.registerFactory<ILogger>([]() { 
        return std::make_shared<ConsoleLogger>(); 
    });

    auto logger = container.resolve<ILogger                

Блоги

Рекомендаций по книгам