Dependency Injection контейнер в C++: библиотека Fruit для внедрения зависимостей

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

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

Начать курс

Dependency Injection контейнер для C++: обзор библиотеки Fruit

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

Что такое Dependency Injection контейнер?

Dependency Injection контейнер — это инструмент, который берет на себя ответственность за создание и связывание объектов (зависимостей). Вместо того чтобы класс сам создавал свои зависимости через new или фабрики, контейнер «внедряет» их извне. Это реализует принцип Inversion of Control (IoC) и делает код слабосвязанным.

Почему Fruit?

Библиотека Fruit (https://github.com/google/fruit) разработана в Google и отличается от аналогов (например, Boost.DI) следующими особенностями:

  • Компиляционная проверка: все связи проверяются на этапе компиляции, что исключает ошибки времени выполнения, связанные с отсутствием зависимостей.
  • Минимальный оверхед: Fruit не использует RTTI и виртуальные вызовы, генерируя эффективный код.
  • Простота: требуется только C++14 и заголовочные файлы — никаких макросов или кодогенерации.
  • Поддержка компонентов: можно собирать граф зависимостей из переиспользуемых модулей.

Установка

Fruit распространяется как header-only библиотека (требуется только fruit.h). Для установки через vcpkg:

vcpkg install fruit

Или склонируйте репозиторий и подключите файл include/fruit/fruit.h.

Основные концепции

В Fruit используются три ключевых элемента:

  • Инжектор (Injector) — контейнер, который создает объекты.
  • Компонент (Component) — модуль, регистрирующий типы и их фабрики.
  • Аннотации — для различения разных реализаций одного интерфейса.

Пример использования

Рассмотрим классический пример: сервис зависит от репозитория. Создадим интерфейс и две реализации:

// interfaces.hpp
#include <fruit/fruit.h>
#include <string>

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

class IUserRepository {
public:
    virtual std::string getUser(int id) = 0;
    virtual ~IUserRepository() = default;
};

Реализации:

// implementations.cpp
#include "interfaces.hpp"
#include <iostream>

class ConsoleLogger : public ILogger {
public:
    INJECT(ConsoleLogger()) = default;  // Fruit-аннотация для конструктора
    void log(const std::string& msg) override {
        std::cout << "[LOG] " << msg << std::endl;
    }
};

class SqlRepository : public IUserRepository {
public:
    INJECT(SqlRepository()) = default;
    std::string getUser(int id) override {
        return "User_" + std::to_string(id);
    }
};

Сервис, использующий обе зависимости:

// user_service.cpp
#include "interfaces.hpp"

class UserService {
private:
    ILogger* logger;
    IUserRepository* repo;
public:
    INJECT(UserService(ILogger* logger, IUserRepository* repo))
        : logger(logger), repo(repo) {}
    
    void processUser(int id) {
        auto name = repo->getUser(id);
        logger->log("Processing user: " + name);
    }
};

Теперь соберем компонент и используем инжектор:

// main.cpp
#include <fruit/fruit.h>
#include "interfaces.hpp"
#include "implementations.cpp"
#include "user_service.cpp"

fruit::Component<UserService> getComponent() {
    return fruit::createComponent()
        .bind<ILogger, ConsoleLogger>()   // привязываем интерфейс к реализации
        .bind<IUserRepository, SqlRepository>()
        .bind<UserService, UserService>(); // регистрируем сам сервис
}

int main() {
    fruit::Injector<UserService> injector(getComponent);
    UserService* service = injector.get<UserService*>();
    service->processUser(42);
    return 0;
}

                
    
    

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