Yep, you get all the downsides of coding in Basic with the bonus of the performance hit you take from all the reflective instantiation.
They can help simplify what is essentially a problem of complexity that is self-induced by the original decision to use OOP. But it's rare that they don't also introduce tons of additional other complexity, e.g., at runtime. Debugging these applications is one of the most infuriating things I've ever done as a programmer.
Secondly, you don't even need to use interfaces with one-off implementations if you don't want. You can inject your concrete implementation class instances directly. Using interfaces just makes it easier to swap implementations as needed. The unit testability argument is completely orthogonal (and also in the case of the JVM, you can mock classes as they're open by default, so again, you don't even need to use one-off interfaces).
And I don't see how this issue is limited to OOP. At the end of the day, you're going to have to hook up your components and object graph anyway, either manually or using some kind of DI, so the complexity is always there. Even if you don't write interfaces with single implementors, non-trivial programs quickly become extremely verbose to implement. Maybe something like Scala's `implicit` can be of help here.
> Debugging these applications is one of the most infuriating things I've ever done as a programmer.
Again, Dagger and Micronaut do DI at compile time using codegen, so nothing to debug at runtime.
Non-trivial programs don't have to be verbose. They are generally made so by the addition of unnecessary "features" that, in turn, require even more additions "to reduce complexity."
I also agree that is kinda of funny to constantly so the Verber classes everywhere in Java, but it has at least improved over the last several years with the standardization around Function<T,R>, Supplier<T>, Consumer<T>, etc. Lambda syntax and method references also help. I'd admit the syntax for using a functional style in Java is still a little jenky, but its quite an improvement from where things were back in the day.
The biggest upside I see for automated DI is to get back concision once you've parameterized your code by the code it depends on. Usually this parameterization is done for unit testing purposes.
Direct calls are hard to exclude or intercept for testing, so you call a method on an interface or a base class or an object instead, and now you need to be initialized with a reference which may be replaced for testing purposes. But the requirement for initialization remains in production, even when there is normally exactly one candidate and it might not even require state if it wasn't for all the other references it in turn needs to call.
The great increase in the burden of initialization creates demand for the IoC / DI container. And once you have a container framework which takes care of gluing objects together, why not let it stick transparent adapters (proxies) around the objects too? Proxies for per-request state, multi-threading, transaction management, caching, all that good Common Lisp advice of before, after and around.
The temptation for clever people can be hard to resist, and before you know it your local code has global side effects unknowable to people who don't have the full picture in their head.
I actually blame unit testing, particularly when combined with a static type system. Static binding, in particular: you could mock static calls if dynamic binding like Emacs Lisp was available!
Unit testing is great at the leaves of the call graph, for self-contained things like container libraries, or things which behave functionally, i.e. they transform things.
Unit testing is like arthritis in the middle of the call graph. It makes rearchitecting much harder by baking in assumptions both upstream and downstream, and the easy temptation of mocking usually overspecifies the interaction with collaborators. The parameterization demands DI which encourages too much cleverness, as discussed. And unit tests don't leave you very confident that things will hold together because test mocks and stubs don't necessarily replicate the behaviour of the real things.
I like integration testing higher up the stack, and trying to convert a program as much as possible into a composition of functional elements - things that belong on or near the leaves of the call graph, and leaving as little "middle" as possible to be infected with unit tests.
It is very hard to do something that is 'good' in technical terms while also being 'good' in product terms. While the discussion often doesn't revolve around the practical implications of taking steps toward a commercially uncommon language or paradigm during business engineering it is pretty much where all the 'stuff gets done'.
And java isn’t where all the ‘stuff gets done’ - well, outside of the enterprise programming bubble. JavaScript and (surprisingly) python are the workhorses of the modern startup ecosystem. I can’t speak with certainty about python, but separating logic and data, and using immutability with stateless components and functions is all very common in the JavaScript world.
C, C++, Java, Python, JavaScript, C#, even PHP are not exactly ideal, even in their special niches, but a lot of real world stuff, if not most 'stuff' gets done using that.
Global variables are called "Singletons". DI injects a reference to all the globals you need to do your work, so you don't feel so grubby. Often DI injects some kind of accessor to the database, a giant store of global variables.
Procedures are called beans, but you can tell they're procedures because they have a single public entry point and are usually called SomeKindOfVerber with a main verb() or do() or execute() method, they have the procedures they call (sorry, I mean "collaborators") injected, and they themselves are injected to where they'll be called from. Their state is ephemeral, and only exists for the purpose of executing a single flow of logic (this is called "bean scope").
All you need to do to describe your code as object-oriented is convert your procedures into beans, and tada!