I'm not trying to rain on the author's parade. I genuinely don't understand the benefit—especially in relation to the complexity of adding an extra dependency and layer to my application.
I'm not trying to rain on the author's parade. I genuinely don't understand the benefit—especially in relation to the complexity of adding an extra dependency and layer to my application.
I can understand some hesitation about using a dependency injection framework. Some of them can be very hard to reason about (especially the run time frameworks). However, they don't have to be that hard to reason about. My personal favorite is a compile time dependency injection framework for scala called Macwire[1]. In scala, you might do manual dependency injection like this:
val instantiated = new Class(dependency1, dependency2)
With Macwire you could write the same thing as: val instantiated = wire[Class]
The wire macro above, automatically looks up dependencies by type and creates an instance of Class with those dependencies as arguments. If you combine that macro with Scala's built in support for laziness then you don't have to worry about initialization order either. This gives you something that is fairly easy to reason about (it is basically what your were doing by hand before) and will sort our necessary dependencies for you (and error out of they are missing).What! This is essential knowledge. Without it, how can you possibly build a coherent mental model of a program?! I’ve never heard this justification before, it’s bonkers.
EDIT - You have a tree of dependencies. This is something you care about. In order to create that tree in code, you have to convert it to a linear series of statements. This conversion is something you don't really care about as long as you end up with the tree you want. Dependency injection frameworks should make it so you can declaratively specify that tree without having to worry about how it is actually constructed.
The point being made is that just writing the construction statements in the main method is exactly "declaratively specify[ing] that tree", and that "having to worry about how it is actually constructed" is incredibly valuable knowledge to maintainers, because it allows them to see explicitly how components are constructed, and how they interact with each other. It's not something you want to hide with a framework, it's something you want to lift up, front-and-center, so everyone can see it.
I’ve found that the advantages for DI frameworks (besides encouraging more unit tests and enabling them) to be more valuable for bigger and messier projects.
Conversely, a declarative, step-wise func main, with components manually constructed and passed to each other as dependencies where necessary, is sometimes tedious, but never confusing, and always refreshingly easy to maintain, no matter how much time you've spent with the project.
var database = new DatabaseFacade(config.get("db.host"), config.get("db.port", Integer::parseInt));
var users = new UserRepository(database);
var catalogue = new ProductRepository(database);
var cart = new ShoppingCartController(users, catalogue);
It's imperative code, of course, but it's sort of declarative in that all the code is doing is wiring things up. It's step-wise in that it does one simple thing after another; it might be quite long, as there are a lot of components to create, but it can be understood locally. Components are manually constructed, by calling constructors, rather than via reflection done by the framework. Components are passed to each other as parameters to establish dependencies, rather than there being some sort of rule-driven lookup, as a framework would do. It is indeed pretty tedious to read, as it's just lots of constructor calls, with no thrilling action. But it's so simple it's never confusing (and you get to use the full power of the IDE to navigate it, jumping to definitions and uses etc). And, as such, easy to maintain. Definitely refreshingly so if you've come from Spring or Guice.The primary application I work on has hundreds of classes that get dependencies injected through DI. Many can be and are singletons, but many - including some very commonly used ones - cannot.
If I manually wired dependencies it would add thousands of lines of code, and there are some where if I changed the constructor parameters I would have to modify 1000+ call sites.
And this isn't even a particularly massive or complex application.
Remember that this setup code is just code, so if you find that adding a constructor parameter means modifying a thousand call sites, you should probably refactor it a bit first, so that it doesn't.
That being said, this approach to DI does take some tedious manual work to maintain. But by paying that cost, you get to take a magical DI framework out of the picture entirely.
https://blog.ploeh.dk/2014/06/10/pure-di/
https://stackoverflow.com/questions/6277771/what-is-a-compos...
var db IDatabase
{
if dev_mode
db = new InMemDatabase()
else
db = new PostgresDatabase(dsn)
}
var users = new UsersRepository(db)
var catalogue = new ProductRepository(db)
// ...
var cart = new ShoppingCartController(users, catalogue, logger, metrics, ...)On the other hand, I've also found myself avoiding writing code which requires deep mocks like that in order to test. Functional core, imperative shell helps with that.