Hacker News new | ask | show | jobs
by sneak 1569 days ago
I'd like someone who knows more about software engineering than I do to tell me why I'd use this over some big globals-but-not-really App struct that holds all of the various dependencies of my app that I can just pass into the different parts of it so they can all access what they need.

If those struct members are typed to interfaces and not types, then it's just as testable, too, because you can drop in mocks as required.

What am I missing? This just seems like a way of obfuscating the fact that, in any modern program, we're going to need 3-20 "global variables" that aren't actually global global (but in practice are singletons in the process).

4 comments

We use Uber/fx in the org I work.

Took me a while to buy into it, because as you said, it doesn't seem any better than hand-written code.

But after a few years I noticed fx was being adopted by more and more teams, and it reduces cross-project contribution friction, some teams started building conventions around it, and more importantly, iterating on these conventions, etc. It's become just really handy for us.

This is many many small teams (5 people per team, more than 4k devs around many offices and countries).

Same. It’s nice to publish an fx module that implements a middleware that plugs into all of the 12000+ microservices we run. One example is adding a debug/flame graph endpoint to your service- it’s a one line import into your app.
The Go convention for adding features like that is

    import _ "example.com/feature"
Á la https://pkg.go.dev/net/http/pprof

Reads a heck of a lot easier!

If the complaint about dependency injection is that it’s magic, brittle, and difficult to debug - globals mutation via init() is much worse. In FX codebases this is heavily discouraged. The concepts of constructors and lifecycle hooks in an FX graph are not that hard to grok; you can pretty readily figure out what’s going on if you want to.
Dependency injection happens all the time in Go. It's just a lot more elegantly expressed in idiomatic Go than in FX.
The “import _” approach you recommend is idiomatic Go but it is inherently a global mutable state approach and not a DI approach.

I suppose elegance is in the eye of the beholder. FX is very elegant in my opinion: just specify your constructors and your needs, let the computer do the topo sort. But you’re right it is not idiomatic Go. Idiomatic Go is always to maximize the volume of rote, low-information-density code to accomplish any given task. Any time you are being clever and automating grunt work you are certainly violating the spirit of Go.

It sure does. But as I commented before, readability might not be the best/only metric to strive for.
It's not terrible to have one big struct, but sometimes it can inhibit the reuse of components in more than one system.

When you call a function that takes this struct as an argument, how do you know which members of the struct really need to be populated? Its dependencies aren't clearly documented. (They are over-specified. It's like a function with many unused arguments.)

If a subsystem only uses some members of the struct and you want to enforce that, maybe you pass in a smaller struct, or pass the struct members as separate arguments.

This is something you'll likely have to do if you want to extract a library for other people to use. Libraries are used in multiple systems, which might or might not have their own big struct.

> how do you know which members of the struct really need to be populated

Interfaces + duck typing? Your function shouldn’t ask for the struct, but rather for an interface describing what it needs.

Practically, though, I often just ask for the struct because I’m a slob.

An interface with many methods can have the same problem. If the function doesn't actually use every method in the interface, do you really need to implement them all?

So then it might be better for the function to declare its own interface with just the methods it uses? But then, all the callers need to be changed if you decide to call another method.

There's no principled solution to predicting what dependencies code might need someday. It's a matter of taste.

An interface with many methods is already a bad design. limiting it to a handful methods is way easier to maintain. it's fine to return an object has implement many interfaces, but you really don't need to use them all on the input side.
It's a scale thing. A bag of globals works, until the scale of what you have gets too big, and the big bag of globals becomes a 500 variable super object, and then your build graph gets bottlenecked on the bag of globals and a bunch of other pain.

If your project scale is small, you don't need DI frameworks, but you should still use IoC so you don't have implicit singleton access and your tests get flaky and stupid.

100%

I've never understood the motivation for these kinds of tools. Have always come across as over engineered.