I've never worked in a strict OOP paradigm, coming from Python and JavaScript mostly myself. I've been puzzled by his insistence that DI is important. He wants us to implement DI containers and take a "code to interfaces not implementations" approach. To me theres not really any value in this approach in JavaScript.
I don't know Go, but I get the impression this stuff is also not as valuable there.
My question is always "what does this get me and what does it cost me?". Outside of strict OOP paradigms like C# and Java, it's pretty unclear what the benefits are to me.
Which is probably true in some cases, but I find most of my code is pretty testable without it. Haven't run into any tests I've wanted to build where I couldn't.
I'm wondering if I have the terminology understood differently to other people, because to me, DI == IoC. DI Frameworks are build on top of the concept that dependencies will be injected, but doing it explicitly in your start-up code is the same thing to me.
This is the same conversation I had with the new hire I mentioned. And as the other comment here mentioned, it's JavaScript. You simply overwrite the object method with the mock at runtime of the test, then restore it after the test finishes running.
Most JS testing frameworks do this under the hood I believe, without any change in how you write your code. It's a fundamental difference in how JavaScript handles things versus something stricter like C#.
It's a dangerous footgun if you use it carelessly, of course. But a useful language feature when applied carefully.
My personal preference would still be to be explicit, to echew secret mutable state even in the tests, but that's just me. My personal baggage makes phrases like "then restore it after the test finishes" bring me out in a cold sweat ;)
I guess I sympathise with your colleague, but I agree it's not idiomatic, and not being idiomatic is not helpful in a team.
Basically, the logic not being inlined into the function is enough to provide this behavior in certain languages.
This is not true for other languages, which is why DI frameworks tend to be more useful there.
In go specifically, I just always throw the dependencies behind an interface and shove them into a struct. Then you just create a struct of mocks for your testing.
Easy refactoring of an interface/constructor? I've been using go for a while, and I decided to follow the advise of not using a DI for my project. Every time I refactor a constructor, it is a fun hunt to make the same changes everywhere I'm using that constructor.
This is also what DI amounts to in practice. Frameworks abstract over it in the name of DRY, but at the same time introduce all the downsides of frameworks.
Because what I think of dependency injection is extremely common in golang - they made interfaces satisfy structurally rather than nominally so that consumers could specify interfaces which anyone is free to satisfy. That the consumer owns the interface (packages shouldn't export interfaces for concretes they implement) is a pretty core tenet, and goes hand in hand with good dependency injection (IoC).
DI frameworks are extremely rare (and IME even more painful to use), because injecting your dependencies explicitly is straightforward. But injected they should be.
Furthermore, DI frameworks also usually have lifecycle management, which is quite handy in many cases.
You have a good point wrt lifecycle management, but I feel like that’s actually a separate class of problem.
For example, rather than have to read the GoDoc for every single constructor i'm supposed to use, I can just see how to use my DI framework to set up this client library and move on with my life.
That being said, DI always strikes me as a layer of abstraction that I don't need in my day to day. But I don't work somewhere at Google/Uber scale so YMMV.
IME at tech megacorps, DI just obfuscates and distracts, shifting the focus onto understanding the accidental complexity it introduces instead of dealing with the intrinsic complexity of the dependency relationships.
1. Complex lifecycle management, maybe. For example say you need to restart a set of go routines after loading a new configuration, without killing the process. I’m on the fence about this one.
2. When there is a combinatorially large number of components that need to be combined arbitrarily at runtime. The only examples I’ve seen for this in the wild are games and simulations using an ECS (entity-component-system) and queries to discover components that fit a certain criteria.
This is easy to see in frameworks like dagger, which compile time generate the boilerplate you could manually do.
And if everything is a singleton with no lifetime management, the framework doesn't buy you too much. The pattern, though, is kind of nice. I rarely have to question how a dependent section of code is linked to the one I'm at. (Contrast to python, where I don't know what is going to happen if I add that import...)
I felt that in my bones.
You don't need to create a "simple combination" of Singletons, Constructors, and Factories.
What does dependency injection give you that a simple combination of Singletons, Constructors, and Factories doesn't? I feel like the only thing you get is the ability to combine multiple independent dependency trees without having to make a sane structure. Kind of like, what redux does for state in JavaScript. It make things easier because it takes away some responsibility, but, in my eyes, that responsibility is an important one, and ignoring it makes code very hard to reason about.