Readit News logoReadit News
surgical_fire · 9 months ago
Dependency Injection has a fancy name that makes some developers uncomfortable, but it's really just all about making the code easier to test. Basically everything that the class depends upon has to be informed during construction.

This can be done manually, but becomes a chore super fast - and will be a very annoying thing to maintain as soon as you change the constructor of something widely used in your project to accept a new parameter.

Frameworks typically just streamline this process, and offers some flexibility at times - for example, when you happen to have different implementations of the same thing. I find it funny that people rally against those Frameworks so often.

To make things more concrete, let's say you have a method that gets the current date, and has some logic there (for example, it checks if today is EOM to do something). In Java, you could do `Instance.now()` to do this.

This will be a pain in the ass to test, you might need to test, for example a date when there's a DST change, or February 28th in a leap year, etc. With DI you can instead inject an `InstantSource` to your code, and on testing you can just mock the dependency to have a predictable date on each test.

loevborg · 9 months ago
Why is it a pain to inject dependencies manually? I think this is because people assume for some reason that a class isn't allowed to instantiate its own dependencies.

If you lift that assumption, the problem kind of goes away. James Shore calls this Parameterless Instantiation: https://www.jamesshore.com/v2/projects/nullables/testing-wit...

It means that in most cases, you just call a static factory method like create() rather than the constructor. This method will default to using Instance.now() but also gives you a way to provide your own now() for tests (or other non-standard situations).

At the top of the call stack, you call App.create() and boom - you have a whole tree of dependencies.

pdpi · 9 months ago
If the class instantiates its own dependencies then, by definition, you're not injecting those dependencies, so you're not doing dependency injection at all!
loevborg · 9 months ago
In other words, despite all the noise to the contrary, hard-coded dependencies are fine.

James explains this a lot better than I can: https://www.jamesshore.com/v2/blog/2023/the-problem-with-dep...

ptx · 9 months ago
> James Shore calls this Parameterless Instantiation

Mark Seemann calls it the Constrained Construction anti-pattern: https://blog.ploeh.dk/2011/04/27/Providerisnotapattern/#4c7b...

bluGill · 9 months ago
many of the things I dependeon are shared services where there should only be one instance. Singleton means a global variable. a di framework lets me have on without a global - meaning if I need a second service I can do it with just a change to how the di works.

there is no right answer of course. Time should be a global so that all timers/clocks advance in lock step. I hav a complex fake time system that allows my tests to advance minutes at a time without waiting on the wall clock. (If you deal with relativity this may not work - for everyone else I encourage it)

almostdeadguy · 9 months ago
It often is not enough. Singletons frequently need to be provided to things (a connection pool, etc.).
IshKebab · 9 months ago
> Dependency Injection has a fancy name that makes some developers uncomfortable, but it's really just all about making the code easier to test.

It's not just a fancy name. I'd argue it's a confusing name. The "$25 name for a 5c concept" quote is brilliant. The name makes it sound like it's some super complicated thing that is difficult to learn, which makes it harder to understand. I would say "dynamic programming" suffers the same problem. Maybe "monads".

How about we rename it? "Generic dependencies" or "Non hard-coded dependencies" or even "dependency parameters"?

layer8 · 9 months ago
The name is confusing because originally [0] it was exclusively about dynamic injection by frameworks. Somehow it morphed into just meaning suitable constructor parameters, at least in some language communities.

I like “dependency parameters”. Dependencies in that sense are usually what is called service objects, so “service parameters” might be even clearer.

[0] https://www.martinfowler.com/articles/injection.html#FormsOf...

akoboldfrying · 9 months ago
I third "dependency parameters". It concisely describes what the actual thing is, and isn't intimidating.

And yes, even though some languages/frameworks allow deps to be "passed in" via mechanisms that aren't technically parameters (like member variables that are just somehow magically initialised due to annotations), doing that only obfuscates control flow and should be avoided IMHO.

ulrikrasmussen · 9 months ago
I think the "injection" is referring to a particular style of frameworks which rely on breaking language abstractions and modifying otherwise private fields to provide dependencies at runtime. I really, really, REALLY dislike that approach and therefore also dislike the name "dependency injection".

