Hacker News new | ask | show | jobs
by BobbyJo 1569 days ago
I've used dependency injection heavily in Java in previous jobs, and, later, spent a few years doing Golang at another job. I never missed it.

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.

11 comments

This is actually something I've been trying to talk about with a new hire at my job. We are running a nodejs stack, and he comes from a strict OOP C# background.

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.

It's not. It saves a few lines of code, with the tradeoff that you are handing some critical functionality--the wiring of dependencies--over to a black box. When things don't behave as you expect (which you might not initially notice), there is no code path to follow, you have to figure out what kind of magic is happening in the black box. It's just not worth the one-time cost of writing a few extra lines of code. This is also congruent with the aesthetic of Go in general.
I've found DI pretty useful when building frameworks, it gives you a sort of generic plugin system. Plus you can hide it from consumers if that makes sense. If you're just building applications I'm not sure it's worthwhile.
The argument I always hear is DI makes your code more testable.

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 think that's a side-effect of how code must be written to work with dependency injection, to make the injection actually possible, IMO it's not the DI itself that makes it testable.
Are you sure you are not mistaking dependency injection with dependency injection containers?
How do you test the code which determines when to fire the torpedoes, without actually firing the torpedoes every time you run the tests?
Just fire them and do a ROLLBACK
By using a mock?
How do you convince the when-to-fire-logic to act on a mock, if the torpedo implementation is not provided from outside it?

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.

> What does dependency injection give you that a simple combination of Singletons, Constructors, and Factories doesn't?

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.

Doesn't the compiler tell you exactly what needs to change?
It does, but having to go through every single error to fix the issues isn't that easy depending on the number of dependants.
Or the IDE. GoLand from JetBrains is very helpful there.
That's exactly what a Factory is for; you defer construction and pass around an object that knows how to create an instance. Then you're hunting at the root of your call tree to swap out a Factory instead of throughout your codebase to change a constructor call.

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.

A service still needs its own dependencies, thus forcing you to change its factories where needed. With a DI you re-define it in a single place.
That's true. However, the decoupling that occurs when refactoring one and not the other creates two different code structures, often with different levels of abstraction. I saw the same thing happening with a combination of react and redux, an it made debugging weird behaviors really, really, hard.
I came here to ask a similar question. I’ve spent most of my career writing C# where DI is prevalent. Over the Christmas holiday I picked up golang a bit. When I went to learn about DI for golang it seemed very counter to the principles of the language so I simply moved on. What am I missing out on?
Do you distinguish between dependency injection (frameworks) and general inversion of control?

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.

Because golang doesn't have constructors, you're forced to implement functions that mimic them yourself. And the language still doesn't prevent you from directly instantiating the struct yourself, meaning it is always possible to bypass the "constructor functions". This is quite terrible and opens up your code to errors.

Furthermore, DI frameworks also usually have lifecycle management, which is quite handy in many cases.

Fine, but that applies to even data structures and other domain types, which even in a more classically-OO language you wouldn’t pipe through a DI framework. I think DI as a code organization concept is great, but if I have to write a factory for every struct anyway then it’s not any harder to simply use those as a rule and define module boundaries sensibly as a way to determine which code should be using new vs the factory methods.

You have a good point wrt lifecycle management, but I feel like that’s actually a separate class of problem.

Please don't confuse DI with DI containers. With DI you still have all the responsibility.
Agree. I really can only imagine this being useful for an organization that publishes dozens of different libraries that a "product team" needs to use such as logging, database access, secret managers, etc. At that point, speaking the common language of a DI framework might be easier.

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.

There's nothing special about DI frameworks there. If a client library has some sane defaults preferred by the devs, then call a constructor that sets the defaults for you. You don't need DI for that.

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.

I mostly agree. I think there are two cases where using a DI framework make sense:

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.

Fx author (in part) here - replied in a separate comment. I mostly agree with you, but DI offers some ecosystem-wide benefits that may be useful in some environments.
I want to say thanks to your team for open sourcing this library so the community can discuss. It’s ambitious and to me feels like a right size approach to the problem, for all the heartache around DI, it seems like it’s different strokes for different folks but that doesn’t mean we should all be taking a collective shit on the engineers who are trying their best to provide a way to organize service complexity strategically. Well done.
Such as?
I feel the same way. IoC is a great pattern, but DI frameworks that try to magically sort out dependencies are just overkill especially in go imho
Just come out and say it: DI is an anti-pattern. No implementation of it that I'd ever use was doable without a good config file schema, and at that point I have to ask: why not just have a declarative build system in the first place as the modern generation of Devops tools does? DI just burdens and litters code unnecessarily with awareness of build files.
You, as many people here, are again mistaking dependency injection with dependency injection containers. You don't need containers for dependency injection, most codebases can just wire everything by hand without any problem.
What's an example of what you're talking about? Because the delegator pattern is not CI, and that's the only thing that's coming to mind that you might mean.
I think you are confusing dependency injecting with frameworks that facilitate it. You are probably using the former with constructors
What's the difference? Isn't the 'injection' part the part where you don't need to connect point A and point B?
Sorta. The pattern is having standard method of injecting dependencies. Having a framework just saves you having to do the actual injecting, manually.

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...)

> (Contrast to python, where I don't know what is going to happen if I add that import...)

I felt that in my bones.

>What does dependency injection give you that a simple combination of Singletons, Constructors, and Factories doesn't?

You don't need to create a "simple combination" of Singletons, Constructors, and Factories.

Don't you? In Java I still had to create the Singleton/Constructor/Factory etc. and register it with the framework as a provider of that type.