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:
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 bothVulkan
andOpenGL
, and select the appropriate API at runtime, based on a command-line argument.We wish to test
Rendering
using aMock
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:
This solution follows the Dependency Inversion principle, which states that systems should depend on interfaces instead of implementations:
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
}
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
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
Check out the accompanying GitHub repository, which contains an implementation of the dummy game application.
If you haven’t yet, read the appendix on Injection Container.
Top comments (0)