Why not call it "dependency resolution"? The only problem frameworks solve is to connect a provider of thing X with all users of thing X, possibly repeating this process until all dependencies have been resolved. It also makes it more clear that this process is not about initialization and lifecycling, it is only about connecting interfaces to implementations and instantiating them.

Edit: The only DI framework I have used and actually kind of like is Dagger 2, which resolves all dependencies at compile time. It also allows a style of use where implementation code does not have to know about the framework at all - all dependency resolution modules can be written separately.

All other runtime DI frameworks I have used I have hated with a passion because they add so much cognitive overhead by imposing complex lifecycle constraints. Your objects are not initialized when the constructor has finished running because you have to somehow wait for the DI framework to diddle the bits, and good luck debugging when this doesn't work as expected.

kevmo314 · 9 months ago
“Global variables that can pass code review”
thiht · 9 months ago
Or don’t use a DI framework, and DI just becomes a fancy name for "creating instances" and "passing parameters". That’s what we do in Go and there’s no way I would EVER use a DI framework again. I’d rather be unemployed than work with Spring.
Gibbon1 · 9 months ago
I'm a small brained primate but when I get down to what dependency injection is doing it's like my firmware written in C setting a couple of function pointers in a radio handler's struct to tell it which SPI bus and DIO's to use. Which seems trivially okay.
surgical_fire · 9 months ago
[flagged]
jen20 · 9 months ago
If it becomes a chore to instantiate your dependency tree with `new` or whatever in the root of your app, it's a good indication that the dependency tree _is too complex_. You _should_ feel the pain of that, to align incentives for simplification.

Using an IoC container is endemic in the Java ecosystem, but unheard of in the Go ecosystem - and it's not hard to see which of them favours simplicity!

surgical_fire · 9 months ago
> instantiate your dependency tree with `new` or whatever in the root of your app

Then you are doing dependency injection, just replacing the benefits of a proper framework by instantiating everything at once the root of your app.

Whatever floats your boat, I guess. Thankfully I don't need to work on your code.

kazinator · 9 months ago
It's not just about testing. When any code constructs its own object, and that object is actually an abstraction of which we have many implementations, that code becomes stupidly inflexible.

For instance, some code which prints stuff, but doesn't take the output stream as a parameter, instead hard-coding to a standard output stream.

That leaves fewer options for testing also, as a secondary problem.

layer8 · 9 months ago
That’s just parameterization, though. It’s overkill to call every parameter (or even most parameters) a “dependency”.
layer8 · 9 months ago
> Frameworks typically just streamline this process, and offers some flexibility at times - for example, when you happen to have different implementations of the same thing.

The very purpose of DI is to allow using a different implementation of the same thing in the first place. You shouldn’t need a framework to achieve that. And my personal experience happens to match that.

ffsm8 · 9 months ago
You're talking from the perspective of Java, which has been designed from the ground up with dependency injection in mind.

Dependency injection is the inversion of control pattern at the heart, which is something like oxygen to a Java dev.

In other languages, these issues are solved differently. From my perspective as someone whoes day job has been roughly 60+% Java for over 10 yrs now... I think I agree with the central message of the article. Unless you're currently in Java world, you're probably better off without it.

These patterns work and will on paper reduce complexity - but if comes at the cost of a massively increased mental overhead if you actually need to address a bug that touches more then a miniscule amount of code.

/Edit: and I'd like to mention that the article actually only dislikes the frameworks, not the pattern itself

mattmanser · 9 months ago
DI wasn't around when Java (or .Net) came out. DI is a fairly new thing too, relatively speaking, like after ORMs and anonymous methods. Like after Java 7 I think. Or maybe 8? Not a Java person myself.

I know in .net, it was only really the switch to .net core where it became an integral part of the frameworks. In MVC 5 you had to add a third party DI container.

So how can it have been designed for it from the ground up?

In fact, if you're saying 10 years, that's roughly when DI became popular.

You're wrong about other languages not needing it, yes statically typed languagess need it for unit testing, but you don't seem to realize that from a practical perspective DI solves a lot of the problems around request lifetimes too. And from an architectural context it solves a lot of the problem of how to stop bad developers overly coupling their services.

Before DI people often used static methods, so you'd have a real mess of heavily interdependent services. It can still happen now but.its nowhere near as bad as the mess of programming in the 2000s.

