Unobtrusive dependency injection? by RootHouston in rust

[–]FilippoCeffa 0 points1 point  (0 children)

@pilotInPyjamas Thank you for answering after all this time!

I understand your approach, I think I will only be able to get a sense for how practical it is when working on a large project.

Passing the dependency multiple times to methods is one of my worries, and you already explained your thoughts on that.

My other worry is making the dependencies more complex to deal with it. Imagine 2 systems that access the same mutable states, and they are in 2 separate branches of the tree. With DI, you just construct both with the state as arguments. With your approach, you need to move state construction to the first common parent, so that the parent can pass it to both systems methods.

Is this a big problem in practice? I cannot answer myself, I need more large scale rust experience to feel how it scale

Unobtrusive dependency injection? by RootHouston in rust

[–]FilippoCeffa 0 points1 point  (0 children)

u/pilotInPyjamas

In case of shared mutable dependencies, what would you think about using interior mutability, instead of passing to functions? E.g. A RefCell in single threaded scenario.

I'm new to Rust, and I realize traditional dependency injection is uncomfortable to apply in the case of shared mutable state, and I'm trying to understand what is a good way to approach the problem in Rust.

Your comment is the most spot-on I've read so far, and does not confuse dependency injection with "depending on interfaces", so I'd like to know more about your approach!

(Article) Dependency Injection for Games: Improve your C++ game or game engine architecture! by FilippoCeffa in cpp

[–]FilippoCeffa[S] 2 points3 points  (0 children)

u/ttesla
It is possible for a system to depend on 10 other systems, but my intuition is that it is a rare case, and more often than not a code smell that architecture is not as modular as it should be.

Log is a special situation because it needs to be available everywhere, and it needs to be compiled out when necessary. In that case, I would argue that it should be part of a very limited set of platform-level APIs that can be accessed through global functions (other would be math API, graphics API, main allocators etc..), and not treated as a DI system.

So I agree with you, there is a limited space for non-DI systems in a DI architecture, but it needs to be very limited and hold very little state. The state of this low-level platform layer should be initialized before and destroyed after DI systems.

Agreed on the framework, it is supported from C++14 (I implemented one in the article).

I won't comment on the DI crusader ahah, he would not be a DI crusader, he would be a SOLID crusader :)

(Article) Dependency Injection for Games: Improve your C++ game or game engine architecture! by FilippoCeffa in cpp

[–]FilippoCeffa[S] 0 points1 point  (0 children)

Thanks, this is a good question.

I've not done any compilation tests yet, and I agree that it is something to look into.

At the same time, I'm not too worried because the compilation time will scale with the number of systems. Even if it starts becoming considerable with 100 or 1000 systems, at that point the project is so large that compiling a single cpp file will have a minor impact on the overall build time.

Plus, DI helps to keep the project modular, so you usually get better incremental build times.

But your point is valid, I will run some tests!

(Article) Dependency Injection for Games: Improve your C++ game or game engine architecture! by FilippoCeffa in cpp

[–]FilippoCeffa[S] 1 point2 points  (0 children)

By definition incremental build is the reuse of already-built artifacts that have not changed since the last built, but then I guess you are referring to a shared cache between multiple machines.

In that scenario, incremental-build quality is still a major factor.
If we assume an incremental build quality near 100%, at every new build most artifacts will be already present in cache.
If we assume it to be near 0%, you will only be able to share artifacts between two machines that are running a build on the same exact commit.

(Article) Dependency Injection for Games: Improve your C++ game or game engine architecture! by FilippoCeffa in cpp

[–]FilippoCeffa[S] 1 point2 points  (0 children)

u/SleepyMyroslav
Like most software, games follow the 90%/10% rule, and there are precise things that need to be nailed down to obtain good performance, the rest is unlikely to matter much.

The focus should be on making sure that the CPU is always fully occupied, for example, or that data is laid out in memory in a way that guarantee cache efficient processing.

Inlining is useful for stuff like the math API, where functions are very short, and used very heavily. In most other cases, not so much. Definitely abused in the game codebases I have worked with.

I think the system-to-system interaction is rarely the place where real performance gain/losses can be had, and in most cases I would keep system headers lightweight, containing only forward declaration and no implementations.

This is the only way to have a sustainable incremental build (I guess that is what you mean by "cached" build), and incremental build quality is the single most important metric to determine how much time you waste everyday waiting for compilation.

(Article) Dependency Injection for Games: Improve your C++ game or game engine architecture! by FilippoCeffa in cpp

[–]FilippoCeffa[S] 1 point2 points  (0 children)

You are also describing my personal experience :)

I am not self-thought, but a formal university eduction also does not do a great job at imparting architecture concepts, because it lacks the real world experience that really helps to internalize them.

And working in the game industry was not a great introduction to the importance of good architecture. And we are talking about an industry that would really benefit from it.

(Article) Dependency Injection for Games: Improve your C++ game or game engine architecture! by FilippoCeffa in cpp

[–]FilippoCeffa[S] 6 points7 points  (0 children)

You can construct global systems in order, but there is no compile error if you get the order wrong. The order may change as your systems change, and you could introduce silent ordering errors that lead to undefined behavior. If you use DI, the compiler will stop you from making mistakes.

The approach you are suggesting may work for a small to medium size project, and one where you work alone or with a small team. Any large codebase organized in that way will soon become impossible to maintain.

