So why is this design bad? It seems like when you actually start running into scaling issues that singleton translates naturally into borrowing from a shared connection pool.
It's much more ergonomic, flexible, testable, and configurable to inject that connection pool thing instead of bodging it together in the static initializer. Classes shouldn't be managing resources of which they are properly only consumer...
Do you not just mock the singleton? I mean don't get me wrong, it has all the usual global variable downsides but I don't see too much a meaningful difference between every method passing around the same connection pool handle explicitly vs ambiently. And either you can mock the connection objects the singleton returns or you can't.
In most apps I've seen "services" like Redis/Rabbit/Memcache/Postgres/External APIs/Storage are cross-cutting concerns and you make your life a nightmare by having to pass
because if you realize deep in your call stack you actually need data from Postgres now you have change all the callers recursively to pass the handle down. If you have this global "service catalog" it's almost always to eliminate the need for passing the same effectively global connection pools to hundreds of call-sites.
It is annoying that it makes every test require a bunch of ambient service mocks but you only really have to set it up once.
Yeah, sure, if you declare every variable global, it makes adding new features feel quick and easy. There's a reason that global variables are considered harmful.
I completely agree with you! But we're talking about one of the few exceptions -- cross-cutting concerns.
I think just about everyone would scoff at the idea that every single method in your codebase should take an explicit `logger` parameter rather than just having a global logger object.
So now we're looking as a case where you have a bunch of services: dbs, caches, apis, storage that are used all over the app ("in every file") and you have to make a judgement call.
* Have hundreds (honestly thousands at current $dayjob) of methods that do a lot of work just to pass around the same connection handle.
* Declare a single object that manages all the connection pools.
I think it's really hard to escape the fact that connection pools are actually global and you either have to admit this or have your runtime hide it from you.
Another example of a cross-cutting concern that runtimes usually hide is event loops. Can you imagine if every function that wanted to use async had to be passed an event_loop variable?