Jonathan Boccara's blog

Write Your Own Dependency-Injection Container

Published June 7, 2019 - 0 Comments

This post focuses on the use of a design pattern to connect the modules of a codebase in a structured and testable way.

This is a guest post from Nicolas Croad. Nicolas has been a professional developer primarily in c++ for most of a 15 year career. Presently working in real time graphics for the New Zealand MetService.

Today I demonstrate a C++ harmonious implementation of the service-locator design pattern. As with most programming techniques there are trade-offs being made in deploying any pattern.
The advantages to this technique are that it,

  • Uses a consistent approach to dependency-injection (facilitating testability) which can therefore be applied to the extent required, rather than piecemeal to the overall project.
  • Minimizes the dependencies of functions becoming explicitly exposed as part of the functions interface.
  • Makes lifetimes of dependencies work in particularly typical way for C++ which in turn makes it easy to manage potential lifetime issues between dependencies.

Before proceeding some of the implementation details have been elided out of the code snippets presented here. Some further details and a working example are available on Github.

What is Dependency-Injection About?

Dependency-injection (as described on Wikipedia or on Martin Fowler’s website) is a design pattern which is frequently used to support modularity and testability of the code base. As a brief summary dependency-injection is when one object or function supplies the required dependencies of another object or function.

There are 4 roles which cooperate to implement dependency-injection

  • The service object to be injected.
  • The client object which depends on the service(s) being injected.
  • The interface through which the client object uses the service(s).
  • The injector which injects the service(s) into the client.

In some cases the interface is separate to the service, however in many examples described here the interface is the public API of the service.

Naive Dependency-Injection

A simple way to organise this can be to pass the dependency as an additional argument to the function being invoked.

The parameter means that when I write test cases for the function foo() I will be able to pass through other services in place of its frobber interface. Depending on the functionality which is being tested these objects may comprise any of stub, mock or fake objects or be the usual services when some kind of integration test is being performed. In the example above tests may verify that the expected value of p is being passed to the frob() function (for values of x) by installing a mock frobber service in testing.

Single-Parameter Dependency-Injection

As a project takes shape the dependencies between modules will develop and change and using the naive implementation of dependency-injection (of passing these dependencies as individual parameters) requires many of these function signatures to change. In addition dependency-injection can lead to exposing all the dependencies of the implementation as part of the public API of a function or type. Frequently the interfaces used by a function are not pertinent details and presenting them in the functions signature may prove disruptive if they are regularly changing.

In order to improve this the dependencies can be grouped together into a dependency-injection container type with the abbreviated name DI. I almost exclusively pass this as the first parameter so typically I have written the equivalent to,

Other Dependency-Injection Approaches

In the section further down, Service Lifetimes, I introduce a program stack-based mechanism for managing the lifetime of the objects in the DI container. Otherwise there are a broad range of approaches to dependency-injection used with the design pattern.

These include constructor-injection (where the dependencies are injected during a constructor call) and setter-injection (where the dependencies are wired into the client using setters after construction). Both of these approaches assume that the lifetime of the service object will span the lifetime of the client object using it.

This assumption suits a programming environment which uses a garbage collector much better than the memory management strategy used in C++. In the practice of using the DI container design pattern it is important to understand that, when program types retain references (or pointers) to either the DI container or any of its members, similar object lifetime issues are reintroduced.

Similarities to the Service-Locator Pattern

So far this is just a description of dependency-injection with a level of indirection added in. Adding this level of indirection makes the approach look very similar to the service-locator design pattern. In that pattern dependency resolution occurs via a service-locator API providing a reference to the service which the client requires.

In fact if all the accesses to the DI container were done via the static method (introduced in Out of Control Function Interfaces) then that would be the most appropriate description of this design.

My personal preference would be to retain the practice of passing the DI container as an explicit parameter in the cases where this is possible. This should make it clear to readers,

  • That the lifetimes of the objects in the container are scoped by the program stack.
  • What the DI container parameter is doing for the function its being passed into.

Service Lifetimes

Another fairly common technique for dependency-injection is to create some kind of a templated service-locator API where the registered or defaulted services are available. The largest problem with this technique relates to the lifetimes of the services which are installed or resolved on demand by that API.

