Advertisement

C++ how do you use dependency injection?

Started by April 02, 2024 10:55 PM
17 comments, last by frob 7 months, 2 weeks ago

I'm working on a engine and game in C++ and I got a couple of manager/subsystem type classes which I probably won't have more then one of, but I don't want to make them singletons (although that would probably be the easiest solution) instead dependency injection seems to be a better alternative(?)

For some context I have a game class which originally was responsible for initializing everything, but the thought had occurred that subsystems like input manager, renderer are part of the engine which gets used throughout the game so I moved that stuff into the main function the problem though is how does the game use them?

My solution was this probably improper use of DI

// main function
inputManager = new InputManager();
toolOverlay = new ToolOverlay();
meshManager = new MeshManager();
shaderManager = new ShaderManager();
ui = new UI(rmlContext);
scene = new Scene();

game = new Game(*inputManager, *meshManager, *shaderManager, *ui, *scene);

// game.cpp
Game::Game(InputManager& inputManager, MeshManager& meshManager, ShaderManager& shaderManager, UI& ui, Scene& scene) :
    inputManager(inputManager),
    meshManager(meshManager),
    shaderManager(shaderManager),
    uiManager(ui),
    scene(scene)
{
    AssetHandle suzanne = meshManager.loadMesh("Assets/Meshes/suzanne.obj");
    AssetHandle basic = shaderManager.loadShader("Assets/Shaders/Mesh.vert", "Assets/Shaders/Mesh.frag");

    player = std::make_unique<Player>(suzanne, basic);
    camera = std::make_unique<Camera>(inputManager);
}

All I really seem to do was add bloat to the parameter list lol.

The difference is you can create it with any instances. It might be a main object for the game, it might be an automated test proxy, it might be a listener or logger or rebroadcast system. You might have a headless client where the interfaces use a network or replay a file or is driven by AI. All the system needs to know is that it implements the interface.

You can also create more than one, as many as you want. That is useful for testing, and any other automation like replay or network uses.

If it were a Singleton you would have a hidden dependency on the specific object instance, the code would be brittle and unable to be used in any other way. It might work for the one original situation, but it is also limited. By intentionally choosing your dependencies and connections, you can be more intentional about design.

Use of Singleton in the systems is a fairly bad code smell. It is a pattern indicating many bad habits are likely present, and many good habits are likely missing.

Having the main function create a bunch of objects lets you control when the work happens rather than either during static initialization before main, or randomly and uncontrollably during first use. It's generally not a big deal to put into a set of initialization functions during the startup routine.

For this specific code, what exactly is Game? Does it need all those things? Why is Game manipulating meshes and shaders? Those dependencies seem unexpected. Why is Game manipulating UI directly, why is that a dependency? Why does Game require an already-created scene when it is constructed, wouldn't a game session have scenes that come and go? A benefit of thinking in systems, including dependencies and dependency injection, is that you can think critically about both object lifetimes and the actual intentional dependencies. These don't look fully considered.

Advertisement

Dependency injection is one of those design patterns that matters a lot in enterprise software with hundreds of developers working on the same project and convoluted bureaucratic processes for getting changes approved, but a lot less for lone wolf game developers.

That said, why are you using new to create your objects instead of just putting them on the stack as local variables? And conversely, why are you passing them in by reference instead of using pointers? Good practice is to avoid dynamic memory allocation where possible and to use pointers instead of references for member variables.

That sure does look like dependency injection to me, and as you say, it does bloat the parameter list a lot if you do it for every subsystem. As in any situation where you're making the code more complicated than you feel it needs to be, it's worthwhile to stop and think about why you want to do this.

What are you trying to accomplish with the dependency injection? Are you just trying to avoid singletons here? Are there things outside of Game that need to talk to those objects meaning that you can't just encapsulate them as part of the Game object? Can you move the dependency injection to pass these dependencies as arguments to only the methods on Game that make use of them?

frob said:
For this specific code, what exactly is Game?

@frob Well I assumed I needed some starting point where game setup stuff happens so I created Game which uses the asset managers to load assets, creates the player, sets the initial ui screen. I also just created it because I felt that I needed to separate the game from the more engine setup stuff, but perhaps this is just another case of overthinking.

In that case, you'll learn more as you go. You will eventually need to think of what systems own what functionality, where everything lives, the life cycles of everything, and much more.

For now it's probably best to piece together whatever you can to make it work. You'll learn a ton along the way, just don't expect to win awards and make fortunes from your first few projects.

If your goal is to make games, you might go look at what the existing engines give you. If you're not using an engine, you'll want to look at the tutorials and guides for the libraries you intend to use. It can be tempting for beginners to re-invent everything, only to discover they've spent years cobbling together an engine and still haven't started their actual game.

Advertisement

frob said:
If your goal is to make games, you might go look at what the existing engines give you

That used to be my goal, but honestly despite the difficulty I've found it more enjoyable to learn this like whole other side of programming and game development. Anyways yeah I'll find something that works and move on rather then spend more time trying to solve a problem I don't fully understand.

@Konjointed You'll want to implement a static service container that's accessible globally. This will save you from having to pass every object around the game. But keep in mind, these are still singletons, but better than static references if you put them in a container.

Yes statics should be avoided, but if you have just one it's a lot easier to manage and adapt your code in the future.

konev13 said:
@Konjointed You'll want to implement a static service container that's accessible globally. This will save you from having to pass every object around the game. But keep in mind, these are still singletons, but better than static references if you put them in a container. Yes statics should be avoided, but if you have just one it's a lot easier to manage and adapt your code in the future.

I'd strongly advise against taking this advice. This is just lazy coding that will come back to bite you later. A singleton is a singleton, no matter how you dress it up. If there are a large number of “global" pointers you need to access, put them all in a struct and pass that around instead. Such as:

struct GlobalServices
{
	InputManager* input;
	ResourceManager* resources;
	GraphicsContext* graphics;
	Scene* scene;
};

It's much easier to add a pointer to this struct than it would be to add another parameter in every constructor. You can even “override” behavior in child objects: the parent object provides the child a different GlobalServices, where it might do things like change the scene, use a different resource manager, etc. I use this pattern throughout my engine's editor and it works well.

Aressera said:

konev13 said:
@Konjointed You'll want to implement a static service container that's accessible globally. This will save you from having to pass every object around the game. But keep in mind, these are still singletons, but better than static references if you put them in a container. Yes statics should be avoided, but if you have just one it's a lot easier to manage and adapt your code in the future.

I'd strongly advise against taking this advice. This is just lazy coding that will come back to bite you later. A singleton is a singleton, no matter how you dress it up. If there are a large number of “global" pointers you need to access, put them all in a struct and pass that around instead. Such as:

struct GlobalServices
{
	InputManager* input;
	ResourceManager* resources;
	GraphicsContext* graphics;
	Scene* scene;
};

It's much easier to add a pointer to this struct than it would be to add another parameter in every constructor. You can even “override” behavior in child objects: the parent object provides the child a different GlobalServices, where it might do things like change the scene, use a different resource manager, etc. I use this pattern throughout my engine's editor and it works well.

If you have a single struct with a collection of pointers that you pass around globally, then theres no difference than having a single instance that you can access globally vs having to pass around the pointer to every object.

The benefit of passing a struct around in constructors is if you have different references inside the struct based depending on where you pass it.

This topic is closed to new replies.

Advertisement