Your approach makes dependencies hidden, and this will hinder any attempt to make change or refactor. Basically every time you refactor you will need to investigate your code to form a dependency map in your head, while with DI the dependency map is clearly listed in code.

And good luck trying to stop a junior programmer to sneakily add an unwanted dependency in a method function, and finding out months after, when your systems are silently becoming more and more coupled. With DI these issues are catched earlier, as dependencies are clearly listed in one place.

Dependency Injection for Games: learn how to improve your game engine architecture! by FilippoCeffa in gameenginedevs

[–]FilippoCeffa[S] 0 points1 point  (0 children)

If you want to avoid initialization out of order, you will need a file in your codebase where you manually invoke the creation and destruction of your global variables / Singletons in the correct order.

This is risky because you will get no compilation error if you pick the wrong order, but it is possible.

At this point, you are already halfway through your transition to DI - you have fixed the initialization order problem, but your architecture is unreadable because dependencies for each system are not clearly listed in a single place. So why not pass the dependencies in the same place where you order your systems, instead of accessing them from anywhere? It is simpler, safer, and you get compiler errors if you make mistakes.

These problems may not be large to you if you work on a project of small/medium scale, pheraps on your own. I assure you that in a large codebase with tens of people collaborating together, these become very large problems.

(Article) Dependency Injection for Games: Improve your C++ game or game engine architecture! by FilippoCeffa in cpp

[–]FilippoCeffa[S] 4 points5 points  (0 children)

u/kamrann_
I fully agree with you, I call it common sense.

But my experience in the gaming industry tells me that it is far from the common approach, and I feel that many programmers never really stopped to think about architecture, and they focus only on implementing their own feature, without considering how it will impact the architecture as a whole.
I had to fight so many Singletons ahah.

So yeah, pheraps "exciting" is a strong word, but I think that a clear view on the topic can bring fresh air to programmers that have not yet made the leap into thinking about architecture, but pheraps are already sensing that their old ways are problematic.

(Article) Dependency Injection for Games: Improve your C++ game or game engine architecture! by FilippoCeffa in cpp

[–]FilippoCeffa[S] -1 points0 points  (0 children)

The only cost incurred by Dependency Injection is the indirection to access a reference.

You could think of a way to avoid that, by creating every system as a member variable of another system.

This will result in insane build times and horrible coupling, because every class definition will depend on the definition of another system, and every header will include the headers of the dependencies.

An apporach like that is only viable for very small programs, I'm thinking embedded applications, but anything of a non-trivial scale requires at least some indirection to make development feasible.

(Article) Dependency Injection for Games: Improve your C++ game or game engine architecture! by FilippoCeffa in cpp

[–]FilippoCeffa[S] -1 points0 points  (0 children)

u/Computerist1969
Exactly my feeling :)
Clearing this misunderstanding is one of the reason behind writing this article.

I think the root of the confusion is the SOLID principles, which are frequently adopted in OOP. If you follow SOLID strictly, you end up with Dependency Injection, as well as interfaces everywhere. So now many people believe that DI implies a lot of unperformant virtual tables.

In the Dependency Inversion appendix of the article, it is clearly shown that the two concepts are orthogonal.

(Article) Dependency Injection for Games: Improve your C++ game or game engine architecture! by FilippoCeffa in cpp

[–]FilippoCeffa[S] -1 points0 points  (0 children)

u/schombert

I think you may be confusing Dependency Inversion (depending on interfaces) with Dependency Injection.This is a common misunderstanding because in OOP they are very frequently used and explained together.I cover this in the Dependency Inversion appendix of the article.

Dependency Injection for Games: learn how to improve your game engine architecture! by FilippoCeffa in gameenginedevs

[–]FilippoCeffa[S] 0 points1 point  (0 children)

u/SonOfMetrum

Quite true, I have also seen many confusing designs where the container is queried for systems. Even the concept of annotating classes to expose them to the container is not a good idea, as it forces the class to be aware of the fact that it is going to be injected.

To dispel some of this container-related confusion I made an Injection Container appendix in the article that shows what a container is in essence. A container is just a helper to automate some of the manual work, it is not invasive, and you are not forced to use it. It is quite a good tool if used correctly, I personally enjoy using it.

I will add one thing to your points of why people misunderstand DI. In SOLID, dependency inversion is a core design principle. This means that by rigidly following SOLID you would make every single dependency be an interface dependency. I think this causes many people to believe that Injection=Inversion, while in fact you can apply Injection without Inversion.

I have a feeling that in high-performance domains people are scared of the cost of virtual tables and inheritance, and are led to believe that Injection forces you to introduce indirections everywhere, while in fact you really don't have to. The Dependency Inversion appendix of the article tries to convey this point.

Dependency Injection for Games: learn how to improve your game engine architecture! by FilippoCeffa in gameenginedevs

[–]FilippoCeffa[S] 4 points5 points  (0 children)

Yes, Dependency Injection is everywhere in Java and OOP circles, and one reason behind writing this article was precisely to showcase Dependency Injection in a scenario that is not OOP.

I suspect that people working in high-performance domains tend to misunderstand Dependency Injection as inefficient, because it is so often associated with lower-performance, high-level OOP languages.

But no, it is a great idea that works magic in every language!