Dependency Injection in C++ in der Praxis: Beispiele und Vorlagen

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

Einleitung: Warum ist Dependency Injection in C++ wichtig?



In der modernen C++-Entwicklung ist die Unterstützung loser Kopplung (loose coupling) zwischen Klassen der Schlüssel zu Flexibilität und Testbarkeit des Codes. Dependency Injection (DI) oder Abhängigkeitsinjektion ist ein Muster, bei dem ein Objekt seine Abhängigkeiten von außen erhält, anstatt sie selbst zu erstellen. Dies setzt direkt das Prinzip der Abhängigkeitsumkehr (Dependency Inversion Principle) aus SOLID um.



Ohne DI sind Klassen oft fest an konkrete Implementierungen gebunden:



// Schlecht: Die Klasse erstellt die Abhängigkeit selbstclass ReportGenerator {    Database db_; // Feste Bindung an eine bestimmte DBpublic:    ReportGenerator() : db_("localhost") {}    void generate() { db_.query("SELECT ..."); }};


Mit DI erhalten wir Flexibilität, die Möglichkeit, Implementierungen auszutauschen (z. B. für Unit-Tests) und vereinfachen die Code-Wartung. In diesem Artikel werden wir drei praktische Methoden zur Implementierung von DI in C++ behandeln: über den Konstruktor, über Schnittstellen und mit einem einfachen Container.



1. Injektion über den Konstruktor (Constructor Injection)



Die gebräuchlichste und empfohlene Methode. Abhängigkeiten werden dem Konstruktor der Klasse als Parameter übergeben. In C++ eignen sich dafür Smart Pointer (std::unique_ptr, std::shared_ptr) oder Referenzen.



Beispiel: Logger und Service



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

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

// 2. Konkrete Implementierungclass ConsoleLogger : public ILogger {public: void log(const std::string& msg) override { std::cout << "[LOG]: " << msg << std::endl; }};

// 3. Klasse, die von ILogger abhängtclass UserService { std::shared_ptr<ILogger> logger_; // Abhängigkeitpublic: // Injektion über den Konstruktor explicit UserService(std::shared_ptr<ILogger> logger) : logger_(std::move(logger)) {}

void createUser(const std::string& name) { // Geschäftslogik... logger_->log("User created: " + name); }};

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


Vorteile:

  • Die Abhängigkeit ist in der Konstruktorsignatur deutlich sichtbar.
  • Das Objekt befindet sich immer in einem gültigen Zustand (nach der Erstellung).
  • Einfaches Austauschen der Implementierung für Tests (z. B. MockLogger).


Nachteil: Bei einer großen Anzahl von Abhängigkeiten kann der Konstruktor umfangreich werden. In solchen Fällen helfen Container (siehe Abschnitt 3).



2. Injektion über Schnittstellen und Polymorphismus



Dies ist eine Weiterentwicklung des vorherigen Ansatzes. Wir definieren rein virtuelle Schnittstellen für alle Abhängigkeiten. Der Client-Code arbeitet nur mit Abstraktionen, während die konkreten Implementierungen zur Objektzusammenstellung (Composition Root) eingesetzt werden.



Beispiel: Repository und Geschäftslogik



#include <vector>#include <memory>

// Schnittstelle des Repositoryclass IUserRepository {public: virtual ~IUserRepository() = default; virtual void add(const std::string& user) = 0; virtual std::vector<std::string> getAll() const = 0;};

// Implementierung für die Datenbankclass DatabaseUserRepo : public IUserRepository {public: void add(const std::string& user) override { // SQL-Abfrage } std::vector<std::string> getAll() const override { return {"Alice", "Bob"}; }};

// Geschäftslogik, die nichts von der DB weißclass UserManager { std::uniq

Blogs

Buchempfehlungen