I haven't really touched Java in a while but I don't get why you'd want a lightweight DI container.
You can just build your object graph and pass dependencies manually if you want a lightweight approach, no? That's just the way people do it in most languages.
I think there are a lot of Java developers, that have just never worked without a DI framework, and just don't have a grasp on just how simple it can be to write code without one.
As someone who hated Java, used it for a few years, and now occasionally misses it...
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.
When you have lots of things-that-create-things-that-create-things, this gets tedious really fast. DI frameworks exist because they result in a lot less code that does nothing but pass dependences along.
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.
If you take the single responsibility principle even as much as half-seriously, the problem domain more or less decides which things will create which things. If your software platform can't support that, you get spaghetti mess when programmers inevitably build workarounds.
You know, you hear Java repeat things like that a lot, while Go programs just tend to stay simple and readable. It's either the culture or the language causing the problem. shrug
I did a fair bit of work in Go at Pivotal. I found Go anything but readable - a comical amount of boilerplate (especially around error handling), incredibly wordy constructs for simple tasks like making http requests, and the language is almost overtly hostile to functional programming (no generics!).
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.
> I did a fair bit of work in Go at Pivotal. I found Go anything but readable - a comical amount of boilerplate (especially around error handling), incredibly wordy constructs for simple tasks like making http requests, and the language is almost overtly hostile to functional programming (no generics!).
Are you saying that Java is better about any of that?
Yes, absolutely. Java has had a competent implementation of generics since 2004 (Java 5) and really embraced functional programming in 2014 (Java 8). Any application of significance will require more LoC in Go than Java, hands down.
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.
Fair points. I haven't worked with Go in a few years, and I remember hating it when I did, but I feel like I remember hating Java more. It's possible that part of the Java hate is not from the language itself, but from the ecosystem.
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.
Here's a Java example that sums the populations of a list of Countries:
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...
That's exactly my complaint- most languages have eager, mutable, non-persistent, collections because they were not designed with functional programming in mind.
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.
Ah. You know what? I forgot that the Java implementation of these concepts isn't stupid like it is in some other languages (except what the heck is mapToInt? Some optimized version that makes a primitive array, I guess? Yucky- I wish the compiler could just figure that out).
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.
Yeah, mapToInt is annoying because the primitive/object dichotomy in Java is annoying. No question about it, it's a wart on the language. Though it does offer some optimization abilities, so the dichotomy is not completely meritless - it's easy to understand why the language designers did it this way. Maybe project valhalla will fix this someday, I don't know. In the mean time, it's not a fatal flaw.
I imagine that is because Go is not used for applications of the same breadth as Java.
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.
As mentioned, Go is not used for ENTERPRISE APP^TM — Java programs can really hold up under insane abstractions and complexity.
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.
So, COBOL? That's an argument that works in a historical moment of Java legacy, until it doesn't. Monzo is an example of a bank writing everything in Go.
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.
Now you seem to have switched the conversation of "Java projects tend to be overly complex" to "Java is great". Common talking point, and you have a lot of people who will agree with you, but pretty much unrelated to the topic.
My original point was that abstractions are not evil, hell, without them we would only have calculators, not computers.
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.
These words are bandied around a lot by people outside the Go community, while the people who end up actually using Go a lot tend to say it's the most readable code they've worked with in their lives. shrug
Why? With such a well-known framework like Spring, you will get the benefit of any Spring-developer knowing instantly the conventions (which is not true with your in-house conventions where I will have to hunt down where does this class come from, oh this ugly abstraction which is buggy as well), less code is less opportunity to introduce bugs, less thing to maintain. Annotations are basically just a declarative DSL for a significant chunk of your code base.
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..)
How is that worthy? You pretty much only have to look at the topmost exception, or at worst the causing one. Whether it has 100 lines after or 3 doesn’t matter, not the slightest.
But how do you handle configuration then ? 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)
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.
What happens with proxied classes? My ClassWithTransactions is actually a subclass of the written one auto-generated by Spring. I can’t inject a new instance of that manually.
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.
This is all considerably more abstraction than I have wanted or needed when writing Java. When handling transactions, I’ve passed around the same connection before committing.
The point of the transaction in this context is that both the database(s) and the business logic/state stay in sync. I don’t think that naive attempts will be logically correct.
As I mentioned, you will have to roll back not only the database, but the relevant application state as well. This is really error-prone if repeated enough times, or the flow of control is through many different methods, etc.
By application state, do you mean something like an in memory cache? I would prefer having no such state in the first place, and have all meaningful state in the DB to be pulled out or mutated as needed.
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.
in my case it's more often which audio, gamepad or graphics backend but yes, I actually had the log backend configuration request once ! (wanted to choose to log stuff in text files or websockets depending on the case, for a GUI app ; there was an explicit requirement that the entire software could be configured and used with only a mouse, no keyboard so many configuration menus were needed)
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?
Which is the standard way to do DI in Spring as well? It will be just called by reflection instead.
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..
Is what you're thinking of equivalent to deep argument passing? I've seen it done where you pass around a global Factory object that can provide dependencies. It's basically rudimentary DIY DI.
It's really very simple, no you don't need to pass around a factory object.
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();
}
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.
So if you are making any kind of reusable design, you cannot annotate your classes with @Bean anymore. Instead you will make an @Configuration (like spring boot auto configuration) that by discretion may pull in some more general (not @Configuration annotated) reusable configuration. Since some classes will be considered implementation details, you won't want to expose those into the dependency injection container of spring (since that is equivalent to making them public, people will inject them and depend on them!). So instead you will only create them inside your own @Configuration and pass them directly when generating an @Bean from a method.
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.
A @Bean method is a signal that a class is so complicated that Spring can’t figure out how to create a valid instance after @Import or @ComponentScan. For limiting use, package-private types and methods are better than creating components yourself and reinventing pieces of Spring like @Profile and @Value and @Scope.
Unless WebServer is the only class that needs dependencies you're either going to have to pass those dependecies repeatedly from class to class or you're going to have a global factory that provides the dependencies to everybody.
Yep. Once you get too many arguments what you do is usually to create some kind of Context class that bundles all of them and just pass that on everywhere.
I'd say that the only one of your listed solutions-in-other-languages that is actually a valid solution is deep argument passing.
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.
Ultimately, I think thisnis going to come down to preference.
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.
(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.
The solution in other languages is to use a DI framework written for them. Which one doesn't have any? In .NET, the basic DI interface (imports/exports etc) is even part the standard library as System.Composition.
You say that, and I usually agree, I mean, constructor args are the simplest form of DI.
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.
That REALLY depends on the size of your codebase. When it’s small, no need for a DI framework. But when it grows large, it becomes quite a pain, and a DI framework is nice, eliminates a bunch of boilerplate with every code change.
A good DI framework just saves you from having to spell out all the glue code; or at least minimize that. DIY dependency injection is indeed a useful skill to have with other languages. Unfortunately, it's not what a lot of people do with other languages because they simply don't know that it would help them.
Particularly in the javascript world there seem to be a lot of people struggling to write good, testable code mainly because they make the rookie mistake of not separating their glue code from their business logic. Basically they have bits of code that initialize whatever and they need to put it somewhere and it ends up in the wrong place and before you know it, it becomes impossible to isolate any functionality such that you can actually test it easily without booting up the entire application. Add global variables to the mix and you have basically an untestable mess.
I still use Spring (but very selectively). They've added multiple styles of doing DI over the years, which is confusing. The latest incarnation of that uses neither reflection nor annotations and is very similar to the type of code you'd write manually if you had the time to clean it up and make it nice to use. Another benefit is that it enables native compilation, which with the recent release of spring-native is now a thing people do. Spring is large and confusing but the DI part is actually pretty easy to use. If you've used Koin or Dagger on Android, it's similar to how that works.
I've used Spring DI. I understand the argument for it, when building bigger applications, though it invariably brings its own complexity too.
What you say about compile-time DI to allow native images makes me feel like we've almost come full circle. I'm still not convinced you need automatic DI at all for smaller services.
Why do "static references become a tangled mess"? In my (limited) experience with runtime DI libraries (albeit in Go) they turn clear, IDE- and debugging-friendly code where the compiler tells you at compile time if you got it wrong ... into a hard-to-debug magical soup.
With static, using-the-language dependency injection, isn't the question of "how does ABC component get access to DEF?" answerable with the normal IDE/language tooling, rather than some magical library's way of doing it? You can just find the calls to a constructor and look at the arguments.
My experience is based on my bad experience with runtime DI libraries, and is definitely biased against them, but I must be missing something here.
There are lots of reasons why static references are undesirable, but some of the more serious are:
* Static dependencies make testing harder, no question about it. This is mediated in dynamic languages like Ruby by mocking statics. While you can actually do this in Java with Powermock, avoiding mocks entirely is even better. If you can't use a real object, use a fake that implements the relevant interface.
* Statics mean singleton, and that invariant often changes as a product matures. It's very easy to go from "the database" to "a database", and when you have 500 places getting "the database" it's very hard to make that evolution.
* Statics make it very hard to maintain module boundaries, because every static is its own exported interface. In a long-running project, binding tends to get tighter and tighter as every module reaches out for the statics of other modules.
Sure, folks can write bad code with DI systems too. And I'm no fan of Spring - not because of the DI, which is fine, but because of the need to wrap everything else in the universe and now you have to understand both how the underlying thing works and the way Spring changes it. But something like Guice or Dagger is just the right amount of glue to hold a system together, without getting in your way.
Just a note: I overloaded (excuse the pun) the word "static": I didn't mean the "static" keyword, but "statically compiled/typed". So it doesn't mean singletons, just that you pass dependencies as explicit arguments to constructors and functions.
I do not understand what distinction you're making. In the world of DI, you still have typed constructors and factory methods. It's not like Guice turns Java into Ruby. The only difference is that you don't have to chain together constructor boilerplate - in fact, the static types determine the injections.
Do you object to passing interfaces vs concrete types? That is a wholly orthogonal concern; you make the choice to extract interfaces with or without DI.
As a Java developer for all my career here is my take. In Java world there is this cultish cottage industry of "frameworks" for all sorts of work. Most Java developers are not expected to write plain code with JDK standard + some external libraries. Creating an object via "new Obj()" might cause programing universe to collapse so DI framework is must for enterprise Java developers.
If I were to tell at work that Go has inbuilt http server where we could implement a few handlers and have a basic service running. They would not be shocked that a http listener could be this simple but rather ask "Does Go bundles "weblogic/websphere/tomcat/netty" server with it or else how can it work? Same with testing, no understanding on how, what is to be tested but everything about "JUNIT/Mockito/Mockster/SpringUnit" or whatever.
There is no requirement for understanding basic concepts for testing, client/server, dependency, error management etc. So even basic functionalities are understood in terms of a branded framework. This is their main frame of reference.
Just to echo this is similar to my experience. Java development culture that I have known in the work place is extremely coddled by their frameworks.
It is hard for them to open the terminal and execute their application JAR from the command line--only ever from the IDE. Oh wait--they need Tomcat/Apache & a few hours of dealing with classpath issues.
That's just a problem with developers who do Java as their 'nine to five' job and don't have any interest or passion to really find out how stuff works. I've met a lot of those people and there's no reason they can't contribute if the project is set up to accommodate it.
On the other hand there are the enthousiasts (like you I presume) who like tinkering and using the language to the fullest. Any successful project needs at least a few of these people, but they can also go overboard by building a lot of custom functionality where any standard library could have been used.
While I'm also an advocate for increasing knowledge for the systems you're working with it's no 'sin' to use some libraries. For instance: for your HTTP server example, it's quite easy to just listen on a socket and respond to a request. But you want parallelism, so you need a thread pool. And queues, configuration and error handling. That will escalate quickly so why not pick whatever Java servlet implementation which has most of the complexity and - more important - is already production tested so it won't fall over when you deploy it live. And then there's stuff like OpenID connect or SOAP (yes, still exists) where you can 'plug in' an implementation on some servers so you can get work done instead of worrying about getting all the implementation details right for some complex protocol.
> but they can also go overboard by building a lot of custom functionality where any standard library could have been used.
Well I am kind of recommending using standard libraries. And servlet implementation I am using embedded tomcat as I mentioned in other comment. What I am not doing is generating gratuitous scaffolding of dozen packages and innumerable classes because that is the "best practice".
Just to clarify it is not my opinion. It is the groupthink of enterprise Java programing where reading Java tutorial itself would be obscure thing. Everything has to be looked from "framework" perspective. Framework says 'new' is bad so it is bad, dependency has to be constructor/setter injected by DI container so that's how it has to be.
No framework says `new` is bad. Feel free to grep for new in any framework application. But if one uses DI, than do use it for classes that ought to be injected. But inside methods of course one can and do use `new` many times over.
In my experience most DI just interferes with being able to use the IDE to track down instantiations. Ive worked on these projects that basically have these fancy runtime things to answer questions that could be answered by the IDE if it werent so obscured. I remember one project we had a fancy thing to generate a graphviz graph, and it was like neat, but we could just use find all references if we just called new.
The dumb thing is most of the time only one type is ever injected. Its all hypothetical flexibility which has a cost but no benefit
> I remember one project we had a fancy thing to generate a graphviz graph, and it was like neat, but we could just use find all references if we just called new.
Ha. Calling `new` would either be an absolute enterprise Java sin or an obscure aracana. Some people are quite proud of the fact of converting compile time errors to runtime exceptions. Because you know "best practices" and all.
It definitely has a benefit at scale. I’m not sure what application were you developing, but the amount of time a single instance was changed into an interface because the client wanted the n+234th little change in this special star constellation.. with DI you don’t have to write any more code, you can even use different implementations per environment (@Profile), so this is not accidental complexity in most cases. Sure, if you need a 100 lines web server that prints hello world it is an overkill, but the correct tool for the job..
"static" in java DI can refer to setting global variables with singleton instances. E.g. java logging libraries usually do this so that you don't have to DI everywhere you want to log. In Go, some packages do this like flag and log. In Rails, this is so common that it replaces DI entirely, but I usually didn't feel like rails suffered from it.
I think what you are referring to is just manually doing DI. I.e. you defined constructors that accept dependencies and then call them all in a main function. I think this is tolerable if your codebase is structured for it. In typical java codebases, it gets ugly really fast. IMO this is caused by a general proliferation in the number of classes (due to class-per-file among other reasons), as well as a tendency to never use "static" DI. As an extreme example, if you needed to inject a logging dependency, then almost all code would need to be part of the DI graph. In a typical web backend, you might DI the sql connection pool. This causes basically all code to need DI since it either uses sql or has a transitive dependency that uses sql. IMO injecting the connection pool is not useful since it's not useful to write tests where you inject anything other than a real sql connection.
Ah, that explains the confusion. Yeah, I didn't mean static as in the "static" keyword in Java / C++, but as in specified in the statically-typed code. Defining constructors that accept dependencies -- exactly. Ah yeah, I can see how Java exacerbates things here with the one-class-per-file rule -- ugh.
Go doesn't require one class (well, type or struct in Go) per file, and has much more flexibility in how you build packages as a result. I think it's a good thing that dependencies like the logger and the database are passed around explicitly: I've learned the hard way that "explicit is better than implicit" even when it means a bit more boilerplate.
This, it’s really exhausting to read this never ending wheel reinvention. Sure any one can use simpler non-spring frameworks, and other “non standard” frameworks and libraries for 1/10 or 1/100 of the functionality, and get 10-100x the bugs and much less or zero support. But we need netty! And then when you add thread pools, jdbc, logging, etc? Yep you’ve reimplemented spring. Just use spring, spend the time to learn it and reap the rewards.
As someone who dealt with a ton of Spring in the recent past, I completely disagree.
First of all, thread pools are part of the standard library. Spring adds little to no value on top of it.
Second, reinventing some of that stuff is absolutely worthwhile, because Spring's library design/implementation is not very good.
Finally, when I had the opportunity to start a new Java project, I opted to not use Spring. I finally had a server that started up fast, took less code than a Spring project, was easily navigable in an IDE, and whose code was generally easier to follow. It was also easier to write tests for.
One thing I learned is that people seem to underestimate just how thin Spring's abstractions are over stuff in the library, servlets, etc. Most of what Spring does is wrap things in a bean interface so they can be used with DI (which is something I’ve never found any value in).
Well you are missing one of the great feature of Spring framework: Converting compile time errors in to runtime exceptions.
Jokes apart you are absolutely right about non-spring based services. I did same using plain Java + embedded tomcat for some services. No cargo-cult like endless decorative packages and classes. Exactly same result as you observed. Less code, fast to start and vastly improved error management.
Yeah, please popularize it. In my case I am unable to make management see reason. If more devs become vocal about it make it a trend, it will be a good thing to happen.
Same here. I've gotten rid of all that stuff, it's just layers and layers of indirection that contribute nothing.
One rule that has also helped me a lot to keep my code clean and make it easier to debug is to fail as much as possible in the constructor. So when you call new MyThing(), you will either get a usable object or it'll throw an exception. Further method calls are expected to work. Of course this is not doable for everything, but it sure helps keeping the methods clean and not have them throw various exceptions.
Could you share your Spring/Spring Boot alternatives? Are they Java based? I'm doing backend stuff with Spring Boot and I would like to test alternatives. Spring boot is not that difficult to work with, but I would like to test a "simpler" solution.
I've had similar experiences. A few years ago, I wrote a small service in plain Java, no frameworks as part of a quick change to improve performance. It worked and we all moved on. Later it was converted to Spring Boot and it slowed way down.
> But we need netty! And then when you add thread pools, jdbc, logging, et
java.util.concurrent has threadpools and pretty damn decent at that; jdbc is a part of very standard jdk, logging is part of java.util.logging. Why do you need netty (which is not a part of spring either way)?
In over 23y of working with Java, I have never needed spring.
Agreed. Stuff like this feels like magic for magic’s sake, and as someone who has had to operate services that use these DI frameworks, they are a big pain.
I read all of the documentation for the service that I operate--per my other comment (https://news.ycombinator.com/item?id=29973282), often times things aren't well-documented and it's crucial to be able to look at the source code to figure out what something does (e.g., how is a configuration parameter used? what are its valid permutations?); however, when the source code is obscured by gratuitous complexity then it imposes a high cost on the user and in the case of DI frameworks, that gratuitous complexity comes with no discernible benefit (a car offers me something of value to justify its learning curve). Personally I'm of the opinion that a person shouldn't have to be a seasoned Java developer to use so many tools that are implemented in Java (or any other language, for that matter).
Operate means “run the service”. If you need to configure things which aren’t well documented, it’s nice to be able to look at the code, but the DI frameworks obscure the code path. This is pretty straightforward; no idea why this would seem like flame bait—I didn’t even realize this was something people held deep emotional attachments to.
Plumbing the construction of your object graph manually does not have a particularly high cost/benefit - most of your services are singletons that depend on each other, and it's already clear enough which ones depend on which others without repeating yourself. A very basic "here's a bag of classes that depend on each other, wire them all together and then let me pull out the instances by type" is often worthwhile for avoiding all that boilerplate, even if it does break the rules of the language a little. Something like Picocontainer or even Guice is pretty good IME.
Not using Java but generally in OO languages I ended up passing forward dependencies in grouped and themed classes like LoginDependencies, InboxDependencies etcetera. Everything under an Interface so you just mock whatever you need. Never ran into serious issues.
Of course logging might be static but “true” dependencies like networking classes never are.
I want lightweight, and compile time. But I'll take compile time only if need be.
In terms of lightweight, I have never needed to use the @Alternative binding [0]. Nearly all of my needs are met by being able to define "this is a singleton, this is a dependency that you should always inject a new instance of, and this is a property."
But it's surprisingly hard to find DI that limits itself like that. The DI in Micronaut and Quarkus are probably the closest to my ideal. Compile time, and only implement a subset of CDI etc.
You can just build your object graph and pass dependencies manually if you want a lightweight approach, no? That's just the way people do it in most languages.