Dependency Injection has become an established part of good design in the past decade, as it allows us to control how a given object will behave by means of controlling its dependencies, instead of allowing it to resolve the dependencies itself. Just a quick refresher:
class Foo { SomeClass o; public Foo() { o = new SomeClass(); } }
In this example Foo resolves its dependency on SomeClass alone. This creates a very hard dependency, that will not easily allow us to swap in an alternative implementation of SomeClass, thus disallowing any extensibility of Foo. This setup will also make testing the interaction between Foo and SomeClass harder than it needs to be. Now, consider this alternative approach:
class Foo { SomeClass o; public Foo(SomeClass oInstance) { o = oInstance; } }
This allows us to easily swap out the actual implementation of SomeClass used to any implementation that inherits from SomeClass. This technique is especially useful when writing unittests, as we can use it to inject mocks into our object, making it easy to test the interaction between Foo and SomeClass by means of the methods Foo calls on SomeClass.
Note that there is also the possibility to inject dependencies by means of setter functions – this is however the probably worst way of doing it, as it is prone to being used wrongly (i.e. one might forget to satisfy all dependencies, leading to all sorts of errors or – just as bad – to errorchecking all over the place instead of just the C’tor).
So far, so trivial, why whould one write yet another blog post?
Glad you asked. Dependency Injection has some drawbacks, when it’s done like above: Important classes tend to amass a lot of dependencies thus making it necessary to pass lots and lots of instances to the constructor. I’ve seen codebases, where the c’tor of these kinds of objects need a dozen or more parameters to satisfy all dependencies. This kills any readability and – even worse – will make testing pretty hard as we’ll have to construct many dummyobjects by hand.
The C++ World
Managed languages have long used Dependency Injection Libraries such as ninject or similar. However, there are few really good dependency injectors available for C++. Google’s fruit which is nice, albeit a bit complicated for my taste. There is also boost.DI which comes with a lot of functionality, however it too is complicated, and – for most cases – we actually need very little. From my experience of the last ten years, the most common use cases are:
- Instanciate a new object
- Resolve a concrete implementation, given an abstract base class.
Ideally we’d be able to rewire the injector if necessary – this would enormously ease test development.
Getting started
Let’s think about the ideal API for our DI:
class Foo { private: std::unique_ptr<IBar> barInstance public: Foo(): barInstance(????) { } } int main() { Foo f; }
What would be the best way to resolve the IBar dependency here? What would be ergnonomic? Let’s assume that we want to express ownership here, i.e. that our Foo instance owns the bar (hence the unique_ptr). We want to avoid passing an instance via C’tor and we also want multiple Foos to all have their own Bars.
Since we’re good programmers we’ll keep away from raw pointers and stick to unique_ptrs or something similar. What if our unique_ptr could resolve the dependency for us? Something like:
class Foo { private: Import<IBar> barInstance public: Foo() { } } int main() { Foo f; }
Truth be told, this is actually trivial:
template<class T> class Import { public: Import() : value(generatorFunc()) { } T* operator->() {return value.get(); } private: std::unique_ptr<T> value; static std::function<T* ()> generatorFunc; };
(I’ve omitted the usual CC’tor and the like for brevity).
Okay, so how does this exactly work and how does it behave afterwards?
- For each type, we want to automatically import, we’ll define a generator function, that knows how to generate the correct instance (generatorFunc)
- The C’tor of Import will create a new instance by calling the generatorFunc and passing the result into a unique_ptr.
- The good: If one tries to import for which no generatorFunc was passed we will be punished by a compiler error. And since generatorFunc is actually an object itself it can never be set to nullptr
- We can easily switch the generatorFunction at runtime, making tests easier.
- We can easily manage our software configuration at compile time if we’re using multiple targets.
Truth be told, these few lines of code actually solve quite a lot of the dependency injection uses I’ve had in the last couple of years. The other second large block is non-owning imports, where a group of objects need access to a central object (a logger would be a common usecase). This is actually even more trivial, as we can skip using a pointer in favour of a reference:
template<class T> class NonOwningImport { public: NonOwningImport() : val(providerFunc()) { } T& operator->() { return val; } T& val; static std::function<T& ()> providerFunc; };
Using it:
IBar& f() { static Bar b; return static_cast<Bar&>(b); } std::function<IBar& ()> NonOwningImport<IBar>::providerFunc = f; int main() { NonOwningImport<IBar> b; }
Having to wire up the static providerFunc (or generatorFunc in case of Import) is a bit nasty, but can also be simplified by means of a macro. Note that we could’ve used a shared_ptr for NonOwningImport as well. I’ve opted against it here as the pattern of using a static variable that lives inside a pseudo-factory function is quite common.
Overhead
Using an owning Import instead of a unique_ptr will generate zero overhead, both generate the same assembly and will thus perform the same (using GCC ARM 8.3.1). I did not check for the NonOwnedImport, but I’d suspect that it’ll behave like a shared_ptr.
Should I use this?
As usual: It depends. I’ve worked quite some codebases, where using this approach would have a decent effect on the complexity of the initialization of the device – i.e. it would reduce complexity, especially in cases where C’tor injection is used. This approach might cause some issues with objects that need complex construction, so you should be wary of that.
Image by Joshua Aragon