C++ how do you use dependency injection?

Started by
17 comments, last by frob 2 weeks, 4 days ago

konev13 said:
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.

There is at least one difference, even in the case that the service locator only ever points at the same things at every call site. Passing the “structure of globals”/"service locator" around instead of having extern globals means that only functions that actually NEED to talk to the globals will be able to do so, and the fact that they do so is explicit. Ideally, you wouldn't even pass all of the services around like this; you would instead pass JUST the services you need around. The services may still be “global” in lifetime, but they wouldn't be “global” in the sense that everything in the entire program could access them from anywhere. This can make the code easier to understand and enforces a bit of discipline.

Also, while the typical use case for having the service locator point to different objects at different call sites would be mock/fake objects in tests, there are others. You may for instance have double-buffered game state so that your gameplay code and rendering code can run at the same time on different frames. The service locator makes this easier; to do this with actual global variables, you need to start getting into using thread-local storage and other such platform-specific things.

Advertisement

@undefined You can use mocks with the container as well. The only thing the container should explicitly hold are interfaces, and then you pass the concrete implementation or mocks as needed.

Now Im not saying you should shove everything into this container, but if you need a true singleton thats accessible everywhere, its better to use a single container with a static reference to it since everything needs to be able to access it, and you want at most one instance of it.

Oberon_Command said:

There is at least one difference, even in the case that the service locator only ever points at the same things at every call site. Passing the “structure of globals”/"service locator" around instead of having extern globals means that only functions that actually NEED to talk to the globals will be able to do so, and the fact that they do so is explicit. Ideally, you wouldn't even pass all of the services around like this; you would instead pass JUST the services you need around. The services may still be “global” in lifetime, but they wouldn't be “global” in the sense that everything in the entire program could access them from anywhere. This can make the code easier to understand and enforces a bit of discipline.

Also, while the typical use case for having the service locator point to different objects at different call sites would be mock/fake objects in tests, there are others. You may for instance have double-buffered game state so that your gameplay code and rendering code can run at the same time on different frames. The service locator makes this easier; to do this with actual global variables, you need to start getting into using thread-local storage and other such platform-specific things.

Oberon_Command said:

There is at least one difference, even in the case that the service locator only ever points at the same things at every call site. Passing the “structure of globals”/"service locator" around instead of having extern globals means that only functions that actually NEED to talk to the globals will be able to do so, and the fact that they do so is explicit. Ideally, you wouldn't even pass all of the services around like this; you would instead pass JUST the services you need around.

Counter-argument: sometimes I want every single bit of code in my program to be able to access a global service. For example, I want every single piece of code in my game to have access to the logging system. Not because it actually needs to log something, but because I want to be able to add and remove logging as needed for debugging specific problems, without walking up the dependency tree and adding the logging system as an argument to who know how many functions.

Logging is a classic mistake there. Yes, you want a generally accessible instance. No, that instance is not a Singleton.

Perhaps the easiest example is standard error stream, or standard out that beginners usually use. The cout stream or cerr stream is globally available, and can be used right away. However, they are not Singleton objects. You can create more, you can create different ones, and you can swap out the built-in with your own if you need.

Similar with logging, yes, it is important to have a globally accessible logging system. But it is still important to allow it to be instanced for additional, special purpose logs. It is important to be able to swap the system with another if needed. Neither fit the Singleton pattern.

Globally accessible does not mean the Singleton pattern. Well-known instances and globally accessible service locator systems are better solutions. Both patterns are just as simple to use as the Singleton pattern, without the downside of “There Can Be Only One. This One.”

a light breeze said:

Oberon_Command said:

There is at least one difference, even in the case that the service locator only ever points at the same things at every call site. Passing the “structure of globals”/"service locator" around instead of having extern globals means that only functions that actually NEED to talk to the globals will be able to do so, and the fact that they do so is explicit. Ideally, you wouldn't even pass all of the services around like this; you would instead pass JUST the services you need around.

Counter-argument: sometimes I want every single bit of code in my program to be able to access a global service. For example, I want every single piece of code in my game to have access to the logging system. Not because it actually needs to log something, but because I want to be able to add and remove logging as needed for debugging specific problems, without walking up the dependency tree and adding the logging system as an argument to who know how many functions.

Sometimes being the key word. Not every subsystem is best off as a true global. Also, logging and assertions are special cases in that most codebases turn them off in release, so the perf cost of locking the logging state with each log call is not paid in the shipping code.

@Aressera An elegant idea, but "GlobalServices" is a curious name: avoiding both true global variables and singletons by explicitly passing references is the whole point of this aggregate of dependencies.

Maybe “ExternalServices” or “GameServices” or “GameContext” would be clearer.

Omae Wa Mou Shindeiru

frob said:

Logging is a classic mistake there. Yes, you want a generally accessible instance. No, that instance is not a Singleton.

Actually, my logging system is not a singleton because it's neither a class nor an instance. It's a global function.

A global function addresses many of the Singleton pattern issues, so it's fine if it works for your system.

Advertisement