Notwithstanding its popularity, is there any empirical evidence that shows that Dependency Injection (and/or using a DI container) helps with, say, reducing bug counts, improving maintainability, or increasing development velocity on real-life software projects?
The empirical data is irrelevant. Tools and practices (like DI) solve particular problems. Understand your problems, learn how to use the tools, and it'll become obvious when a tool is valuable -- and you'll be able to explain the outcomes much more prophetically than any generalized, aggregated, empirical data.
And now, with much more verbosity ...
Is there empirical evidence?
Sure, probably. Or at least maybe. But, who cares? It isn't relevant.
A statistical cost-benefit analysis of DI may be interesting academically, but, it doesn't necessarily predict individual success. Aggregated results hide individual successes and failures. And, I might argue that data regarding "evangelical" practices is particularly poisonous. These disciplines tend to attract both zealots and fools, both of which obscure the net impact of a "pure" implementation, and either of which you could be!
So, how do we know Dependency Injection is valuable at all?
Good question! GREAT question, in fact. And I'm with you -- I hate wasting time and mental effort on dogmatic "best practices" that no one can justify. So, I'm glad you asked.
Uhh. But, here's the embarrassing problem ... In general terms, you don't know. And, even more embarrassingly, your code may not actually get better in any way by introducing DI.
⊙▃⊙ . . . (╯°□°)╯︵ ┻━┻
So, maybe now you're wondering ...
Why should I bother 'bout things ain't been proved er nuthin'!?
First of all, let's all just -- on everywhich side of the debate -- just settle down. I can assure you that between the dogmatism and skepticism lies a beautiful paradise of reason and level-headedness. (And the occasional eccentric SE.SE post.) And, the POAP can lead you there.
... By which I mean, the Principle of Applying Principles:
Principles, patterns, and practices are not final purposes. The good and proper application of each is therefore inspired and constrained by a superior, more final purpose.
You need to understand why you're doing what you're doing!
(The POAP is not exempt from the POAP.)
(I'd say, "emphasis mine," but it's from my own "blog" anyway. So, it's all mine!)
Let me reiterate the main point there: You need to understand why you're doing what you're doing.
And perhaps to clarify, it doesn't usually make sense to take any given "something" (like Dependency Injection), and use it without already understanding what problem it solves -- for you specifically. If you understand your problems and how the "something" (like DI) works, it'll be somewhat "obvious" how helpful the "something" is, very much regardless of what the generalized, aggregated, empirical data suggests.
If DI's helpfulness or un-helpfulness to you is not obvious -- or at least beyond your reasoning powers -- you either don't understand DI, or you don't understand your own problems.
Let's consider a real-world "parable."
Now, we may have some broad empirical data to show that boxes constructed with screwdrivers are significantly more robust boxes overall, as compared to those constructed with hammers. But, if you try to screw those nails in, you won't end up with a box at all. And, if you try to smack them in with the screwdriver, you might eventually get them in; but, it'll require significantly more time and effort, and the final result will be less precise (and robust) than had you simply used the hammer.
And, if you've ever seen someone use either tool before, and if you understand what a box looks like, the decision is obvious.
Err ... hmm...
Yeah-So, what problem does Dependency Injection solve?
It works to prevents rigid, un-configurable code, which is often therefore un-testable.
It does this by permitting invoking code to decide what objects a module operates with. And I know you're thinking it, and you're right: This isn't even a remotely new concept. Method/Function parameters have existed since algebra happened.
We started evangelizing basic parameter passing, calling it "Dependency Injection", once we accumulated and inherited enough code to see our imbalances. The mountains of code we were sitting on top of couldn't easily be changed, tested, or even reused, simply because the dependencies were hidden away.
Hence, the zealous crusade for Dependency Injection...
K. But, I can pass arguments in just fine. Why the frameworks?
As I understand it, DI frameworks primarily solve the problem of boilerplate buildup (due to overzealous DI, IMO) -- particularly when there are standard "default" dependencies for all modules that require them. DI frameworks do magical (potentially even naughty!) things to slip those default dependencies in when they're not explicitly passed at the point of invocation. (Same effect as a Service Locator when used in this manner, mind you!)
Dependency Injection, as a "discipline", is actually really hard to get right. It's not a matter of using DI or not; it's a matter of knowing which dependencies are likely to change or need mocking and injecting those. And then, it's matter of deciding whether DI fits the better than some alternative, like Service Location ...
I searched Google, Google Scholar, ACM, and IEEE Here were the papers I've been able to find:
Dependency Injection frameworks: an improvement to testability?. It argues that "testability" can be defined as "low cohesion". It states that DI leads to lower cohesion, lower cohesion is correlated with higher test coverage, and that higher test coverage is correlated with more faults found. It says that, based on this, DI improves testability.
I don't like this for a couple of reasons. First of all, it's saying "A is correlated with B, B is correlated with C, so A causes C", which is a couple of steps in the logic I don't see as being well supported by the paper. Second, it admits that it's only measuring "subparts of testability", and that 'testability' in general is not something easily defined. Finally, their measure of testability is defined in terms of the number of injected dependencies!
Effects of Dependency Injection on Maintainability. They compare projects using DI to projects not using DI they found on SourceForge and see if there are any differences in cohesion metrics. To reduce bias, they paired off the projects to be as similar as possible. Ultimately, they saw signals that projects with a lot of DI were somewhat less coupled than projects with only a little DI. However, it seems there was no significant difference in cohesion between the DI projects and their non-DI pair, so it could be a consequence of the specific domains. They list "no correlation" as their primary result and the "maybe it helps a little?" as a topic for further study.
Empirically assessing the impact of dependency injection on the development of web service applications. The abstract doesn't really explain what they're looking for. I dug up and read a preprint and as far as I can tell it's actually about how well automated tooling can discover services. Services written in a DI style were more easily discovered. Also, it cites the previous study I listed as providing empirical evidence that DI reduces coupling, which is the opposite of what that paper claimed.
For these three papers (and for An Empirical Study into Use of Dependency Injection in Java, which is only about detection) I followed up on all the papers that cited them, none of which were about determining the effectiveness of DI. Given all of this, I'm confident saying that no, we do not yet have empirical evidence on whether or not DI improves software quality.