Dependency Injection контейнер для C++: обзор и практические примеры
В современной разработке на C++ всё чаще применяются принципы Inversion of Control (IoC) и Dependency Injection (DI). Эти паттерны позволяют создавать слабосвязанный, тестируемый и легко расширяемый код. В отличие от языков с управляемой памятью (Java, C#), C++ не имеет встроенной рефлексии, поэтому реализация DI-контейнеров здесь требует метапрограммирования шаблонов и использования современных стандартов (C++11/14/17/20).
В этой статье мы рассмотрим три популярных подхода к внедрению зависимостей в C++: библиотеки Boost.DI, Google Fruit и Kangaru, а также напишем собственный простой контейнер для понимания механики.
Что такое Dependency Injection?
Dependency Injection — это техника, при которой объект получает свои зависимости извне, а не создаёт их сам. Основные виды DI:
- Constructor Injection — зависимости передаются через конструктор.
- Setter Injection — зависимости устанавливаются через сеттеры.
- Interface Injection — зависимости передаются через интерфейс.
Пример без DI (жёсткая связь):
class Logger {
public:
void log(const std::string& msg) { /* запись в файл */ }
};
class Service {
Logger logger; // Жёсткая зависимость
public:
void doWork() { logger.log("Work done"); }
};
Пример с DI (слабая связь):
class ILogger {
public:
virtual ~ILogger() = default;
virtual void log(const std::string& msg) = 0;
};
class ConsoleLogger : public ILogger {
public:
void log(const std::string& msg) override { std::cout << msg << std::endl; }
};
class Service {
std::shared_ptr<ILogger> logger;
public:
explicit Service(std::shared_ptr<ILogger> l) : logger(std::move(l)) {}
void doWork() { logger->log("Work done"); }
};
Обзор DI-контейнеров для C++
1. Boost.DI
Boost.DI — одна из самых популярных библиотек. Она использует шаблоны C++14/17 для автоматического разрешения зависимостей. Поддерживает полиморфизм, области видимости (singleton, unique) и модульную конфигурацию.
Пример использования:
#include <boost/di.hpp>
#include <iostream>
namespace di = boost::di;
class IWriter {
public:
virtual ~IWriter() = default;
virtual void write(const std::string&) = 0;
};
class ConsoleWriter : public IWriter {
public:
void write(const std::string& msg) override {
std::cout << msg << std::endl;
}
};
class App {
std::shared_ptr<IWriter> writer;
public:
explicit App(std::shared_ptr<IWriter> w) : writer(std::move(w)) {}
void run() { writer->write("Hello from Boost.DI!"); }
};
int main() {
auto injector = di::make_injector(
di::bind<IWriter>.to<ConsoleWriter>()
);
auto app = injector.create<std::unique_ptr<App>>();
app->run();
return 0;
}
Плюсы: мощная типизация, минимальный runtime overhead, поддержка C++14/17.
Минусы: сложные сообщения об ошибках компиляции, увеличение времени сборки.
2. Google Fruit
Google Fruit — контейнер от Google, работающий на C++11. Использует макросы для регистрации компонентов и поддерживает инъекцию через конструктор.
Пример:
#include <fruit/fruit.h>
#include <iostream>
class IWriter {
public:
virtual void write(const std::string&) = 0;
};
class ConsoleWriter : public IWriter {
public:
INJECT(ConsoleWriter()) = default;
void write(const std::string& msg) override {
std::cout << msg << std::endl;
}
};
class App {
IWriter* writer;
public:
INJECT(App(ASSISTED(IWriter*) writer)) : writer(writer) {}
void run() { writer->write("Hello from Fruit!"); }
};
fruit::Component<App> getAppComponent() {
return fruit::createComponent()
.bind<IWriter, ConsoleWriter>();
}
int main() {
fruit::Injector<App> injector(getAppComponent);
App& app = injector.get<App>();
app.run();
return 0;
}
Плюсы:
Также в библиотеке
Работа с XML в C++: библиотеки pugixml, TinyXML-2 и RapidXML
Работа с изображениями в Java: от чтения до обработки
HTTP-клиент для JavaScript (Node.js): топ библиотек
Конфигурация приложений PHP: библиотеки и инструменты для гибкой настройки