Usually this still leads to relatively complicated test code where a number of to-be-injected dependencies need to be set up and torn down around the tests and a failure to maintain this frequently leads to the execution order of tests becoming rigid (e.g.: the tests only pass when executed in a specific order). Also depending how your API is implemented this can also lead to well-known static initialization and/or destruction order problems between services.

The DI container approach on the other hand, uses the program stack to define the lifetime of the services in the container. To accomplish this a class template is used:

The job of this class template is a fairly-typical RAII-like task. It holds onto an initialized member of the DI container. Following construction of item_ a pointer  member_ in the DI container is pointed at it, and just before destruction the pointer is returned to null. Thus, objects in the DI container have their lifetime managed by the C++ compiler.

In the event that further inspection or initialisation of the service object kept alive by this class template is required then this is available using the getComponent() methods.

Before Guaranteed Copy Elision

This previous implementation of the DILifetime template works when the compiler supports Guaranteed Copy Elision. However many projects will not be exclusively using C++17 compilers just yet.

The identical class interface is however possible using earlier language standards as long as you are willing to allocate installed services themselves on the heap. One of the primary features of the class template is that it should support installing services which do not themselves have either copy or move functionality.

Using earlier standards a syntactically equivalent interface is supported by the following class template.

The Question of God (Classes)

With only this tiny framework we are ready to implement the DI container class itself. Reuse and sharing of library code between projects is often described positively and there are obvious benefits, however in the case of the DI container itself the contents are manifestly types of and maybe a reflection of the architecture of the project using the container. Due to this my suggestion would be that this class should be implemented specific to each projects requirements.

The first implementation concern is that your DI container should be able to be included with only the names of all the interfaces it resolves. The main reason it’s important for this container to work with only a forward declaration is an architectural principal.

As this technique proliferates through your project the DI container provides access to more of the components. This can lead to the usually unintentional design known as the god class, so this class is restricted to purely providing access to a collection of types without specifying their APIs. In C++ specific terms the DI container type is a header-only class and all the methods described below can be written inline.

For each type contained in the DI container there are two methods and one field added to the container.

The methods intentionally return a non-const reference in the constant accessor. Injecting the container consistently as a const DI& parameter and making the installXXX() methods non-const uses the compiler to enforce that initialization happens in only one area of the program (as described in Container Initialization).

Accessing an interface which has not previously been installed in the container, or replacing the services in the container with others are not supported and immediately triggers an assert. This avoids any kind of hidden relationships between the container components (such as order of execution dependencies between tests).

As more types are added to the container there can become a lot of self similar code being added to the DI class. In order to address this the field and the functions getXXX() and installXXX() might be written as a (non-trivial) function macro making the declaration/definition if the DI class into a list of the container members.

Arguably there are stronger benefits to writing out each container member long-hand and so enabling the use of the customization points described below to highlight the intended use. The implementation of this type also represents a good place to document the projects architecture.

For the macrophobe a third example is among the accompanying gist, which uses multiple inheritance in place of the above macro.

Container Customization Points

The getFactory() and installFactory() functions enable a number of customization points depending how the services in the DI container behave.

  • For any available interface which has a fully const API the getXXX() function is able to return a const reference to the service.
  • When, as will quite frequently be the case, services installed with installXXX() don’t require constructor parameters then the args parameter of this function can be dropped.
  • The template parameter T of installXXX() can have a default argument. This allows components to be installed without an explicit template argument at the call site.
  • In the rare case of an optional interface the getXXX() function returns a pointer to any service installed instead of a reference.

These customization points ought to be used to highlight the intended usage of the interfaces available from the DI container.

Out of Control Function Interfaces

In some cases the API of some of the functions being implemented in a project will not be modifiable. In these cases such functions can still require access to the DI container but will not be able to accept it as a parameter.

To facilitate this use case the DI container can be made available statically fairly easily. The expectation for container usage is that there will be only one DI container in any program or test program at any time, or in some multi-threaded instances this could be one per thread.

To facilitate this the DI container can be updated as follows,