DI helped reduce coupling and spaghetti code.

DI also forces you to 'declare' your dependencies, so it's easy to see when a class has got out of control.

Edit: I could keep on adding, but one final thing. Java and .Net are actually quite cumbersome to use DI, and Go is actually easier. Because Go has implicit interfaces, but older languages don't and it would really help reduce boiler plate DI code.

A lot of interfaces in Java/C# only exist to allow DI tow work, and are otherwise a pointless waste of time/code.

Deleted Comment

zzo38computer · 9 months ago
> but it's really just all about making the code easier to test. Basically everything that the class depends upon has to be informed during construction.

It is useful for more than testing (although, depending on the kind of tests being made, it might not always be useful for all kind of tests). It also allows you to avoid a program having too many dependencies that you might not need (although this can also cause a problem, it could perhaps be avoided by providing optional dependencies, and macros (or whatever other facility is appropriate in the programming language you are using) to use them), and allows more easily for the caller to specify customized methods for some things (which is useful in many programs, e.g. if you want customized X.509 certificate validation in a program, or customized handling of displaying/requesting/manipulation of text, or use of a virtual file system).

In a C program, you can use a FILE object for I/O. Instead of using fopen or standard I/O, a library could accept a FILE object that you had previously provided, which might or might not be an actual file, so it does not need to deal with file names.

> This will be a pain in the ass to test, you might need to test, for example a date when there's a DST change, or February 28th in a leap year, etc.

I think that better operating system design with capability-based security would help with this and other problems, although having dependency injection can also be helpful for other purposes too.

Capability-based security is useful for many things. Not only it helps with testing, but also helps to work around a problem if a program does not work properly on a leap year, you can tell that specific program that the current date is actually a different date, and it can also be used for security, etc. (With my idea, it also allows a program to execute in a deterministic way, which also helps with testing and other things, including resist fingerprinting.)

shadowgovt · 9 months ago
I frequently find DI pattern to show up in Java... But I also frequently find that Java gives me all the handcuffs of systems languages with few of the benefits of more flexible duck-typing languages.

If you can't monkey-patch the getDate function with a mock in a testing context because your language won't allow it, that's a language smell, not a pattern smell.

akoboldfrying · 9 months ago
> If you can't monkey-patch the getDate function with a mock in a testing context because your language won't allow it, that's a language smell, not a pattern smell.

Not so fast. Constraints like "no monkeypatching allowed" are part of what make it possible to reason about code at an abstract level, i.e., without having to understand in detail every control path that could possibly have run before. Allowing monkeypatching at the language level means discarding that useful reasoning tool.

I'm not saying that "no monkeypatching allowed" is always ideal, but it is a tradeoff.

(Consider why so many languages have something like a "const" modifier, which purely restricts what you can do with the object in question. The restriction reduces what you can do with it, but increases what you know about it.)

surgical_fire · 9 months ago
> If you can't monkey-patch the getDate function with a mock in a testing context because your language won't allow it, that's a language smell, not a pattern smell.

Of course you can do it in Java. But it is widely considered poor practice, for good reason, and is generally avoided.

arjvik · 9 months ago
You CAN in fact monkeypatch getDate - look at a Mockito add-on known as PowerMockito! While it's impossible to mock it out in the normal JVM "happy path," the JVM is powerful enough to let you mess with classloading and edit the bytecode at load-time to mock out even system classes.

(Disclaimer: have not used PowerMockito in ages, am not confident it works with the new module system.)

JyB · 9 months ago
Regarding 'frameworks'. Golang already ships with a framework natively because of the design of the language. Therefore the point is moot in that specific context. Hence the post.
lemagedurage · 9 months ago
Another downside of DI is how it breaks code navigation in IDEs. Without DI, I can easily navigate from an instance to where it's constructed, but with DI this becomes detached. This variable implements Foo, but which implementation is it?
rightbyte · 9 months ago
Ye debugability and grepability is terrible.

DI seems like some sort of job security by obscurity.

