Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

My take on Dependency Injection and Singleton

Discussion in 'General Discussion' started by Le_Tai, Mar 3, 2020.

  1. phobos2077

    phobos2077

    Joined:
    Feb 10, 2018
    Posts:
    350
    If you read my first post carefully, there are a couple of clear examples when DI is needed. DI is a good practice in certain scenarios because it makes code cleaner, easier to re-use and harder to break. Is that not enough benefit for you?

    The whole point I wanted to make with my post is NOT to generalize all game development and say that certain practices are always bad. And yet you say that I'm trying to generalize?

    And what is this paradigm exactly? Unity provides a set of tools that solve some problems, but they don't solve them all. That's exactly why DOTS was created and why people write custom frameworks and systems for their games. I agree that you should 100% use everything that Unity provides out the box if it works well for you and not try to re-create the wheel.

    I've never mentioned any data structures specifically. You do realize that some games have multiplayer and some multiplayer games need backend server right? Even many single-player games use backend API's for all kinds of cloud features. That's also game development.
     
    Last edited: Mar 7, 2020
  2. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,337
    Just don't, honestly. People can use their own brain, draw their own conclusions and guide themselves using their own experiences. They do not need a shepherd or a prophet. This thread worked well so far for sharing information and viewpoints, and I feel that starting a response to it with "OMG I can't believe" and then throwing "lols" in responses can simply increase hostility and get it locked. It also doesn't really add any weight to your own argument.

    Gamedev represents a sufficiently different problem domain, and unity's insistence on using C# for it creates unique challenges/issues that set it apart from something like C#-based web backend. For example, use of immutability is desirable from theoretical perspective (as C# does not have const correctness that is present in C++), but not a good idea in unity will spam garbage for GC. That already sets it aside.

    So, yes. Some practices do not apply due to the differences in framework and problem domain. Both framework and domain matters. There are pieces in unity code that try to apply "standard practices" in overly rigorous manner and that reduces their usability. One of such systems is unity UI and the entire "EventSystem" thing which looks overdesigned to this day and feels like it is full of unnecessary complexity which could be reduced.

    Regarding DRY and SOLID, the difference between use and overuse is paper-thin, and from those two SOILD is much more likely to end up being overused.

    As for consequences... there will be consequences to not using a technique, but there will also be consequences for using it. There's no guarantee that either of the consequences will be of desirable kind.
     
    RecursiveFrog and hippocoder like this.
  3. hippocoder

    hippocoder

    Digital Ape Moderator

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    Before it gets heated I'll remind people why we even have templates or patterns and so on.

    Programmers typically have at least some of these problems often:
    • drat, this class is way too big
    • those methods look pretty generic, I could put them in a library
    • partial classes aren't just for generative code
    • etc
    Now usually all of these templates for good programming practises revolve around solving one single thing: "my code is too big".

    You might question that and think about coupling and so on but ultimately the main problem is a human being chopping code up so that they can consume it without choking or being rushed to hospital. The reason patterns emerge is because in a company, you need a house style (publishing) or coding practise (developer) and so on. Without consistency, it's really hard for the next programmer to make head or tail of it all, and this costs money.

    It's when developers fresh out of school or a recent job want to apply that to a whole game because they've seen it done (by a large body of programmers) and assume it'll work as-is for themselves (it will not, and they rarely ever finish a game).

    That's the long and short of it. But how do you know what pattern to use and where? The answer to this I find is to just design the code for today. If you keep doing what-if scenarios you'll end up with DI everywhere, SOILED trousers and a DRY mouth, everything based on events, with absolutely no gameplay code done a year later.

    So the real question is not so much "does the shoe fit" or "tool for the job" or "horses for courses" but ... which of these will be dominant in my project? It can't be all or none and if we keep switching between them we'll end up with smaller and more efficient code, but mapping that, coming back to that, will screw with your mind.

    (I've been there, I think we all have, so it's a cautionary tale).
     
    phobos2077 likes this.
  4. phobos2077

    phobos2077

    Joined:
    Feb 10, 2018
    Posts:
    350
    And yet people asks questions like "what is the benefits of using DI?", which for my experience sounds a bit like "what is the benefits of breathing air?". People enjoy different things in development. For some, designing clever complex architectures is more fun, for others - making the actual gameplay without any concern of the code quality. But in terms of economics, there's always the one most efficient way to develop software. It requires a lot of experience to find this balance, and most of the time it's not even possible, but to be successful you should search for compromise between cleaner code, faster code and faster development. Going either way full-on will be counter-productive in the long run.

    In some cases faster approach is good, e.g. for fast prototyping, testing things out or game jamming.
    In other cases you can over-engineer to your heart's content, again, if this code is just for learning for example.
    For real production you need balance.

    And yet, as I explained already, games require tools for designers and artists. These tools are closer to traditional development (like web) than actual run-time gameplay code. Games may have complex UIs not dissimilar to web apps. Games need web APIs which are not dissimilar to any other web API. Game dev is not JUST real-time gameplay mechanics. And different considerations can be applied to each field for the best results.


    You're right on that one. I was just surprised with where the thread was going. I felt that some people here didn't understood what DI is (which OP tried to explain) and yet started some kind of holy war against it before. Less experienced developers might read this thread and have the wrong impression that DI is some kind of arcane concept that's only useful for enterprise development. The OP wanted to address this misconception and I support the attempt. I really don't understand what can be argued about it?

    @hippocoder, have anyone argued about designing the code for the future? Your post makes a lot of sense, but I'm not sure how it's relevant to the argument about "what is DI", "what is the benefit of DI" and "when do I use it". As I said already, over-engineering is equally as bad as spaghetti code. Good coding practices does not mean over-complicated code. It means writing code that achieves your goal in the most efficient way.
     
    Last edited: Mar 7, 2020
  5. Hikiko66

    Hikiko66

    Joined:
    May 5, 2013
    Posts:
    1,303
    Are you saying that you don't use interfaces and generics?

    Artists shouldn't be changing code.
     
    phobos2077 likes this.
  6. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,337
    You shouldn't be surprised, and there was no holy war.

    Like I said, there is no need for a shepherd or shielding "Less experienced developers". They will learn from their mistakes.

    The thing is, people in general are susceptible for cargo cults and all sort of tribalistic behavior. They grab a concept, run around with it, and proclaim it to be the holy grail of the most efficient programming, and then they laugh at anyone who do things differently. "always use this or thing", "always do this", "never do that". The younger the developer is, the more likely they'll be to fall into one of those traps, and because of this it is necessary to determine whether a person promoting a technology is a fanatic, whether you're dealing with a cargo cult or they are sharing something they've learned after stepping on couple of landmines with disastrous consequences.

    Someone experienced will be less likely to feel strongly about another acronym and instead question its utility or explain why it is a good idea with concrete examples. And that is what has been happening in this thread so far - developers with history of having shipping a product, discussing an acronym concept and its utility.

    No holy wars involved.
     
    RecursiveFrog likes this.
  7. hippocoder

    hippocoder

    Digital Ape Moderator

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    You only design for the future with DI. The whole idea of injecting a dependency means you're thinking about where your data might end up rather than will end up.
     
  8. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,337
    And incidentally this goes against YAGNI principle.
    https://en.wikipedia.org/wiki/You_aren't_gonna_need_it

    Unused flexibility costs maintenance and development time. When flexibility is REQUIRED, you can introduce it by extracting abstract base from the original inflexible class and introducing polymorphic alternative.
     
    Martin_H and RecursiveFrog like this.
  9. phobos2077

    phobos2077

    Joined:
    Feb 10, 2018
    Posts:
    350
    I don't understand this. If you have a some kind of provider interface with two different implementations that are determined at runtime (via configuration, for example) and a consumer. Instead of having the construction logic inside the consumer, you just do it outside and inject as dependency to a consumer. Your code ends up cleaner and easy to work with. It's not a big deal to do, doesn't cost any considerable time and solves the problem you have at present.

    Simply passing an interface into a class so it can interact with it is also DI. Do you never use interfaces at all?

    Let's say I have some manager that handles creation of some "item" entities. These entities need to interact with some provider entity which has different life cycles and there's more than one for some reason (let's say a different provider for each "category" of "item"). To do this you inject references to these providers when creating "items". This is DI. How is this designing for the future?

    You just said yourself that one shouldn't be absolutist and yet you say "you only design for the future with DI". A bit of a contradiction.

    Regarding the more advanced concept of DI container frameworks, the benefit they give is the simplicity to use any service/provider in any class that is managed by such framework. It just removes the extra mental effort required to use DI extensively without singletons. You also have the ability to scope pieces of your code very easily in one place (when configuring your container). And by having lots of DI this way you gain the ability to unit-test much easier.

    Do I recommend to use these frameworks in most game projects? No, I do not. But for some projects the extra costs of integrating such framework and understanding how to use it might be outweighed by the benefits mentioned above.

    Agree 100% about unused flexibility (which was part of my argument from the beginning), but abstract classes are not the only solution (obviously).

    What exactly are you arguing against here?
     
    Last edited: Mar 7, 2020
  10. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,337
    And if you don't?

    Why would you use DI and pass provider as argument instead of directly accessing it?
     
    RecursiveFrog likes this.
  11. phobos2077

    phobos2077

    Joined:
    Feb 10, 2018
    Posts:
    350
    Then you don't need it in this case. Are you sure we have something to argue about? It seems like you just misunderstood some of my statements. I repeat again and again, it's the matter of using the best tool for the job. Knowing these tools and more importantly, when to use them is beneficial for your work.

    In my first post I outlined what is DI in my opinion, what it isn't and why it can be useful. I never said anything about never using direct construction of dependencies or never using singletons. I use all of the above.
     
  12. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,337
    For the record, I'm not arguing. I'm talking. As far as I'm aware there's no argument going on. People can talk without having an argument, right?

    The thing is, it is highly likely what hippo had in mind is that when you use dependency injection, is that you'll be using it in hope that there will be a need for polymorphic behavior. The provider becomes changeable. Maybe it will get turned into in an abstract class. Maybe there will be several different ones. The thing it, it didn't happen now, it might happen in the future, and that's why it is "for the future" behavior. And that's why it falls under YAGNI.

    If you check the thread on previous pages, then you would find a post by me where I said that the concept doesn't need a name, because it automatically emerge in practice when needed. It is called polymorphism and encapsulation.
    First you make a class that request external service directly.
    Then turns out there are several different instances of this class that can provide the service, so you turn it from direct access into parameter.
    Then a need for polymorphic behavior arises, and you you split the original class into an abstract base and two descendents, and the original "recipient" of the service now operates on the abstract base unaware of implementation details.

    This is basic polymorphism, and people were using it before the DI as a term got coined. As someone said "25 dollar name for 5 cent concept".
     
    hippocoder likes this.
  13. phobos2077

    phobos2077

    Joined:
    Feb 10, 2018
    Posts:
    350
    Doesn't make sense. Maybe I'm falling on a language barrier here, but how can you say that if you want to use some technique when it makes sense, then you're automatically wanting to use it when it doesn't? (when you're not solving current requirements, e.g. "designing for the future")

    I completely agree. My gripe was with the impression I got from reading previous posts that some people advice strongly for never using DI because it's suddenly some advanced thing that involves callbacks and events for some reason (?!). I respect your point of view that people must learn with their own mistakes, but it doesn't mean I can't have a desire to share my point of view based on my work experience with others. And maybe learn something that I might miss in an argument.
     
  14. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,337
    ...
    The way I see it,

    Your initial post in this thread created certain impression and that impression resulted in replies you got.
    The impression was of a "strongly opinionated DI proponent" (there was one before, long time ago). That was largely because of "OMG I can't believe" and "lol" in responses (I see that you edited them out).

    That's the reason you received the replies you got, as you created impression of believing that "DI is the only way" and that "one should always use it". You clarified the position afterwards.

    Like I said, there wasn't a holy war here in the first place. No offense intended.
     
  15. RecursiveFrog

    RecursiveFrog

    Joined:
    Mar 7, 2011
    Posts:
    350
    I think I can appreciate the point that the two approaches aren't one for one equivalent to each other. That said, does the distinction matter if you're concerned primarily about computation time and GC avoidance instead of developer time?

    When you bring up the convenience of creating closures in LINQ this to me reads as even more reason to avoid it because of implicit allocations that closures entail when the same effect can be achieved without creating them.

    To be sure, it's not always a bad approach to use LINQ. There are times in any game where the tradeoff pays off.

    That said, I have never seen an instance where anyone had to remove a complex handwritten iteration because it caused problems with framerates or memory pressure. I cannot say the same in reverse.
     
    phobos2077 likes this.
  16. hippocoder

    hippocoder

    Digital Ape Moderator

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    A lot of GC collections are smoothed out by the iterative garbage collector but such a thing would be dead on arrival on the Quest or other similar game where you cannot negotiate the framerate. For many, that's doing your job as a programmer.
     
  17. phobos2077

    phobos2077

    Joined:
    Feb 10, 2018
    Posts:
    350
    Well I appreciate you clarify this. But if you've read my first post carefully, you'll see that this impression is false. Because all I was trying to say is that game dev programming is not just writing gameplay logic and game projects differ very much from each other. That's why many tools that seem like "always useless" for some who never worked on a large project actually have place in some cases.

    My OMG was due to the apparent sentiment against using certain practices altogether in game development, which I've felt strongly about. (and the continued misunderstanding about the concept of DI from certain members despite the best efforts of OP and others...)
     
  18. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    20,189
    I was wondering how long it would be before someone made the asinine statement that someone hasn't worked on a large project if they've never used certain programming patterns.
     
    Martin_H and RecursiveFrog like this.
  19. phobos2077

    phobos2077

    Joined:
    Feb 10, 2018
    Posts:
    350
    That's brilliant of you to take that phrase out of context and add a different meaning to it, bravo. Now try to read that again.
     
  20. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    20,189
    I've already tried reading it multiple times and I can't interpret it in any other way than that.
     
  21. hippocoder

    hippocoder

    Digital Ape Moderator

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    Talk about code not people from this post on, everyone.
     
    Martin_H likes this.
  22. phobos2077

    phobos2077

    Joined:
    Feb 10, 2018
    Posts:
    350
    But can't you continue enjoying benefits of LINQ and avoid having GC-related issues by just being smart about when you use it? Like restricting to non-runtime code or event-driven UI code that doesn't happen every frame etc. Also not all games are on the very restrictive platforms, right?
     
  23. RecursiveFrog

    RecursiveFrog

    Joined:
    Mar 7, 2011
    Posts:
    350
    These are worthy goals, and correct use of patterns absolutely help with these.

    DI and IoC can help create code that is primarily based on the idea of composition over inheritance. In general that is a good thing, and definitely benefits projects of even moderate complexity.

    Thank you for explicitly stating that DI frameworks are not synonymous with DI as a concept. My own reaction to this subject is because I have often seen evangelism for DI and IoC used as a trojan horse to smuggle evangelism for DI frameworks into projects. *This* is where I tend to take a dim view, in software in general and in Unity specifically.

    To be clear, I have been involved in enterprise level development and have seen the difference before DI frameworks are introduced and after. Invariably the result was not to create more robust code, it was to create more obscure code with more obscure pathways, and with more difficult to author connections that were more difficult to debug both in terms of complie time problems as well as runtime configurations. The onboarding for the codebase also took more time because of this, at a time when onboarding new hands was critical. It soured me on the entire idea, especially since one of the biggest purported benefits, ease of writing test code, was never budgeted for by the project managers. We got all the con without an opportunity to even try to use the pros.

    I also watched Unity themselves attempt to implement a form of DI framework into what they assure us is the future of development and it was among the first things they removed. In their Unite conference talk about this they cited explicitly that it was both more difficult to write code using it and that the generated code from the annotations was so inefficient that it defeated the purpose of a high efficiency approach to game development.
     
    phobos2077 likes this.
  24. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,337
    Actually in addition to computation time I'm also concerned about readability, and Linq code is at least shorter. So situation where you start worrying about computation time and GC is likely to be happening last (in polish/boost phase).

    Regarding allocation, Linq is not hemorrhaging allocations, so basically you should get ONE allocation per link in the chain. So, one alloc per Where(), Select() and so on, and not per element of the generator. And it should be few bytes, because it encloses an anonymous class with yield statemachine. So it is mostly acceptable.

    Additionaly the whole generator thing makes certain things easier. For example, I don't recall what exactly I was doing with it, but I some point in the past few months I was writing some sort of expression parser, and I found that daisy chaining enumerables and yields produces a very convenient way of parsing things in streaming fashion. Because bottom level of the chain eats letters and produces tokens, the level above that consumes tokens and builds actual sentences/statements. It was fairly elegant and without class overloads or manual queues as containers.

    The real issue is C# lacking proper memory management, which makes deterministic lifetimes impossible, and that's why we worry about GC. C++ does not have such issue, and C++ now has yield and coroutines too. No extension methods though. Also, C++ has a ton of hidden "new" calls (and the memory is always released properly due to RAII pattern), and while those do not actually create garbage, they can also be a bottleneck.
     
    Ryiah, phobos2077 and RecursiveFrog like this.
  25. phobos2077

    phobos2077

    Joined:
    Feb 10, 2018
    Posts:
    350
    Well in Unity you now have native collections. They don't use normal memory allocation and garbage collection at all. Another possible plus side to the DOTS way of writing code.
     
  26. ippdev

    ippdev

    Joined:
    Feb 7, 2010
    Posts:
    3,800
    Artists do not want to change code. Game designers may want a mechanic to work differently and I would give them options via the Inspector. Then they do not have to bother me and await whilst I drop what I am doing to attend to them. Check this boolean to get this mechanic going this way and check that boolean and change this value to get another. I use what gets the job done and on over 100+ major projects I have rarely had to use more than Unity components to get a performant framework established to hand back to the clients.
     
  27. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,337
    That is not the same thing.

    C++ RAII pattern guarantees that when a variable goes out of scope, it is destroyed, its destructor is called, and all resources associated with it are freed. Standard practice for RAII resource handles is to ensure that constructor performs initialization, and destructor performs cleanup, that guarantees resource release.

    Regarding ECS... I haven't had a good chance to play with it but in a video I watched, I really disliked this part:
    upload_2020-3-8_1-42-21.png
    To be more specific, describing execution order through attributes.

    Really looks like a great way to make a mess.
     
    Ryiah and RecursiveFrog like this.
  28. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,594
    While indeed, it may be hard to track ordering attributes, having many dependent systems, these attributes are optional, not necessary.
    There are also other means, of keeping systems ordering.

    If is just for rendering, probably you wont need to worry too much about ordering.
    Multiplier and determinism, that where ordering things become mostly important.
     
    phobos2077 likes this.
  29. phobos2077

    phobos2077

    Joined:
    Feb 10, 2018
    Posts:
    350
    I'm well aware what RAII is. The point is there now an option to control when to allocate, even if it's a manual process. The closest thing to RAII in C# is IDisposable that might help you to achieve something vaguely similar.

    Well in a project I'm working on we do use these Update attributes. Whenever you want to see what systems are running in what order, you just look at the Entity Debugger. Haven't had too much trouble with this yet.

    Also you can create systems and groups manually any way you want. ECS is flexible in that regard, it's just a library that you can use in various ways.
     
  30. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,337
    I'm aware of IDisposable, and it indeed performs behavior similar to RAII. However, IDisposable cannot guarantee release of data from internal containers and is largely intended for file handles and the like.

    In C++, as you likely know, each variable acts like an IDisposable in a using block without having to spell it out.

    Basically, IDisposable or not, C# lacks ability to say "By my royal decree I command this memory block to be released here and now, so GC won't have to look for it later". That's the biggest issue with the memory management. The point of RAII in C++ is that it allows you to control when things die. That's the part with deterministic lifetimes.