I only miss DI. I miss being able to say "this system depends on these external things" and having a consistent, convenient way of sharing/swapping/testing those components and dependencies.
The solution in other languages? Unstructured globals, deep argument passing, or monkey patching with mocks?!
Yea, I can write simpler code without DI... By ignoring a bunch of stuff.
If you write classes with final fields, with a constrcutor that takes the class' dependencies,and don't use static fields to hold mutable data.
You are doing DI. Just call `new` yourself, instead of having the framework do it for you.
In the main method, then you can pass the configured values wherever you need to when new-ing classes.
> At some point you want a user-facing UI where the available features (which are generally classes) are listed and the user can choose the feature, say which log backend is enabled, without having to change code - that's the whole point of it. (And the most tedious code to write by hand - a complete waste of time)
I consider DI a valuable pattern, but I've never experienced anything close to this need.
And you may say that you don’t need Aspect Oriented programming, but the usual handling of transactions in many other languages without some meta-programming is.. to not handle transactions. Putting a single annotation over a method is imo a very elegant way to handle this needed functionality.
I recognize that what you’re advocating for makes of sense in some applications, I just wanted to point out that I haven’t felt the need for it in my eight years of software development.
Literally every non-toy software I had to develop in my life required that lol
But at that point, why would I want to?
There are reasons I wouldn't want to, but there is no inherent value, to me, in manually calling new.
You just have a class/classes that construct/wire all of your singleton objects and passes the required dependencies into their respective constructors as necessary.
Here is a contrived example of what the wiring code might look like for a web app that uses a database.
public static void main(String[] args) {
MyConfig config = readConfigFile();
DatabaseConnection dbConn = new DatabaseConnection(config.dbHost(), config.dbPort());
UserDao userDao = new UserDao(dbConn);
UserController userController = new UserController(userDao);
List<Controller> controllers = List.of(userController);
WebServer webServer = new WebServer(config.listenPort(), controllers);
webServer.runAndBlock();
}- Don't need to depend on a DI library, makes code more modular and portable.
- Faster application initialization time.
- Easy to navigate and understand relationships between classes, good IDE support.
- Easier to break apart and test parts of the application.
- Easy to understand, don't need to learn the intricacies of a complex DI framework.
I'm not saying there is no place for DI frameworks, although I do think they are overused.
I know it is a nitpick, but I see this way more often than I should as a main reason to prefer alternatives.
Finding out what gets injected is not particularly hard, especially when only the basic capabilities of spring’s DI is used. In that case it will be almost always the single implementing class of the given type.
Congratulations, your @Configuration is manual dependency injection. That is easy enough. Why did we need inversion of control over the dependency injection in the first place? It isn't immediately obvious to new engineers what aspect of the @Autowired is dependency injection and which aspect is inversion of control. Many of us don't see much of a benefit to the inversion of control if you are taking care of your application's hygiene in the first place.
This reminds me of SQL/ORM debate. "Just use SQL!" Sure, until you get tired of typing the same SQL over and over and realize you can cut out most of that crap by adding an ORM.
https://steve-yegge.blogspot.com/2006/03/execution-in-kingdo...
Go is typically structured with many relatively small binaries. Each binary can be relatively self-contained.
The way I've seen Java used, it typically has fewer binaries with each binary bundling many services. Many of which include clients for services at the company but a different org - where that other org can just provide a Guice module that sets up the client to call their service and anything that needs it can easily inject it.
I still hate Java but, damn, I see why it's used at B I G companies.
I use Go as a "better C". Though I'm honestly disappointed with even that. My current company, we built an image processing service in Go. It performed poorly and had poor stability (the imagemagick bindings appear to be half-baked). I rewrote it in Java and it is faster, more stable, and the code is much cleaner.
Honestly, the next time I need a "better C", I'll probably pick up Rust or D.
YMMV.
Are you saying that Java is better about any of that?
Just compare Java streams with Go container classes. Go's aren't typesafe (though that will hopefully change when generics are officially released) and almost every operation requires imperative code. And endless `if err != nil return err` every time you want to call a function - which actually destroys useful stack information.
I won't apologize for the crap Java code out there - but you can write crap in any language. Modern Java is capable of producing pretty, svelte code.
Can you elaborate on Java streams vs Go's containers? I assume you mean things like List and Heap in Go? I'm not sure why you'd compare those to Java's stream API rather than Java's collections. In any case, I do agree that Java's standard library has WAY better collections than Go does, and Go doesn't have the excuse of wanting a minimal standard library.
However, I'll push back a bit on the complaint that working with Go's containers/collections/whatever requires imperative code for everything. Now, I'll remind myself that one of your original points was that Go was "actively hostile toward functional programming" and I retorted to imply that Java was just as bad at all of the things you mentioned. I'll concede that Java isn't actually quite as hostile toward functional programming as Go. But, I'll move the goalposts a bit and claim that supporting some few functional programming patterns isn't inherently good and doesn't automatically make a language better.
> And endless `if err != nil return err` every time you want to call a function - which actually destroys useful stack information.
I agree and disagree. I'm one of the few people who still thinks that checked exceptions are a good idea for a language. I have my complaints about how they're implemented in Java, but I think the concept is still a good one and I honestly think that even the Java implementation of checked exceptions is mostly fine. The issue, IMO, is with training and explaining when to use checked vs. unchecked exceptions and how do design good error type hierarchies.
Go's idiomatic error handling is mostly stupid because Go doesn't have sum types. But, I'd argue that if you are wanting stack information, it means that you shouldn't be returning error values at all- you should be panicking. Error values are for expected failures, a.k.a. domain errors. You can and should attach domain-relevant information to error values when possible, but generally, there shouldn't be a need for call-stack information. A bug should be a panic.
int population = countries.stream().mapToInt(Country::getPopulation).sum();
The Go implementation: var population = 0
for _, country := range countries {
population += country.Population
}
It gets more perverse if you need to flatMap, or transmute components of map types, etc. If you want even more power, take a look at https://github.com/amaembo/streamex. This sort of container manipulation is bread and butter for business processing. I use it every day, sometimes with a dozen operations. This (with liberal use of `final` values) makes for some pretty functional-looking code.I'll grant you the Kotlin or Scala version is slightly more compact. But not fundamentally different, like the Go version.
I (and the pretty much every language designer in the post-Java era) disagree with you about checked exceptions, but that's a whole different thread...
Something else to consider is performance, in most implementations the for loop is going to be more efficient.
Then FP became the hot new shit, so they all added some of the lowest hanging fruit so that people can say absolutely weird things like "I do FP in C#". The problem is that the majority of these implementations just eagerly iterate the collection and make full copies every time. So, you're much better off with a for-loop.
To be fair to GP, though, Java has legit engineering behind it, and the way they did it was to introduce the Stream API, which is lazy sequences, and they made the compiler smart enough to avoid actually allocating a new Stream object per method call (which is what the code nominally does, IIRC- each method wraps the original Stream in a new Stream object that holds on to the closure argument and applies on each iteration).
Have you used it? I'd be curious to hear how well it works in practice.
It seems like the only "big" things Scala has over this is its implicits (which so many people hate, but have been really improved in version 3) and its for-comprehension syntax.
It's so interesting to see a bunch of projects converge on really similar things. You look at Scala, at this Vavr stuff, and at Kotlin + Arrow.kt, and they're implementing all of the same stuff over Java.
So, I concede that Java's addition of the stream API is a legitimately good example of adding an aspect of functional programming to an otherwise very non-FP language.
But, let me go off on my tangent, anyway. ;)
It's not that you need to convince me that functional programming is great. It's just that I find that consistent and coherent designs tend to work well and that kitchen-sink or be-everything-to-everybody approaches tend to be good at nothing and mediocre-to-bad at everything.
MOST languages that have tacked on the low-hanging fruit of FP (map, filter, etc combinators on collections) have done it in a really sub-optimal way.
JavaScript, for example. JavaScript has eager, mutable, non-persistent, arrays as the default collection data structure. When they added map, reduce, filter, etc to Array, they added them in the most naive possible way, which means that doing something like your example above (map-then-sum), would create an entire extra array with the same number of elements as the original, and would end up looping both arrays once. So we have ~2N memory usage and 2N iterations where we really should just have an extra 8 bytes to hold the sum and iterate over the array once (N iterations).
Same thing with other languages like Swift and Kotlin.
Kotlin maybe should have an asterisk because it has Sequence, which will mostly work like Java's streams. However, there are two issues: it still offers them on eager iterables, instead of forcing us to use a sequence/stream to access them, and with suspend functions you have to be careful with Sequences. In you Java example, we're theoretically allocating a new Stream object with every combinator call, BUT we "know" that the compiler is smart enough to avoid those allocations and the result code will be about as fast as writing a for-loop. With Kotlin's suspend functions, we can very easily thwart the compiler's ability to do that. If you use a Sequence chain inside a suspend function and call another suspend function as part of that chain, then that's a yield point and the compiler can no longer optimize away the allocation of the intermediate Sequence object(s).
So, my point is that designing a language with some initial philosophy and then trying to borrow from, frankly, incompatible other philosophies usually leads to sub-optimal implementations and/or APIs. Again, though, Java's streams are a good counter example to my claim.
> I (and the pretty much every language designer in the post-Java era) disagree with you about checked exceptions, but that's a whole different thread...
Indeed it is! :) I'm willing to be the black sheep, and die on that hill, though (too many metaphors?). And, honestly, I don't think it's as unanimous as some people claim. I see returning monadic error values as isomorphic to checked exceptions, and several languages have gone that route since Java: Scala, Swift, and Rust, to name a few. Kotlin's lead dude, Roman, simultaneously claims that checked exceptions were a terrible mistake, but then also advocates for using sealed classes for return values when failure is expected or in the domain, which sounds a lot like what checked exceptions are supposed to be used for. TypeScript can't have monadic error handling because of its design philosophy of being a thin layer over JavaScript, but many in that community have embraced using union types for return values instead of throwing Errors.
Cheers!
Also, Go has really poor abstracting capability, which may be good for small code bases where having abstractions is a detriment, but abstractions are the only way to handle complexity. If you have the logic spread out over many different parts (or God save us, copied code!), a new programmer will have much more trouble picking up what the hell is supposed to happen.
In the extreme case, compare reading assembly to a high level language. Sure, each instruction is trivial in the former case, but you have no idea what does the whole do.
Java is in the unique position of excellent performance (state of the art GC, very good JIT compiler) and observability with no-overhead real time options. Due to the language having multiple implementations of a standard and it being one of the top 3 biggest ecosystem, it is nothing like Cobol. You can say it is legacy for 3 decades to come, but it will not die. Hell, it improves with a never-before seen speed.
Go not having too high abstraction power, while can be an advantage (as per the creator, not my words, you can throw as many bad developers at a project as you want), but it is a disadvantage as well, because then you will have the logic in distributed places, copied verbatim etc, hindering maintainability, understanding the original intent, new dev onboarding, everything.
When I debug a well written Java code usually the callstack is about 50 levels deep.
It's not because of the Single Responsibility Principle.
I really don’t see any cons, other than a slight learning curve (and yeah sure, “developers” that just bash keys will have trouble with understanding what does an annotation do and blindly copy-pasting them can be dangerous but they will also fk-up regular code as well..)
And adding an ORM isn't either/or. You can still use native SQL when necessary.
The only bad things I can see are:
1. Constructors with many parameters
2. Needing to pass a dependency many levels deep
But, I would still think that those are not big deals (what do you have, 40 parameters or something?) and that the explicitness can be helpful. Isn't it good to know that the top level service depends on your email-sender dependency from just looking at its code instead of needing to analyze its code and every single object under it?
Spring takes care of that, but doing it manually (and without dynamic proxies) would add to the verbosity.
But frankly, how will you call that new if it depends on a class which is a singleton, another which has some more complicated scope so it may or may not have to be reused? DI is not only about calling new..
And I fail to see why it's a problem. If your FooService depends on a BarService, which depends on a BazService, and BazService needs a database connection, then that means your FooService really does also depend on a database connection. Hiding that information, to me, seems like a mistake. Can you articulate why one would prefer not to have FooService explicitly require that database connection, or am I inadvertently arguing against a straw man? If so, please correct me, because I'm asking sincerely.
Of all the time I spend thinking about my code and writing code, I truly can't say that adding a dependency and having the compiler complain until I fix a bunch of constructors has really caused me that much grief. And I'm not going to pretend that it has never been the case that I've had to fix 20 constructors.
I would prefer not to have to fix 20 constructors.
It's tedious and time consuming. The intermediate classes that _do technically depend on FooService because BarService does_ - the intermediate classes don't care! It clutters the code everywhere else for minimal benefit.
Manually, you see all your dependencies just shy of main where the binary initializes them all and starts passing things down. In DI, you have a module file somewhere with them all.
But thank you for responding anyway.
(As a clarification, in case it's needed: I obviously didn't LOVE it when I had to update 20 ctors after changing a somewhat fundamental "service" to need a new dep. My point was that, even as painful as that was, it wasn't that bad and it's usually much less bad than that.)
I guess the (philosophical) difference comes to this statement:
> The intermediate classes that _do technically depend on FooService because BarService does_ - the intermediate classes don't care!
I can definitely understand what you're saying there, but it's interesting to me that I don't see it that way. I think I'm just less pragmatic and more... "academic" (?) about how I read and understand my own code. If X depends on Y and Y depends on Z, I'm comfortable with X explicitly depending on Z because I imagine "inlining" Y's functionality in X. Either that or you turn Y into an interface and then X only depends on IY. But, my brain just likes the explicit continuity I guess.
Cheers!
But then, working in a complex codebase, I introduce a new dependency that is instantiated early in the tree, used two disparate classes rather deep in the tree, suddenly I'm changing 10 different constructors just to get the new dependency where it needs to be.
The tree of constructors is where DI shines as an alternative.