diggan · 9 months ago
If your IDE starts to decide how you code and what kind of architecture/design you can use, I kind of feel like the IDE is becoming something more than just an IDE and probably you should try to find something else. But I mainly program in vim/nvim so maybe it's just par for the course with IDEs and I don't know what I'm talking about.
hx8 · 9 months ago
Are you not using an LSP with your text editor? If you are then you'll run into the same issue because it's the underlying technology. If you aren't using an LSP then you're probably leaving some workflow efficiency on the table.
surgical_fire · 9 months ago
In IntelliJ at least this is a non-issue.
Kiro · 9 months ago
How?

Deleted Comment

grugagag · 9 months ago
This is what I hate most about DI as well and when I told some other devs about this pet peave of mine they were looking at me like I had 2 head or something.
wiseowise · 9 months ago
Which language? Android Studio, for example, allows you to navigate to Hilt injection points.
wordofx · 9 months ago
Not an issue in C#
zo1 · 9 months ago
Definitely still an issue in C#. C# devs are just comfortable with the way it is because they don't know better and are held hostage. Everything in C# world after a certain size will involve IOC/DI and the entire ecosystem of frameworks that has co-evolved with it.

The issues are still there. You can't just "go to definition" of the class being injected into yours, even if there is only one. You get the Interface you expect (because hey you have to depend on Interfaces because of something something unit-testing), and then see what implements that interface. And no, it will not just point to your single implementation, it'll find the test implementation too.

But where that "thing" gets instantiated is still a mystery and depends on config-file configured life-cycles, the bootstrapping of your application, whether the dependency gets loaded from a DLL, etc. It's black-box elephants all the way to the start of your application. And all that you see at the start is something vague like: var myApp = MyDIFramework.getInstance(MyAppClass); Your constructors, and where they get called from is in a never-ending abyss of thick and unreadable framework code that is miles away from your actual app. Sacrificed at the alter of job-creation, unit-testing and evangelist's talk-resume padding.

rootsofallevil · 9 months ago
I haven't really done any c# for 5+ years. What has changed?

I remember trying to effectively reverse-engineer a codebase (code available but nobody knew how it worked) with a lot of DI and it was fairly painful.

Maybe it was possible back then and I just didn't know how ¯\_(ツ)_/¯

asp_hornet · 9 months ago
C# code bases are all about ruining code navigation with autofac and mediatr
danpalmer · 9 months ago
"Dependency injection is too complicated, look at this one straight-line implementation" is not exactly a fair argument.

The whole point of DI is that when you can't just write that straight-line implementation it becomes easier, not harder. What if I've got 20 different handlers, each of which need 5-10 dependencies out of 30+, and which run in 4 different environments, using 2 different implementations. Now I've got hundreds of conditionals all of which need to line up perfectly across my codebase (and which also don't get compile time checks for branch coverage).

I work with DI at work and it's pretty much a necessity. I work without it on a medium sized hobby project at home and it's probably one of the things I'd most like to add there.

jjice · 9 months ago
But can’t you do your DI by hand? The frameworks can really become absurd in their complexity for a lot of tasks (I’m sure they make sense in many situations). DI is a concept that can be orchestrated with normal code at the top of the execution just fine, eliminating a lot of cruft.

To be fair, the numbers you h the row out sound like a framework becomes valuable, but most places I’ve seen DI frameworks, they could be replaced with manual DI and it would be much simpler.

danpalmer · 9 months ago
When you say "do your DI by hand", do you mean wiring up each case, or do you mean writing a DI system that treats the cases generically? The former is what I'm suggesting becomes untenable at some point. The latter is just writing your own DI framework for, which I think would be fine.

DI frameworks are complicated, but they're a constant level of complicated, they don't get more complicated as the codebase grows. Not using a DI framework is simple at the beginning, but it grows, possibly exponentially, and at some point crosses the line of constant complexity from a DI framework.

Finding where those lines intersect is just good engineering. Ignoring the fact that they do intersect is not.

teeray · 9 months ago
> What if I've got 20 different handlers, each of which need 5-10 dependencies out of 30+, and which run in 4 different environments, using 2 different implementations.

Listen to your code. If it’s hard to write all that, it probably means you have too many dependencies. Making it easier to jam more dependencies in your modules is just going to make things worse. “But I need all those…” Do you really? They rarely ever are all necessary, in my experience. Usually there’s a better way to untangle the dependency tree.