This in turn allows functions which require access to the DI container to access it with a call to DI::getDI() so long as a container has been created earlier in the program.

Container Initialization

In some cases a complex project will implement multiple executables, however even in such cases we may still prefer to have one container initialization routine.

To enable this the container can be initialized in one function and then passed into a type-erased function call (which allows a lambda to be passed at the call site).

Wherever this function is defined will need to sit at a fairly high layer of a project as it will need to include many of the specific services of the project.

How Does the Resulting Code Look

The implementation code ends up making use of the DI container as shown here.

Further test cases for this example could be written roughly as follows (using Catch2 by Phil Nash)

Some Variations

Another reason to implement the DI container type bespoke is that there can be some project specific characteristics around dependency-injection. Next I will describe a couple of obvious variations which demonstrate that adaptations can often be implemented without significantly adding to the complexity of the approach.

Performance Specifically Virtual-Function-Call Overhead

The instinctive challenge to a lot of dependency-injected code is how much this impacts a program’s running time.

When implementing this technique a common approach is to make your interface abstract and then implement it for exactly one service which is always used in the real program. The abstract interface then provides an injection point for stub, mock or fake types which are frequently injected in the test code.

The outcome of this is that instead of making function calls, code which provides this testability often ends up making virtual function calls.

Using the DI container technique however there is a reasonably expedient technique which can trade off number of objects being built to de-virtualise such calls. Such a service is then added to the DI container and makes it possible to compile the unit either with the virtual functions when building test code, or without the virtual functions when building release code.

Though in most cases this technique is probably premature optimization it is quite simple to apply it to classes which primarily implement behaviour without implementing state.

In addition when performance is not a concern the technique of having the actual implementation code be provided as a virtual function call can still be used to facilitate easy substitution of actual for stub, fake or mock calls during testing.

Programs with Multiple Threads

In a multi-threaded program many clients may resolve interfaces without necessarily having a thread-safe API for these services. To enable this the DI container itself can be placed in thread-local storage and the service objects can be added during container initialization specific for each thread.

Further to this the initialization functions for the container don’t need to be the same or provide a matching set of service objects.

Conclusion

Across the whole of a large code base encouraging expressive code can be about a widely applied solution fitting into many parts of the program. The trade-offs involved with this dependency-injection implementation seem fairly ergonomic and natural.

Where a solution requiring dependency-injection is needed this implementation should routinely be applicable. The consistency this fosters in turn makes it easy to recognise the familiar solution being applied again, rather than a less familiar solution from the quite broad portfolio of available dependency-injection mechanisms.

The overall scheme grew out of a more trite idea, to group together a number of injected function parameters into a single struct and so reduce the total parameter count. This also had the benefits of re-encapsulating these dependencies into the implementation and only exposing the fact that the function was using dependency-injection in the function declaration. Even this becomes unnecessary as long as you are willing to provide static access to the relevant DI container, though I think that test cases seem to read more clearly having an explicit DI container parameter.

One of the key trade-offs at play here seems to be a choice between forcing explicit specification of services or alternatively supporting implicit setup of the service objects by specifying a default implementation.

The provision of a default implementation which is then returned when no explicit service has been installed is typical of many similar dependency-injection mechanisms especially those involving static access to interfaces (e.g.: often a singleton pattern). I believe the alternative here of requiring explicit setup and teardown of services into the DI container and a clear place designated towards actual container initialization makes the object lifetimes comparatively simple to observe. It is also very nice to have a large part of this implemented and managed automatically by the C++ compiler.

In summation I think this pattern could be used to meet most dependency-injection needs in almost any C++ code base and that doing so would frequently make the code base simpler to comprehend, pliable and testable.

You will also like

Become a Patron!
Share this post! Facebooktwittergoogle_pluslinkedin    Don't want to miss out ? Follow:   twitterlinkedinrss

Get a free ebook on C++ smart pointers

Get a free ebook of more than 50 pages that will teach you the basic, medium and advanced aspects of C++ smart pointers, by subscribing to our mailing list! On top of that, you will also receive regular updates to make your code more expressive.

No spam. You can unsubscribe at any time.