Dependency Injection в C++ на практике: примеры и шаблоны

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

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

Начать курс

Введение: почему Dependency Injection важен в C++?

В современном C++ разработке поддержка слабой связанности (loose coupling) между классами — залог гибкости и тестируемости кода. Dependency Injection (DI) или внедрение зависимостей — это паттерн, при котором объект получает свои зависимости извне, а не создает их сам. Это напрямую реализует принцип инверсии зависимостей (Dependency Inversion Principle) из SOLID.

Без DI классы часто жёстко привязаны к конкретным реализациям:

// Плохо: класс сам создает зависимость
class ReportGenerator {
    Database db_; // Жёсткая связь с конкретной БД
public:
    ReportGenerator() : db_("localhost") {}
    void generate() { db_.query("SELECT ..."); }
};

С DI мы получаем гибкость, возможность подменять реализации (например, для юнит-тестов) и упрощаем поддержку кода. В этой статье мы разберём три практических способа реализации DI в C++: через конструктор, через интерфейсы и с помощью простого контейнера.

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

Самый распространённый и рекомендованный способ. Зависимости передаются в конструктор класса в качестве параметров. В C++ для этого удобно использовать умные указатели (std::unique_ptr, std::shared_ptr) или ссылки.

Пример: логгер и сервис

#include <iostream>
#include <memory>
#include <string>

// 1. Абстракция (интерфейс)
class ILogger {
public:
    virtual ~ILogger() = default;
    virtual void log(const std::string& msg) = 0;
};

// 2. Конкретная реализация
class ConsoleLogger : public ILogger {
public:
    void log(const std::string& msg) override {
        std::cout << "[LOG]: " << msg << std::endl;
    }
};

// 3. Класс, который зависит от ILogger
class UserService {
    std::shared_ptr<ILogger> logger_; // Зависимость
public:
    // Внедрение через конструктор
    explicit UserService(std::shared_ptr<ILogger> logger)
        : logger_(std::move(logger)) {}

    void createUser(const std::string& name) {
        // Бизнес-логика...
        logger_->log("User created: " + name);
    }
};

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

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

  • Зависимость явно видна в сигнатуре конструктора.
  • Объект всегда находится в валидном состоянии (после создания).
  • Легко подменить реализацию для тестов (например, MockLogger).

Недостаток: при большом количестве зависимостей конструктор может стать громоздким. В таких случаях помогают контейнеры (см. раздел 3).

2. Внедрение через интерфейсы и полиморфизм

Это развитие предыдущего подхода. Мы определяем чисто виртуальные интерфейсы для всех зависимостей. Клиентский код работает только с абстракциями, а конкретные реализации подставляются на этапе сборки объекта (composition root).

Пример: репозиторий и бизнес-логика

#include <vector>
#include <memory>

// Интерфейс репозитория
class IUserRepository {
public:
    virtual ~IUserRepository() = default;
    virtual void add(const std::string& user) = 0;
    virtual std::vector<std::string> getAll() const = 0;
};

// Реализация для базы данных
class DatabaseUserRepo : public IUserRepository {
public:
    void add(const std::string& user) override {
        // SQL запрос
    }
    std::vector<std::string> getAll() const override {
        return {"Alice", "Bob"};
    }
};

// Бизнес-логика, не знающая о БД
class UserManager {
    std::unique_ptr<IUserRepository> repo_;
public:
    explicit UserManager(std::unique_ptr<IUserRepository> repo)
        : repo_(std::move(repo)) {}

    void registerUser(const std::string& name) {
        repo_->add(name);
    }
};

// Использование с DI
int main() {
    auto repo = std::make_unique<DatabaseUserRepo>();
    UserManager manager(std::move(repo));
    manager.registerUser("Charlie");
    return 0;
}

Такой подход идеален для юнит-тестирования: вы можете передать MockUserRepository, который не обращается к реальной БД.

Важно: владение и время жизни

  • Используйте std::unique_ptr, если владение единственное.
  • Используйте std::shared_ptr, если зависимость разделяется между несколькими объектами.
  • Избегайте сырых указателей (ILogger*) для долгоживущих зависимостей — это нарушает инкапсуляцию владения.

3. Простой контейнер зависимостей (DI Container)

Когда проект разрастается, ручное создание цепочек зависимостей становится утомительным. На помощь приходит DI-контейнер — фабрика, которая автоматически создаёт объекты с их зависимостями. В C++ нет встроенного контейнера, как в C# или Java, но его легко написать самому или использовать библиотеки (например, Boost.DI).

Пример самодельного контейнера

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

// Простейший DI контейнер
class DIContainer {
    using Factory = std::function<std::shared_ptr<void>()>;
    std::unordered_map<std::type_index, Factory> factories_;
public:
    template <typename T>
    void registerFactory(std::function<std::shared_ptr<T>()> factory) {
        factories_[std::type_index(typeid(T))] = [factory]() {
            return std::static_pointer_cast<void>(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                

Блоги

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