danpalmer · 9 months ago
This was a hypothetical, but I've seen plenty of codebases like this. There's going to be some cruft in there, but even just the baseline for a service capable of releasing, with feature flags, safely, with canaries etc, to XXk-Xm requests per second and <XXms per request, with no downtime, is quite a lot.
JyB · 9 months ago
Dan, are you referring to handling/implementing DI principle in Golang projects or not. I am curious.
danpalmer · 9 months ago
I am referring to DI as a general practice. Go does not seem particularly well suited to DI, but I'm not a fan of it as a language in general because I don't think it lets you build the right abstractions.

I've done DI in Java (Guice), Python (pytest), Go (internal), and a little in C++ (internal). The best was Pytest, very simple but obviously quite specific. Guice is a bit of a hassle but actually fine when you get the hang of it, I found it very productive. The internal Go framework I've used is ok but limited by Go, and I don't have enough experience to draw conclusions from the C++ framework I've used.

JyB · 9 months ago
You are missing the point the post is about Golang specifically.
SillyUsername · 9 months ago
Every so often a developer challenges the status quo.

Why should we do it like this, why is the D in SOLID so important when it causes pain?

This is lack of experience showing.

DI is absolutely not needed for small projects, but once you start building out larger projects the reason quickly becomes apparent.

Containers...

- Create proxies wrapping the objects, if you don't centralise construction management it becomes difficult.

- Cross cutting concerns will be missed and need to be wired everywhere manually.

- Manage objects life cycles, not just construction

It also ensures you code to the interface. Concrete classes are bad, just watch what happens when a team mate decides they want to change your implementation to suit their own use cases, rather than a new implementation of the interface. Multiply that by 10x when in a stack.

Once you realise the DI pain is for managing this (and not just allowing you to swap implementation, as is often the the poster boy), automating areas prone to manual bugs, and enforcing good practices, the reasons for using it should hopefully be obvious. :)

timclark · 9 months ago
The D in SOLID is for dependency INVERSION not injection.

Most dependency injection that I see in the wild completely misses this distinction. Inversion can promote good engineering practices, injection can be used to help with the inversion, but you don’t need to use it.

layer8 · 9 months ago
Moreover, dependency inversion is explicitly not about construction, which conversely is exactly what dependency injection is about.
SillyUsername · 9 months ago
Agreed, and I conflated the two since I've been describing SOLID in ways other devs in my team would understand for years.

Liskov substitution for example is an overkill way of saying don't create an implementation that throws an UnsupportedOperationException, instead break the interfaces up (Interface Segregation "I" in SOLID) and use the interface you need.

Quoting the theory to junior devs instead just makes their eyes roll :D

thiht · 9 months ago
Honestly inversion kinda sucks because everybody does it wrong. Inversion only makes sense if you also create adapters, and it only makes sense to create adapters if you want to abstract away some code you don’t own. If you own all the code (ie layered code), dependency inversion is nonsensical. Dependency injection is great in this case but not inversion.
pydry · 9 months ago
It's not just not needed for small projects it is actively harmful.

It's also actively unhelpful for large projects which have relatively more simple logic but complex interfaces with other services (usually databases).

DI multiplies the amount of code you need - a high cost for which there must be a benefit. It only pays off in proportion to the ratio of complexity of domain logic to integration logic.

Once you have have enough experience on a variety of different projects you should hopefully start to pick up on the trade offs inherent in using it to see when it is a good idea and when it has a net negative cost.

jen20 · 9 months ago
While I agree this is largely a "skill issue", I'm not so sure it's in the direction you seem to think it is.

