Dependency Injection for Games — Appendix: Dependency Inversion

filippoceffa

Filippo Ceffa

Posted on September 11, 2023

Dependency Injection for Games — Appendix: Dependency Inversion

In the main Dependency Injection for Games article, we used a dummy game application as an example to explain how Dependency Injection can help you organize your game or game engine architecture.

For the sake of simplicity, the example intentionally omitted a technique commonly adopted in real-world applications: Dependency Inversion.

The goal of this appendix is to expand our problem to include Dependency Inversion, and prove that Dependency Injection remains the ideal solution.

We will start by introducing Dependency Inversion through a practical example, by modifying our dummy game application to make use of it.

Problem (re)definition

First, let’s recap the dependency graph of our dummy game application:

Image description

Rendering uses the OpenGL graphics API, but as the requirements of our project grow, we may want Rendering to work with multiple graphics APIs. The following scenarios are common:

  • We wish Rendering to support both Vulkan and OpenGL, and select the appropriate API at runtime, based on a command-line argument.

  • We wish to test Rendering using a Mock graphics API, in order to make test results independent of graphics API details.

We can provide this flexibility by making Rendering depend on the IGraphics interface, and make OpenGL, Vulkan and Mock implement it:

Image description

This solution follows the Dependency Inversion principle, which states that systems should depend on interfaces instead of implementations:

Image description

The introduction of Dependency Inversion slightly changes the nature of our problem — we are not dealing anymore with a dependency graph of systems, but with a dependency graph of systems and interfaces.

In the next section we will see how Dependency Injection comfortably supports these new requirements, and remains the ideal solution to organize your game architecture.

Problem solution

Dependency Inversion shields a system from the implementation details of its dependencies, including their constructors.

Consequently, when adopting Dependency Inversion, a system cannot be responsible for creating its own dependencies. They must be created somewhere else, and shared with the system through their interface.

An application that adopts Dependency Injection creates all systems in main(), and forwards them to their dependent systems. This provides the perfect framework to support Dependency Inversion:

int main()
{
    OpenGL openGL{};             // dependency created outside system
    Rendering rendering{openGL}; // dependency passed to system
}
Enter fullscreen mode Exit fullscreen mode

The only step necessary to adopt Dependency Inversion is to change a system’s dependency from a concrete implementation to an interface:

class OpenGL : IGraphics {...};

Rendering::Rendering(OpenGL&){}    // Injection, but not Inversion
Rendering::Rendering(IGraphics&){} // Injection and Inversion
Enter fullscreen mode Exit fullscreen mode

Conclusions

Dependency Injection can effortlessly support Dependency Inversion.

Dependency Inversion is a powerful tool in your toolbox — it has benefits in modularity and testability, and it has drawbacks in complexity and performance. Like every tool, it should be used when appropriate.

Dependency Injection grants you full control in deciding if and where to adopt Dependency Inversion, allowing you to design your game or game engine architecture in complete flexibility.

Next steps

💖 💪 🙅 🚩
filippoceffa
Filippo Ceffa

Posted on September 11, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related