Dependency inversion is a principle useful for decoupling. In essence, it means that high-level modules should not depend on lower-level ones. Instead, both should depend on abstractions.
C++ allows two ways to inverse the dependencies between your classes. The first one is the regular, polymorphic approach and the second uses templates. Let's see how to apply both of them in practice.
Assume you're modeling a software development project that is supposed to have frontend and backend developers. A simple approach would be to write it like so:
class FrontEndDeveloper {
public:
void developFrontEnd();
};
class BackEndDeveloper {
public:
void developBackEnd();
};
class Project {
public:
void deliver() {
fed_.developFrontEnd();
bed_.developBackEnd();
}
private:
FrontEndDeveloper fed_;
BackEndDeveloper bed_;
};
Each developer is constructed by the Project class. This approach is not ideal, though, since now the higher-level concept, Project, depends on lower-level ones – modules for individual developers. Let's see how applying dependency inversion using polymorphism changes this. We can define our developers to depend on an interface as follows:
class Developer {
public:
virtual ~Developer() = default;
virtual void develop() = 0;
};
class FrontEndDeveloper : public Developer {
public:
void develop() override { developFrontEnd(); }
private:
void developFrontEnd();
};
class BackEndDeveloper : public Developer {
public:
void develop() override { developBackEnd(); }
private:
void developBackEnd();
};
Now, the Project class no longer has to know the implementations of the developers. Because of this, it has to accept them as constructor arguments:
class Project {
public:
using Developers = std::vector<std::unique_ptr<Developer>>;
explicit Project(Developers developers)
: developers_{std::move(developers)} {}
void deliver() {
for (auto &developer : developers_) {
developer->develop();
}
}
private:
Developers developers_;
};
In this approach, Project is decoupled from the concrete implementations and instead depends only on the polymorphic interface named Developer. The "lower-level" concrete classes also depend on this interface. This can help you shorten your build time and allows for much easier unit testing – now you can easily pass mocks as arguments in your test code.
Using dependency inversion with virtual dispatch comes at a cost, however, as now we're dealing with memory allocations and the dynamic dispatch has overhead on its own. Sometimes C++ compilers can detect that only one implementation is being used for a given interface and will remove the overhead by performing devirtualization (often you need to mark the function as final for this to work). Here, however, two implementations are used, so the cost of dynamic dispatch (commonly implemented as jumping through virtual method tables, or vtables for short) must be paid.
There is another way of inverting dependencies that doesn't have those drawbacks. Let's see how this can be done using a variadic template, a generic lambda from C++14, and variant, either from C++17 or a third-party library such as Abseil or Boost. First are the developer classes:
class FrontEndDeveloper {
public:
void develop() { developFrontEnd(); }
private:
void developFrontEnd();
};
class BackEndDeveloper {
public:
void develop() { developBackEnd(); }
private:
void developBackEnd();
};
Now we don't rely on an interface anymore, so no virtual dispatch will be done. The Project class will still accept a vector of Developers:
template <typename... Devs>
class Project {
public:
using Developers = std::vector<std::variant<Devs...>>;
explicit Project(Developers developers)
: developers_{std::move(developers)} {}
void deliver() {
for (auto &developer : developers_) {
std::visit([](auto &dev) { dev.develop(); }, developer);
}
}
private:
Developers developers_;
};
If you're not familiar with variant, it's just a class that can hold any of the types passed as template parameters. Because we're using a variadic template, we can pass however many types we like. To call a function on the object stored in the variant, we can either extract it using std::get or use std::visit and a callable object – in our case, the generic lambda. It shows how duck-typing looks in practice. Since all our developer classes implement the develop function, the code will compile and run. If your developer classes would have different methods, you could, for instance, create a function object that has overloads of operator() for different types.
Because Project is now a template, we have to either specify the list of types each time we create it or provide a type alias. You can use the final class like so:
using MyProject = Project<FrontEndDeveloper, BackEndDeveloper>;
auto alice = FrontEndDeveloper{};
auto bob = BackEndDeveloper{};
auto new_project = MyProject{{alice, bob}};
new_project.deliver();
This approach is guaranteed to not allocate separate memory for each developer or use a virtual table. However, in some cases, this approach results in less extensibility, since once the variant is declared, you cannot add another type to it.
As the last thing to mention about dependency inversion, we'd like to note that there is a similarly named idea called dependency injection, which we even used in our examples. It's about injecting the dependencies through constructors or setters, which can be beneficial to code testability (think about injecting mock objects, for example). There are even whole frameworks for injecting dependencies throughout whole applications, such as Boost.DI. Those two concepts are related and often used together.