I have a very strong aversion to Dependency Injection frameworks. Hate... is a little strong; but I'm comfortable saying I hate DI Frameworks.
"But why?" I hear you ask. I mean... I actually read it. This post is the answer to that question I was asked in a private exchange.
(Note: a lot of places I use just "DI" which I'm using as "Dependency Injection Framework").
Could there be situations where DI is needed... Maybe. Let's cut out the real world in this discussion. I fully recognize that inheriting code; DI is probably a god-send. For code I've inherited I've used things like PowerMock to get tests in. Is PowerMock a good tool - Hell No!. (I mean, it's a fucking AMAZING tool and the best thing in the world to avoid re-writing code; but as a planned tool when writing new code - NOOOOOO!)
Let's not talk about the non-ideal and constrained situations we oft find ourselves in. This is a conversation centering on our ideal path in an ideal greenfield situation; which is the time and freedom to do what we think is the right thing.
I haven't seen everything. What I have seen and done shapes my opinion, new experiences will change that shape. And my experiences has me hating on DI Frameworks.
I want to find where DI is the better option; I haven't.
OK, let's hit some bullet points of my standard tropes against DI Frameworks.
Black - Fucking - Magic.
It's black magic. If I can't understand what the tools are doing; I'm not a fan of planning to use them. It's harder to reason about and while I trust the tool has been hardened; reflection is fragile.
Accepting a tool that functions on reflection is like inviting a vampire in - It's allowed now. If you want to bring up Newtonsoft - I don't like using it's reflection based serialization either.
Letting reflection into your code flow is accepting it in your code flow... You'll find a reason to use it.
Black magic doesn't have to be just reflection. It's the blackest of magics, sure. Let's just go to the cryptic nature of just USING a DI framework.
We don't have to understand how to configure DI Frameworks very often. We get it working and... just copy and paste. If that's not you; why the hell not? If a line does what you want, just duplicate and modify.
We lose our understanding of how to use the DI Framework. Trying to figure out everything but the simplest case... hours of time to find the correct incantation for it to work. Heaven forbid if you sacrifice a rooster instead of a hen when the ritual called for a "chicken"... You should have know. Instead; memory leak and a random crash.
Lifetimes and scopes are going to operate a little different depending on the tool you choose. Or are they? I DON'T KNOW!!! If I understand one, will that be subtle wrong for a different one? I DON'T KNOW! I have to do some detailed digging to know how, what, and why of most of the tools behavior.
They aren't self documenting. The developers I've worked with have always had to do a lot of digging and reading to understand what's actually happening; and to do something new.
I've been on a team with a 5 day outage because the DI framework we used was slightly different in the Singleton scope than another they'd used. The slight variation; took us out for A WEEK!
If for no other reason - A DI framework took us off line for a week.
I definitely have more reasons... but uhg... that one still burns.
DI? What that do?
A project I joined as a tech coach used a DI Framework. It got handed off a bit before and developers followed the patterns as they existed when implementing similar things. Because we're lazy! And commonly structured code is easier to understand.... but I focus on being lazy.
I looked at the DI for the new feature the team was implementing. It didn't quite match the class constructor. Which... really odd. The dependencies weren't all being injected...
This ties strongly into the "Cryptic" bit above. The developers didn't understand how the DI worked, or what it did, so they just copy & pasted what their example had and continued.
So... this new feature didn't use it. OK; check the example feature. ... Same... thing... Doesn't ... use DI...
There were 50 lines of maintained and added DI Framework code that... nothing used.
It felt SO GOOD to delete that entire mess.
Whatever class does your object graph - It's a god object. I bet it's the most changed code file in your repository. It /has/ to know about everything in the system.
New dependency - Modify it.
Update which dependency - Modify it.
Remove a class - Modify it.
The dependencies flowing into the god class is going to be huge.
Much of this classes existing is a code smell.
Does A LOT.
Changes a lot.
It's... just an ugly piece of code. Ugly code is problem code.
A project at work had hundreds of DI configuration declared. Practically everything was injected through dependency injection.
They had a couple hundred lines with
#regions (thanks for that C#) for what was being configured.
Even had some configuration lines commented out because it was configured for an earlier component.
//di.RegisterSingleton<IMyInterface>(new ThisClass());//Registered above
... I didn't want to work in that code base. Fortunately I was just checking it out and didn't have to. I continued to dig into their code and it highlighted a issue with tooling that can arise.
Where's this class used? No where? Oh; 1 reference. Registering for the DI framework.
What classes use this? ... Well... ... I don't know.
I can't find what where it's instantiated and passed in; or created... Do I need to run the app and output the object graph to see where it's being used TODAY? What about tomorrow?
What about my new interface?
What if I want to update the class constructor? ... Have fun in the object graph creation.
The tools that can amplify out functionality in the code are hamstrung. They can't do as much for us. They start to LIE to us. I've seen classes that are /unused/ due to the DI framework.
When I can't trust some aspect of my IDE and tools... I can't trust any of them. Cruft accumulates and the code decays.
DI Decays Code.
DI hides dead code. If a class and interface isn't used anymore due to a refactor; we'll never know. The DI Framework will reference the class and possibly interface... so there's ALWAYS a use; of unused code.
I can't trust my tool to help me clean up my code. It's going to sit; unused and... I'll never know.
DI Hides Dead Code.
DI causes cruft to accumulate.
Destroys the big picture.
I can't see how things relate. What's using what... DI Frameworks hide how classes are used and interact. I have to know how the DI framework functions to find what class is getting loaded.
I can't look at code flow from the code I'm in. I have to constantly go back to this god class to find what is PROBABLY the class being loaded for the interface in the constructor.
This causes a much higher cognitive load and lower confidence that I'm even looking at the right code.
It destroys my (and sure, maybe it's just me... but this is why /I/ hate DI Frameworks) ability to see "the code". There is no longer a cohesive application. There are a lot (and the way I code, I mean A LOT) of small units of functionality and some giant god object of black magic that weaves them together at run time.
That's a piece meal code base. I think it'll be more bug prone because the actual flow and interactions are hidden from us; the people reading the code; while we read it.
DI Frameworks make understanding the code /hard/.
My experience when trying to apply the Interface Segregation Principle with a project that uses a DI Frameworks is... a slog. The DI Framework changes required are constant. I can't focus on the code, I HAVE to keep bouncing and modifying the DI code. Every interface separation (or joining) requires modification to this black magic god class.
It slowed the changes down. A LOT. If a mistake is made... You won't know until that code is hit while it's RUNNING. We've shifted type validity from compile time to run time. That's... a crappy time to know if your object is instantiated correctly. Hope you tested every flow in the actual application.
Your tests will hide the failure to configure. You can configure the test and fail to configure the actual DI framework... How will you detect that? Runtime. IFF you execute that path.
I openly admit that DI Frameworks make quick work of things when there is a 1:1 class:interface relationship.
That's not how code stays.
What if we implement the interface in more than one class?
Clearly we need to specify which class gets which concrete instance when it has that interface.
di.Register<IInterface>(typeof(TheOneConsumingClass), typeof(ConcreteImplAwesome)); di.Register<IInterface>(typeof(OtherConsumingClass), typeof(ConcreteImplBoring));
Now we have coupled which concrete class uses which implementation of the interface. The exact thing DI Frameworks are intended to avoid!
How each framework decides to actually implement this specification... different. At least the three I've worked with each had a distinct way of doing it.
If I want
OtherConsumingClass to use a different concrete type... I don't touch
OtherConsumingClass even though THAT is what's changing... I have to go modify the god object to do something different.
Oh; and let's add a third... Hope everything stays correct.
I recognize that I might be hitting some extremes with these complaints... but they are what I've ran into. It's why I hate using DI Frameworks.
DI Killed a Refactor
I was helping an intern refactor some code. There were two code flows identical except for 2 or 3 variables. We started at the exit of the system to pull common classes together.
It worked fantastic.
Up until we hit the controller.
There were 1:1 class:interface relationships in the existing code. I was refactoring it down to one interface on multiple classes.
I don't recall exactly what about the DI killed it; but what it would take to configure the DI; or to remove it; would take too long. So we reverted it all and ... it stayed as massively duplicated code.
The system had to stay in a worse state because a DI framework was used.
I've only seen DI be detrimental to the system. I've never had a "oh thank god we used DI here" moment.
Only bad examples?
Maybe I've only seen bad examples. Sure. Are they're good examples; maybe? I've only seen DI produce code smells. Are these smells better than the ones it's trying to fix - Not that I've seen.
Maybe C# has been a special case for me. Maybe I can find language tricks to accommodate what DI solves for others... I don't think I'm that clever. OH - I think I'm clever; but... Not uniquely so.
So many DI frameworks wouldn't exist for C# if it had language solutions.
I've only seen DI be bad for working in the code.
Haven't needed it
In the end; I haven't needed it.
I find the systems I've removed it from better for taking it out.
I very much WANT to find a situation where my current approaches do not work. I want to find where DI is a better option than anything else I have to try.
What do I do...
That's the next blog post. I wrote it as part of this... but I'm already over 2000 words, so it's the next post.