Almost nothing written using Go uses an IoC container (which is what I assume you're meaning by DI here). It's hard to argue that "larger projects" cannot or indeed are not built using Go, so your argument is simply invalid.

exac · 9 months ago
Agreed. DI Containers / Injectors are so fundamental to writing software that will be testable, and makes it much easier to review code.
latchkey · 9 months ago
I've written a couple large apps using Uber's FX and it was great. The reason why it worked so well was that it forced me to organize my code in such a way as to make it super easy to test. It also had a few features around startup/shutdown and the concept of "services" and "logging" that are extremely convenient in an app that runs from systemd.

All of the complexity boils down to the fact that you have to remember to register your services before you can use them. If you forget, the stack trace is pretty hard to debug. Given that you're already deep into FX, it becomes pretty natural to remember this.

That said, I'd say that if you don't care about unit tests or you are good enough about always writing code that already takes things in constructors, you probably don't need this.

jillesvangurp · 9 months ago
Separating your glue code from your business logic is a good idea for several reasons. That's all dependency injection, or inversion of control is. It's more of a design pattern than a framework thing. And structuring your code right means that things are a bit easier to test and understand as well (those two things go hand in hand). Works in C, Rust, Kotlin, Javascript, Java, Ruby, Python, Scala, Php, etc. The language doesn't really matter. Glue code needs to be separate from whatever the code does.

Some languages seem to naturally invite people to do the wrong thing. Javascript is a great example of this that seems to bring out the worst in people. Many of the people wielding that aren't very experienced and when they routinely initialize random crap in the middle of their business logic executed asynchronously via some event as a side effect of a butterfly stirring its wings on the other side of the planet, you end up with the typical flaky untestable, and unholy mess that is the typical Javascript code base. Exaggerating a bit here of course but I've seen some epically bad code and much of that was junior Javascript developers being more than a little bit clueless on this front.

Doing DI isn't that hard. Just don't initialize stuff in places that do useful things. No exceptions. Unfortunately, it's hard to fix in a code base that violates that rule. Because you first have to untangle the whole spaghetti ball before you can begin beating some sense into it. The bigger the code base, the more likely it is that it's just easier to just burn it down to the ground and starting from scratch. Do it right and your code might still be actively maintained a decade or more in the future. Do it wrong and your code will probably be unceremoniously deleted by the next person that inherits your mess.

rco8786 · 9 months ago
What you’re describing is generally good coding practice. But not, I don’t think, what people associate with DI.
jillesvangurp · 9 months ago
I think people's associations might be wrong then. In general, people seem to have a lot of misconceptions about DI. Like needing frameworks. Basically by inverting control of what initializes code, you create a hard separation between glue code and logic. Any logic that initializes code would violate that principle. You inject your dependencies because you are not allowed to create them yourself.

And yes, that is good coding practice. That kind of was my point.

superdisk · 9 months ago
It always blew my mind that "dependency injection" is this big brouhaha and warrants making frameworks, when dynamic vars in Lisp basically accomplish the same task without any fanfare or glory.
silvestrov · 9 months ago
Because "big brouhaha" is what people really want.

They don't want simple and easy to read code, then want to seem smart.

Dead Comment

mattmanser · 9 months ago
Because in statically typed languages they require a bit more scaffolding to get working.

And it is a bit magic, and then when you need something a bit odd, it suddenly becomes fiddly to get working.

An example is when you need a delayed job server to have the user context of different users depending who triggered the job

They're pretty good in 95% of cases when you understand them, but a bit confusing magic when you don't.

TeMPOraL · 9 months ago
> when you need a delayed job server to have the user context of different users depending who triggered the job

I feel this is just a facet of the same confusion that leads to creating beautiful declarative systems, which end up being used purely imperatively because it's the only way to use them to do something useful in the real world; or, the "config file format lifecycle" phenomenon, where config files naturally tend to become ugly, half-assed Turing-complete programming languages.

People design systems too simple and constrained for the job, then notice too late and have to hack around it, and then you get stuff like this.

JyB · 9 months ago
Golang is statically typed.
kazinator · 9 months ago
There is absolutely fanfare and glory, even more than about dependency injection.

And "dynamic scope" is also a lofty-sounding term, on par with "dependency injection".

DanielHB · 9 months ago
DI is a very religious concept, people hate it or love it.

I myself am on the dislike camp, I have found that mocking modules (like you can with NodeJS testing frameworks) for tests gives most of the benefits with way less development hell. However you do need to be careful with the module boundaries (basically structure them as you would with DI) otherwise you can end up with a very messy testing system.

The value of DI is also directly proportional to the size of the service being tested, DI went into decline as things became more micro-servicy with network-enforced module boundaries. People are just mocking external services in these kind of codebases instead of internal modules, which makes the boundaries easier.

I can see strict DI still being useful in large monolith codebases worked by a lot of hands, if only to force people to structure their modules properly.