Введение: почему 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