Dependency injection frameworks are a horrible idea.
They optimize for the wrong thing; making code easier to write but harder to read and edit.
Writing code is a one time cost whereas reading and editing it is an ongoing effort, so dependency injection frameworks are optimizing for the uncommon case at the expense of the common case.
Compile-time dependency injection frameworks are a big improvement over runtime dependency injection frameworks because you can at least read the generated code, but they are still a lot worse than manual dependency injection when it comes to editing code.
Thank you for this. I'd even say that dependency injection frameworks eliminate the most important thing of dependency injection: explicit dependencies.
Is still a class you can't instantiate without a correct instance of the given type.
(If you're talking about field injection, then I agree, that is the devil and must be purged with fire. Constructor injection or none.)
And when there's multiple instances of a given type available, a good framework refuses to guess. And I really like compile time DI for surfacing these errors far quicker than the traditional runtime approaches.
I could understand if the environment would depend on network loading speeds, and having to lazy load and cache/inject dependencies only when they are actually used (like in the Browser).
But with go as build environment this kind of contradicts everything that go is good at, so even after reading I'm asking myself for a legit use case for dependency injection in go (other than malware development or hijacking a loaded DLL of another program).
I recognise that DI frameworks may have a place when working with large teams and large applications, but otherwise I discourage them for a few reasons.
The prerequisite knowledge problem:
"Manual" DI requires knowing the language. Framework DI requires knowing the language and the framework. This alone makes it harder for new people to approach the codebase if they have not familiarity with the framework, even if they have mastered the language. There are already several DI frameworks for golang, but which ones are your new hires familiar with?
The "how do I do X" problem:
If you want to do something with your dependency graph, it's usually straightforward to express with plain code, but it might not be immediately obvious how to accomplish the same with the framework. For example, having multiple dependencies of the same type. With "manual" DI this is totally unremarkable. With wire, it goes against its mechanism and requires a minor workaround.
The implicit graph problem:
A DI framework the frees you from having to think much about the dependency graph may cause you to accidentally create a messy, illogical graph. It's easy to end up cornered by dependency cycles, for example. With manual DI, a flawed graph is more obvious early on because it's code you have to read and write. Explicit, not implicit.
It's a pretty big assumption to say DI makes code harder to read.
Personally, a decade and a half ago I felt like it was pretty easy to understand what I was working with. I'd hope observability has improved since, that there's been further improvement.
There's a horrible reputation for DI, people with old scars, that seems like there's no escaping from. But my workplaces had a great time using a variety of DI tools, again and again. We got great monorepo code sharing across numerous projects & easy to understand parts. My experience has been great. I want to keep asking, what would make folks reconsider their negative attitude?
For me, it would take at least one positive and significant experience.
I would probably need to work on a team where at least some of my teammates had some mastery of the framework, including knowing how to use it effectively in the long term.
This has not been my experience with dependency injection frameworks in C# at all. You know what happens in a large code base when you tell all of the developers to do manual dependency injection? They don’t do it and instantiate their dependencies in the constructor because it’s easier.
So deterministic instantiation then, which I as a (predominately C#, currently) developer actually prefer - I find it much easier to follow unfamiliar code. But whatever work for your team ...
I work in a large code base where we've told all of the developers to use dependency injection frameworks.
We are now using a mixture of 3 different dependency injection frameworks, manual dependency injection, and instantiating dependencies in the constructor.
I'd prefer what you describe, because I would still be allowed to fix it (change it to manual DI).
I usually write manual DI when I want to iterate on the system and make sure each part does what it's supposed to.
Then once it works I convert it to Joe Armstrong's gorilla-holding-the-banana-and-the-whole-jungle (framework DI) so it gets past code review, and I hope to never touch it again.
DI frameworks always make me deal with way more of the system than I want to.
I want to just be able to 'new' any class in the system, and start playing with it under test.
When I try to test a Parser class under a framework DI, it ends up instantiating the most fiddly bits of unrelated crap. I get null pointers in the database config, which had nothing to do with what I wanted.
It can be controlled, by adding even more annotations and mocking frameworks. But I'd rather take out annotations rather than put them in.
Not sure if I agree they’re horrible, but I think you should put it off until you really need it.
I disagree that they optimize for write: a DI is quite easy to real, but hard to debug. Adding @Inject(databaseHandle) makes it pretty clear you’re getting a handle to your database. That’s easy to read. When the database handle doesn’t work, suddenly it’s hard to debug.
I also disagree with others that it’s harder to learn. You can barely know a language and understand that a library is auto-magically injecting that database into that variable. New users can pretty quic Write code that relies on existing injections or copies other examples and it’ll usually work without flaw.
The problem with DI is when you exit the happy path: it’s really hard to diagnose what is failing, and the implicit ordering make it hard to figure out quickly. This could be worth it to reduce a ton of boilerplate on a large and mature monolith, or to speed up an agile team, but it could just give you one more thing to endlessly tweak and change.
I despise "magic" but DI has worked incredibly well for me for years. It almost always "just works". Even my homebrew DI framework I coded up in a couple of days worked extremely well. They're not that complicated. If you only have one instance of each object type it works nicely. If you need multiple then you need a bit of magic to pick one but even that isn't bad.
I disagree that they make code harder to read. It's the opposite in fact in my experience, especially with the prevalence of mature IDEs that allow you to navigate the graph and injection points. It's quite a nice experience.
You don't "need" it in golang, but for me, it encourages nice code separation in a way that makes it really easy to write tests for my components. You tend to think a lot more about how code is organized in order to make it as easy as possible to unit test. I also like the service start/stop functionality.
In my experience, having used wire and uberfx (while swe at uber), they always felt like a clunky system to ultimately just have something automatically pass a few variables to a constructor.
> felt like a clunky system to ultimately just have something automatically pass a few variables to a constructor
That's exactly what it is. The benefit being that if you go with that pattern, now you have this encouraged separation of code which enables thing to be tested a lot more easily.
In golang, it is super easy to just use the foo.NewFoo() pattern all over your code, but when you do that, you lose the ability to test things easily because you're no longer passing things around in constructors.
I'm just beginning to think this is a terrible idea.
A few years ago, I was like, Spring yeah, wow, complex is better but then Guice looked even better with less boilerplate. They just moved things from Java to XML and then back. That sounded great.
(On an aside, We seem to live in the YAML era of microservices. I'd like to say another complexity wave of this sort will likely happen 15 years from now.)
Now I'm like, this wiring is all unnecessary. You're writing more process code than configuration code and it doesn't improve quality of life or extensibility.
It gives more work to most developers and serves as process fuel to the buzzword driven world we live in.
If there was a way of describing jumping a shark, this would fit the description.
Thanks for the direct link, medium wanted me to sign in.
Looking over the example code, my gut reaction is "don't do this, this is an anti pattern and bad."
They replaced a simple, linear, hand written, and easily understandable few lines of code with code generation and indirection.
The idiomatic way to solve this problem is to have struct properties and/or a constructor that takes interfaces. In the concrete implementation, you pass the real dependencies. In the tests, you pass fakes (test doubles that match the interface). It requires no code gen.
My team is spinning up a new http service. It took me less than a morning to refactor the app from gross globals to creating server instances that can be unit tested in parallel. There are three core dependencies and the unit testing is super easy. On any dependency, set its fake to have an error. Validate the error is handled the way you want. Done.
Dependency injection is solved for Go with interfaces. Also note the Go Proverb and keep interfaces small.
I don't really use dependency injection. I once had tried to penetrate a Java program that used dependency injection using Guice to initialise a bunch of things/dependencies, and I found it abstracted away the control flow that I really couldn't figure out how to modify things at initialization time. I eventually abandoned the pursuit because I was out of time and had to work on other things.
But looking at this example, I don't really see the benefit in the example. You save 2 lines of code, but now you have all these additional build steps you need to maintain? I'm trying to honestly see the value of dependency injection; admittedly in Go it's a lot easier to follow and understand these frameworks compared to Java, but I'm not really understanding how it's saving time or helping me. The other issue is I have to understand the concept of dependency injection and while simple at its core, a quick google gives you examples and tutorials in the most complicated framework.
I really dislike these abstractions that hijack the flow of initialising your program; I'd rather just do everything manually. Maybe I've not met a codebase where I need to manage say 100 dependencies, but even then, I'd probably just prefer straight-line obvious code on individually setting them up and initialising the program.
Edit: I realise I actually like manual DI, I just despise the frameworks and found them more irritating than useful. Dependency injection has never been an easy topic for me to understand.
> admittedly in Go it's a lot easier to follow and understand these frameworks compared to Java
That's because the golang ones are doing lesser work. There's nothing inherent in Java that makes such frameworks in it more difficult to follow. As a matter of fact, it could be the opposite with the presence of mature IDEs that allow you to navigate to the injection point and see the relevant graph.
I recall working on a golang project that did not use DI, and it was a total mess to read and write.
36 comments
[ 1.1 ms ] story [ 83.5 ms ] threadThey optimize for the wrong thing; making code easier to write but harder to read and edit.
Writing code is a one time cost whereas reading and editing it is an ongoing effort, so dependency injection frameworks are optimizing for the uncommon case at the expense of the common case.
Compile-time dependency injection frameworks are a big improvement over runtime dependency injection frameworks because you can at least read the generated code, but they are still a lot worse than manual dependency injection when it comes to editing code.
(If you're talking about field injection, then I agree, that is the devil and must be purged with fire. Constructor injection or none.)
And when there's multiple instances of a given type available, a good framework refuses to guess. And I really like compile time DI for surfacing these errors far quicker than the traditional runtime approaches.
It's not that clear with Foo(SomeType). What I usually deal with is:
Now I just want to test that AuthService accepts/rejects a user based on whether they're in the system or not. I want to do something like: > And when there's multiple instances of a given type...(There should always be (in the case that you're writing a test for something))
> a good framework refuses to guess
I do too! I want to manually wire the classes inside main() in the usual case, and manually wire (different combinations) of classes in the tests.
But with go as build environment this kind of contradicts everything that go is good at, so even after reading I'm asking myself for a legit use case for dependency injection in go (other than malware development or hijacking a loaded DLL of another program).
The prerequisite knowledge problem:
"Manual" DI requires knowing the language. Framework DI requires knowing the language and the framework. This alone makes it harder for new people to approach the codebase if they have not familiarity with the framework, even if they have mastered the language. There are already several DI frameworks for golang, but which ones are your new hires familiar with?
The "how do I do X" problem:
If you want to do something with your dependency graph, it's usually straightforward to express with plain code, but it might not be immediately obvious how to accomplish the same with the framework. For example, having multiple dependencies of the same type. With "manual" DI this is totally unremarkable. With wire, it goes against its mechanism and requires a minor workaround.
The implicit graph problem:
A DI framework the frees you from having to think much about the dependency graph may cause you to accidentally create a messy, illogical graph. It's easy to end up cornered by dependency cycles, for example. With manual DI, a flawed graph is more obvious early on because it's code you have to read and write. Explicit, not implicit.
Personally, a decade and a half ago I felt like it was pretty easy to understand what I was working with. I'd hope observability has improved since, that there's been further improvement.
There's a horrible reputation for DI, people with old scars, that seems like there's no escaping from. But my workplaces had a great time using a variety of DI tools, again and again. We got great monorepo code sharing across numerous projects & easy to understand parts. My experience has been great. I want to keep asking, what would make folks reconsider their negative attitude?
I would probably need to work on a team where at least some of my teammates had some mastery of the framework, including knowing how to use it effectively in the long term.
We are now using a mixture of 3 different dependency injection frameworks, manual dependency injection, and instantiating dependencies in the constructor.
I usually write manual DI when I want to iterate on the system and make sure each part does what it's supposed to.
Then once it works I convert it to Joe Armstrong's gorilla-holding-the-banana-and-the-whole-jungle (framework DI) so it gets past code review, and I hope to never touch it again.
No, they optimise for making code easier to test.
Dependency injection makes code easier to test, and I’m a big fan of dependency injection.
What I’m opposed to is the frameworks that automate (and often hide) the construction of the dependencies.
As someone else said in this thread “explicit is better than implicit”.
I want to just be able to 'new' any class in the system, and start playing with it under test.
When I try to test a Parser class under a framework DI, it ends up instantiating the most fiddly bits of unrelated crap. I get null pointers in the database config, which had nothing to do with what I wanted.
It can be controlled, by adding even more annotations and mocking frameworks. But I'd rather take out annotations rather than put them in.
I disagree that they optimize for write: a DI is quite easy to real, but hard to debug. Adding @Inject(databaseHandle) makes it pretty clear you’re getting a handle to your database. That’s easy to read. When the database handle doesn’t work, suddenly it’s hard to debug.
I also disagree with others that it’s harder to learn. You can barely know a language and understand that a library is auto-magically injecting that database into that variable. New users can pretty quic Write code that relies on existing injections or copies other examples and it’ll usually work without flaw.
The problem with DI is when you exit the happy path: it’s really hard to diagnose what is failing, and the implicit ordering make it hard to figure out quickly. This could be worth it to reduce a ton of boilerplate on a large and mature monolith, or to speed up an agile team, but it could just give you one more thing to endlessly tweak and change.
You don't "need" it in golang, but for me, it encourages nice code separation in a way that makes it really easy to write tests for my components. You tend to think a lot more about how code is organized in order to make it as easy as possible to unit test. I also like the service start/stop functionality.
[0] https://github.com/uber-go/fx
That's exactly what it is. The benefit being that if you go with that pattern, now you have this encouraged separation of code which enables thing to be tested a lot more easily.
In golang, it is super easy to just use the foo.NewFoo() pattern all over your code, but when you do that, you lose the ability to test things easily because you're no longer passing things around in constructors.
A few years ago, I was like, Spring yeah, wow, complex is better but then Guice looked even better with less boilerplate. They just moved things from Java to XML and then back. That sounded great.
(On an aside, We seem to live in the YAML era of microservices. I'd like to say another complexity wave of this sort will likely happen 15 years from now.)
Now I'm like, this wiring is all unnecessary. You're writing more process code than configuration code and it doesn't improve quality of life or extensibility.
It gives more work to most developers and serves as process fuel to the buzzword driven world we live in.
If there was a way of describing jumping a shark, this would fit the description.
Looking over the example code, my gut reaction is "don't do this, this is an anti pattern and bad."
They replaced a simple, linear, hand written, and easily understandable few lines of code with code generation and indirection.
The idiomatic way to solve this problem is to have struct properties and/or a constructor that takes interfaces. In the concrete implementation, you pass the real dependencies. In the tests, you pass fakes (test doubles that match the interface). It requires no code gen.
My team is spinning up a new http service. It took me less than a morning to refactor the app from gross globals to creating server instances that can be unit tested in parallel. There are three core dependencies and the unit testing is super easy. On any dependency, set its fake to have an error. Validate the error is handled the way you want. Done.
Dependency injection is solved for Go with interfaces. Also note the Go Proverb and keep interfaces small.
But looking at this example, I don't really see the benefit in the example. You save 2 lines of code, but now you have all these additional build steps you need to maintain? I'm trying to honestly see the value of dependency injection; admittedly in Go it's a lot easier to follow and understand these frameworks compared to Java, but I'm not really understanding how it's saving time or helping me. The other issue is I have to understand the concept of dependency injection and while simple at its core, a quick google gives you examples and tutorials in the most complicated framework.
I really dislike these abstractions that hijack the flow of initialising your program; I'd rather just do everything manually. Maybe I've not met a codebase where I need to manage say 100 dependencies, but even then, I'd probably just prefer straight-line obvious code on individually setting them up and initialising the program.
Edit: I realise I actually like manual DI, I just despise the frameworks and found them more irritating than useful. Dependency injection has never been an easy topic for me to understand.
That's because the golang ones are doing lesser work. There's nothing inherent in Java that makes such frameworks in it more difficult to follow. As a matter of fact, it could be the opposite with the presence of mature IDEs that allow you to navigate to the injection point and see the relevant graph.
I recall working on a golang project that did not use DI, and it was a total mess to read and write.
For me, it is like that they invented a solution to an imaginary problem they created themselves.
Also to be noted that this kind of thing could be needed for go as it is a shitty language that tries to do object things without having objects.