Threads for samber

  1. 5

    I think I don’t understand the problem that DI is supposed to solve. I have a medium large service and the problem I have is that there are some mild dependency chains: A service depends on the HTTP client which depends on the logger, for example. It can be a little tricky to follow the initialization. I wish it were simpler. I think DI is supposed to help with this somehow, but I don’t see how DI would actually help. I would still have an annoyingly deep dependency chain, but instead of just being able to trace the function calls, it would all be hidden in a DI object. Why is that better than just having global state?

    1. 8

      DI makes dependencies explicit and enables you to swap them out. In practice this sort of flexibility isn’t really that frequently useful, with a major exception - unit tests. So I’d say the unequivocal benefit of DI is making testing easier.

      It’s also important to distinguish the concept of DI from DI frameworks. The idea of DI is quite simple - if object A needs to use object B, do so via some sort of dynamic reference that can be altered, instead of a global variable. This can be done by simply declaring B as a field of A, and that is in fact a very common way of doing DI in Go.

      When it comes to DI frameworks, my theory of their origin is that Java didn’t (and still doesn’t I suppose?) support named arguments, so when an object has more than (say) 3 dependencies, listing them in a constructor call becomes unreadable because you don’t know which object fulfills which dependency. So people invent this entire DI framework to make things more “readable” because at least it allows you to establish that relationship.

      Go doesn’t have named arguments either, but its struct syntax supports named fields. So either you’ll just construct the object using the struct syntax, like this (using the classical payment service example):

      ps := &PaymentService{CreditCardService: ccs, EmailService: es}
      

      Or define a struct that can be passed to a “constructor” function, like this:

      ps := NewPaymentService(
        &PaymentServiceDeps{CreditCardService: ccs, EmailService: es})
      

      This will scale to essentially an arbitrary number of dependencies - I’ve seen dependency list of 10 objects at work. You may have to pass over some of the dependency several times, which is annoying to write, but still reasonable readable. It’s not beautiful, but I find this to be much better than the unwarranted complexity of DI frameworks.

      1. 2

        DI makes dependencies explicit and enables you to swap them out.

        Right. The contrast here is to package level variables, which are not explicit and are harder to swap out. But just passing in the dependency as an argument is totally explicit and totally easy to swap out. So I again I don’t understand what a DI framework is buying. Explicitly wiring the dependencies makes understanding the code and swapping in mocks much easier at the cost of making it somewhat verbose to do the full production level initialization.

        1. 4

          I think we are in the same camp. I don’t think DI frameworks are particularly useful in Go, it’s just a relic from Java.

      2. 2

        Echo what the peer comment said “it is a managed global state”. DI in a world of microservices doesn’t make much sense to me. But if you have a monolith, it can help provides a convention for how one component interact with another. You can absolutely do this as passing in a context or dependency object everywhere, and codify a convention.

        Some conventions it helps (I will take Dagger2 as an example):

        1. If component A needs to notify component B to do something, DI has convention for component B to register some kind of notifier or listener that component A can call to decouple the hard dependency between these two (which some times can be circular if done naively);
        2. DI helps to set convention on when you accessing provided dependency, whether that is created once lazily, or created upon start, or created every time when access;
        3. DI provides rich visualization tools help you to query / visualize dependencies one component needs at runtime.
        1. 1

          It’s just a global state, but with DI it’s managed. Dependency chains are relatively easy to manage yourself, but at work we have a whole spider web. It’s a three headed server hosted in IIS, I don’t really know where calls come in for any of these so I wouldn’t know where to put the initialization of a new service. It’s so easy that the DI container does it for me.

          1. 7

            My usual take on DI frameworks is that they make everything much harder to debug in order to save a few tens of lines of code. You’re confirming the shit out of my biases here; I hope whoever is on call for this system drives a Ferrari.

            1. 2

              Idunno about Go, but my C# debugger passes through library code. It works fine. For our project I’d say a few ten thousands lines of code less, and making refactoring possible at all. Through DI we handle different types of lifetimes for the same classes, handle transactions, check privileges, and do easy mocking.

              Don’t use DI if you don’t need it, and you probably don’t.

            2. 1

              Yes, and DI helps a lot when you need to build tests using mock.

              1. 1

                I don’t understand this. I mock my service that depends on an http.Client which depends on the logger by just passing it an http.Client that returns what I want it to return. Why do I need something fancy to mock it? Mocking is really easy in Go. It’s easier than wiring up the real dependencies because you don’t have to go any deeper.

                1. 3

                  You have 3 services? That’s not a case for DI. Just did a quick count. We have 489 interfaces with many of those having 3 implementations, up to a few dozen, not counting testing. (holy shit btw, didn’t realize it was that many)

                  1. 1

                    I have about a dozen services, but none are more than three deep.

          1. 1

            @samber Is there any chance you might consider changing this package to use wire’s codegen approach? Do you at least provide any means of debugging the connections graph built by the framework?

            To any readers, I heartily ask you, please, please do yourself and your co-workers a favor and try hard to use wire instead. Wire allows for trivial inspection and verification of what its DI magic will generate, thus also enabling sane debugging. I still distinctly remember the trauma of having to wade through some monstrous Java DI XML, hopelessly trying to guess What Did The Framework Inject Where, and how to connect the dots. I shudder in terror thinking of a day I might be forced to do a similar kind of debugging in Go.

            1. 2

              Yes, I planned to code some debug helpers for visualizing graph.

              I don’t really like codegen approach, mostly because of tooling. Especially since generics are in the place…

              Could you please describe in an issue what you expect for inspection and debugging? It would be very nice to learn from your experience.

              1. 1

                Yes, I planned to code some debug helpers for visualizing graph.

                That could hopefully at least mitigate the issue.

                I don’t really like codegen approach, mostly because of tooling. Especially since generics are in the place…

                What specifically do you mean by tooling here?

                As for generics, I imagine wire could also be extended with them.

                Could you please describe in an issue what you expect for inspection and debugging? It would be very nice to learn from your experience.

                It was very long ago, but what I remember, is that I was unable to find out what types were resolved by the framework in the graph, why, how a change in the input “specs” would resolve/impact the tree, where the specific type I’m debugging is located in the graph.

                With wire’s codegen, all of that was quite easy:

                • after changing the input specs, I could trivially run the codegen and see the resulting graph and compare with what was there before (e.g. with git diff);
                • as for the rest, the “graph” generated by the codegen tol It’s Just Regular, Perfectly Readable Go Code All The Way, so all the normal tools work in it as usual (go to definition, find all references, etc. etc. etc.).
              1. 4

                The author mentions a performance loss. My benchmarks suggest the opposite.

                I often write badly performing code and it’s ok. Even a 30% CPU overhead is often cheaper than an unreadable code.

                In some cases, using FP patterns or data structure makes the code more readable, since it abstracts a lot of recurring things.

                We don’t have to use it everywhere. But sometimes it helps a lot.