Hacker News new | ask | show | jobs
by WatchDog 1613 days ago
You can do DI without a framework.

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.

8 comments

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.

The trick is to not encourage that many things-that-create-things-that-create-things. That's a uniquely Java problem.

https://steve-yegge.blogspot.com/2006/03/execution-in-kingdo...

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.

YMMV.

> 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?

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.
Is that why Google has a DI code generation tool for Go (Wire)?
That thing that's largely not used? Sure, some person who signed Google's onerous employment contract wrote that. Look what other stuff those people are pushing https://cloud.google.com/open-cloud/ and see how it's all "enterprise solutions" while the community adoption is at 694 projects importing wire: https://pkg.go.dev/github.com/google/wire?tab=importedby
When I debug a well written C/C++ code usually the callstack is about 10 levels deep.

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.

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..)

> 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.

And adding an ORM isn't either/or. You can still use native SQL when necessary.

Yep, but if you have to change 5 constructors to get a new dependency to where it needs to be, calling `new` yourself starts to suck.
> Just call `new` yourself, instead of having the framework do it for you.

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.

by calling new yourself you get a sane stack trace when something is misconfigured. that alone is worth the tiny additional amount of code in my book.
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)
> But how do you handle configuration then ?

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.
What do you consider naive? JDBC supports transactions, I’ve had success with that.
> I consider DI a valuable pattern, but I've never experienced anything close to this need.

Literally every non-toy software I had to develop in my life required that lol

Do you seriously allow users to configure which logging framework is used through a UI? Perhaps I’m misunderstanding what you’re saying.
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)
My company chose exactly this design for several of our microservices. It is now almost universally considered to have been a mistake.
Why? What happened?
It very quickly becomes an unmaintainable mess once the service grows past a certain size.
But, how, exactly?

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?

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..

Another useful feature of Spring is aspect-oriented-programming (like when we manage transactions boundaries with @Transactional).

Spring takes care of that, but doing it manually (and without dynamic proxies) would add to the verbosity.

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();
    }
How is it better to make a dev write out that plumbing and others reread it? I’m made of meat, so I want to automate everything we safely can.
Advantages:

- 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.

> don’t need to learn

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.
You’re reading and writing the “plumbing” no matter what you do. So, does it matter if you are writing a config file or Java code?
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.