DEV Community

Cover image for Dependency Injection for Games — Appendix: Injection Container
Filippo Ceffa
Filippo Ceffa

Posted on • Originally published at Medium

Dependency Injection for Games — Appendix: Injection Container

In the main Dependency Injection for Games article, we explained how Dependency Injection helps you organize your game or game engine architecture, but we did not address one inconvenient issue that affects it.

In large applications, Dependency Injection requires the user to write and maintain a lot of tedious and time-consuming boilerplate code.

The goal of this appendix is to demonstrate how this burden can be relieved by using a Dependency Injection Container library.

We will start by defining the problem, and providing a theoretical solution. Finally, we will show how a Container solves the problem in practice.

Practical problem

When examining the main() function in our dummy game application, we see that Dependency Injection burdens the user with two tedious tasks:

  1. Construct systems in an appropriate order;

  2. Manually pass dependencies to their dependent systems.

int main()
{
    State state{};
    // 1: User must construct Physics before Simulation
    Physics physics{}; 
    Simulation simulation {state, physics};
    OpenGL openGL{};
    Rendering rendering{state, openGL};
    // 2: User must manually pass simulation and rendering to Loop
    Loop loop{simulation, rendering}; 
}
Enter fullscreen mode Exit fullscreen mode

This is a negligible problem in our simple example, but the cost to write and modify this boilerplate code grows significantly in an application with hundreds of systems. Is there a better way than to do this work by hand?

Theoretical solution

From a theoretical point of view, our problem is very clear — we are working with a directed acyclic graph, where nodes represent systems, and edges represent dependencies:

Image description

This graph contains all the information to automate the two manual tasks:

  1. Systems construction order is determined by a depth-first traversal.

  2. Passing dependencies only requires knowing the edges of the graph.

How can we take advantage of this knowledge in our implementation?

Dependency Injection Container

The Container is the software that implements our theoretical solution. Let’s take an overview at how it works:

  • The user provides the list of services (the nodes) to the Container.

  • The Container infers the dependencies (the edges) by looking at the parameters of the system constructors.

  • The Container constructs and destructs systems in the correct order.

  • The Container passes dependencies to the dependent systems.

What follows is a possible API for a Container library, written in C++. Please, be aware that the API refers to systems as “components”, a more common term in the context of Dependency Injection:

int main()
{
    // We register components with the ComponentSet helper class.
    // No construction happens here, and systems can be added in any order.
    ComponentSet components; 
    components.add<Loop>();
    components.add<Physics>();       
    components.add<Rendering>();
    components.add<Simulation>();
    components.add<OpenGL>();
    components.add<State>();

    // Systems are constructed in correct order in Container constructor
    Container container{components}; 

} // Systems are destructed in reverse order in Container destructor
Enter fullscreen mode Exit fullscreen mode

The Container can also support Dependency Inversion, we only need to specify which system implements an interface:

// Rendering depends on IGraphics
components.add<Rendering>(); 

// OpenGL implements IGraphics
components.add<OpenGL>().implements<IGraphics>(); 
Enter fullscreen mode Exit fullscreen mode

Integrating a Container in your application is non-invasive— the only file that depends on the Container is main.cpp.

Conclusions

A Container is a great tool to reduce Dependency Injection boilerplate code. However, there are some drawbacks:

  • It is an additional dependency to maintain in your project.

  • An invalid graph is detected at runtime instead of compile time.

In the end, whether to use a Container or not is up to you — it is perfectly possible to adopt Dependency Injection without using one.

You will need to evaluate how much time you waste writing and modifying boilerplate code, and decide if using a Container is worth the tradeoff.

Next steps

  • If you wish to check out the implementation of the dummy game application using a Container, or are curious about the theory and implementation of a C++ Container library, please refer to the accompanying GitHub repository.

  • If you haven’t yet, read the appendix on Dependency Inversion.

Top comments (0)