Search Unity

To Singleton or not to Singleton. How can I unit test my code with all these Singletons?

Discussion in 'Scripting' started by Reshima, Jun 6, 2017.

  1. Reshima

    Reshima

    Joined:
    Dec 10, 2012
    Posts:
    51
    On Unity, it seems that most of the "example" code, tutorials, and documentation, encourage the use of Singletons. I don't want to debate if it's good or bad, I just want solutions to a few things, so I'm going straight to the point:

    1. How can I unit test my code with all these Singletons? Specifically, mocking Singletons such as NetworkManager (I really don't want to rely on network to test parts of my code when I run the build pipeline).

    2. Have you ever used Zenject? What's your experience with it? Does it hurt performance, readability? How long does it take for them to update the lib to latest/beta version in case if it ever breaks?

    I can understand why Unity uses Singletons (it's just easy and fast to make your game with them) and, although I try to be "puritan" to a few patterns (louse coupling and S***), I'm leaning towards using them as well. I'm just concerned about being able to unit test and being able to mock a few singletons. Overall, what's your experience so far? What have you used and what do you recommend?
     
  2. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    This is one of the biggest flaws of singletons.

    They're counter to unit testing.

    As for dependency injection...

    I personally don't use Zenject when working on games in unity. For one I find dependency injection overkill for my projects. Dependency injection is mostly useful for iterative development with a decently sized team of developers. Working on ever evolving enterprise software heavily benefits from dependency injection... oh, you have a data parser that needs to do some special work not originally considered when designing your structure, so the interface of it would need to be modified. Causing you to either refactor, or having to make a weird abstract class and then a hanging dependent class for this specific setup.

    Where as if you had been using dependency injection from the get go. You just, well... inject the new dependent code, so that it works. No refactoring!

    And of course, it's highly testable... since you can just inject your test/mock objects. Again, no refactoring to support testing.

    ...

    Thing is game design is a bit counter to this design.

    First and foremost, most game development has designers/artists/etc who are NOT programmers and need to be able to work within the game and set things up. Drop components onto objects, wiring some properties together, and go. Dependency injection introduces an extremely complicated object structure that even the most competent developers have a hard time wrapping their head around.

    And it's done in a setting where the benefits aren't exactly seen. Most games aren't long term iterative projects. I mean sure you iterate on design... but not in the way that dependency injection really shines.

    Of course, some can argue the counter. Often people who live and die by dependency injection. And I'm not trying to knock the concept of it... I just don't get as much of a benefit from it in a game development setting.

    And furthermore, if you're only attempting to use it because it's allows for easier unit testing... well, that's bad reason to be using dependency injection. The fact it allows for smoother unit testing is an auxiliary feature of the concept, not its core feature.

    ...

    If you're going to move forward with such an idea... well, give all of unities static classes and singletons clearer object identity.

    For example, I do this with the Time class and Random class:
    ITimeSupplier:
    https://github.com/lordofduct/spacepuppy-unity-framework/blob/master/SpacepuppyBase/ITimeSupplier.cs
    And implementation:
    https://github.com/lordofduct/spacepuppy-unity-framework/blob/master/SpacepuppyBase/SPTime.cs

    IRandom:
    https://github.com/lordofduct/spacepuppy-unity-framework/blob/master/SpacepuppyBase/IRandom.cs
    and implementation:
    https://github.com/lordofduct/space...lob/master/SpacepuppyBase/Utils/RandomUtil.cs

    This gives object identity to what are otherwise static interfaces.

    In the case of singletons, which already have object identity. Abstract that identity. Like with NetworkManager, you can just have a MockNetworkManager and a MyGameNetworkManager, both implementing NetworkManager. But you never actually access it by the NetworkManager.singleton interface, and rather inject your NetworkManager depending on context.
     
    Dean-Kuai and Reshima like this.
  3. kru

    kru

    Joined:
    Jan 19, 2013
    Posts:
    452
    Zenject is great, but buy-in is hard to acquire when you have "experienced" developers who can do the job "just fine" without it. I've stopped pushing dependency injection in unity until it matures.

    Instead, to counter the singleton craze, I've switched the team over to a global Service locator implementation. Services can be mocked far more easily than direct, static singleton access, and the code is only a little bit more verbose (not really). A bonus of using a service locator is that the users can use interfaces, instead of concrete classes. Another bonus is that lazy-loading is not allowed with my service locators because lazy loading is a tool of the devil.

    Code (csharp):
    1. // Instead of concrete, hard-to-mock access like this
    2. SomeSingleton.Instance.DoSomething(parameters);
    3.  
    4. // we can get access like so
    5. Services.Get<SomeSingleton>().DoSomething(parameters);
    6.  
    7. // or better, where SomeSingleton : ISomeInterface
    8. Services.Get<ISomeInterface>().DoSomething(parameters);
    9.  
    10. // we can swap out services for testing pretty easily, where TestingSingleton : ISomeInterface
    11. Services.RegisterService<ISomeInterface>(new TestingSingleton());
    12.  
     
    longchau1210, dnnkeeper, LMan and 4 others like this.
  4. Reshima

    Reshima

    Joined:
    Dec 10, 2012
    Posts:
    51
    Wow, thanks a lot. Using a service locator is great, an option I didn't think about. Thanks!
     
  5. forcepusher

    forcepusher

    Joined:
    Jun 25, 2012
    Posts:
    227
    Thumbs up for that design of service locator - this is literally all you need.
    I've used Zenject in some projects and I'm not a big fan of it because reflection magic ruins code navigation. More than that, Zenject cripples the performance. I had to come in and gut the MonoKernel from using Update(), LateUpdate() and FixedUpdate() in every object that has injections.
     
    Last edited: Apr 23, 2020
    MNNoxMortem likes this.
  6. MNNoxMortem

    MNNoxMortem

    Joined:
    Sep 11, 2016
    Posts:
    723
    We use Zenject in a small to medium sized/complex project and it is wonderful and very easy to work with. However, be careful to avoid the ProjectInstaller, as it is just another Singleton and it's installers are installed project wide - tests or not.

    That should not happen and I've not seen that happen yet, unless I am not understanding what you mean exactly.

    Any DI framework is overkill if your project is simple and everything it does for you, you can do by hand. A DI framework is valuable when doing it by hand is more tedious than using the framework:

    • Different contexts and bindings or complex binding rules
    • Bindings to the same type, but different targets depending on complex rules
    • Complex Resolve Getter patterns.
    Otherwise a simple ServiceLocator or Poor Man's DI do absolutely fine. That said: I do like DI frameworks and advice against using it to anyone who does not require one.

    Personally I prefer Poor Man's DI and constructor injection wherever possible over ServiceLocator patterns as the dependencies are better visible.
     
    Last edited: Jan 30, 2019
  7. RidgeWare

    RidgeWare

    Joined:
    Apr 12, 2018
    Posts:
    67
    If you're making a small-to-medium size project by yourself, and you know your code inside out, to be honest Singletons are fine. Just don't go too crazy with them.

    I have about 5 in my project.
     
    MNNoxMortem likes this.
  8. Cogniad

    Cogniad

    Joined:
    Feb 17, 2017
    Posts:
    15
    Are these singletons monobehaviours? Where do they get constructed?
    I'm very interested in the service locator pattern, and I'd love to see an example of it being implemented in Unity.
     
  9. forcepusher

    forcepusher

    Joined:
    Jun 25, 2012
    Posts:
    227
    @MNNoxMortem I've recently finished working on a pretty big contract project using Zenject.
    Zenject logic alone consumed about 30% run-time performance and 60% performance when loading scenes. I'm not even talking about the difficulties it introduced by ruining code navigation and conflicting with UNet HLAPI design.
    (Honestly, if I was in charge, I would've got rid of both UNet and Zenject since the start)

    Every object that has injections adds a MonoKernel component, which implements all update messages - Update(), LateUpdate() and FixedUpdate(). This severely cripples the run-time performance. Thankfully that's easy to fix.
    Loading performance is atrocious as well. It's extensively using Resources.FindObjectsOfTypeAll when resolving injections, and that was apparently marked as "todo" for years. See line 247 https://github.com/modesttree/Zenje...ns/Zenject/Source/Install/Contexts/Context.cs
    This library is not production ready, besides that - it's just bad.
     
    MNNoxMortem likes this.
  10. MNNoxMortem

    MNNoxMortem

    Joined:
    Sep 11, 2016
    Posts:
    723
    @forcepusher not to really disagree overall but to point out a few things, just for people who stumble upon this thread:
    • MonoKernel is not added for every object that has injections, it is added for every Context, e.g. Scene or ProjectContext (or others, which names are not to be spoken of). You can build pretty large projects with a single ProjectContext (which I would not recommend) or a single SceneContext (which is the better way to go, but depends on the lifetime of your scenes, how you handle multiple scenes, and has other implications).
    • 30% runtime performance / 60% performance "when loading scenes" depends pretty much on what your reference value is. We have seen less than 1% of our runtime performance spent in Zenject and did not even bother to ever evaluate the startup time as it did not show up on any of our treshholds.
      • ... but also had a single SceneContext
      • ... never used any GameObject Contexts (horrible thing. is this your the source of your many MonoKernels?)
      • ... did not require TickableManager (completly unecessary and can be implemented easily on your own)
      • ... did likely not use whatever you used to cripple your performance that much, but I seriously can not think of something that I would use in the first place that falls within that kind of category (many GameObjectContexts? Not sure exactly what happened there).
    Out of curiosity:
    • How does Zenject conflict with the UNet HLAPI design?
    • How does it affect code navigation after object construction/injection? At which part is zenject in between your calls except for object construction and injection?
    • Which part of Zenject does use your 30% and 60% and what is your reference values (100%)?
    Regarding the production readiness, I would also not recommened Zenject/Extenject for a production level project anymore.
    That beeing said, for small to medium sized projects (whatever that means for you hopefully might be in line what it means for me). you absolutely can use it without necessarily running into large problems - we did so for years and although I left the company I am still in contact with the lead developer there and they never ran into such problems yet. Does that mean I would recommend it? No.
     
    Last edited: Apr 23, 2020
    forcepusher and Twyker_gp like this.
  11. forcepusher

    forcepusher

    Joined:
    Jun 25, 2012
    Posts:
    227
    - We used lots of GameObject Contexts (none that was my idea, I was just playing along).
    - I've measured run-time performance hit using average frame time. To measure loading performance hit, I've summed up all the loading frame times and compared it to the time it took to execute Zenject logic. Mostly it was spiking with Resources.FindObjectsOfTypeAll and Reflection methods (over 3 seconds on a high-end desktop CPU).
    - Code navigation is crippled because you can't right click things and "Find Usages". Reflection-based magic method invocation turns the entire thing into spaghetti. I was completely lost for the first couple of weeks.
    - HLAPI has very specific networked object instantiation flow that can't be messed with without shooting yourself in the foot.
     
    Last edited: Apr 23, 2020
    MNNoxMortem likes this.
  12. RafaelAlcantara

    RafaelAlcantara

    Joined:
    Mar 30, 2016
    Posts:
    11
    I have a package on Asset Store that integrates the game with Facebook and PlayFab. There's a main class that deals with all methods from both services, which is a MonoBehaviour Singleton.
    Every time I need to send an update it gives me a deep sadness because I have no automated tests!

    I thought about separating the concerns creating interfaces (IFacebookHandler and IPlayFabHandler) and then injecting them. But since this class is a MonoBehaviour I cannot "new" it and pass the dependencies via constructor.

    Could you guys recommend me something that reduces code coupling and allows me to write unit tests?
     
    Last edited: May 12, 2020
  13. forcepusher

    forcepusher

    Joined:
    Jun 25, 2012
    Posts:
    227
    We decided to use a ServiceLocator for simple dependency graphs, like @kru suggested above.
    I'm still developing mine. It's not yet ready for production, but you'll get the idea to make your own.
    https://github.com/forcepusher/ServiceLocator
     
    Reshima and RafaelAlcantara like this.