"Dependency Injection is just passing parameters", "Spring is necessary to do Dependency Injection properly", "Dependency Injection is a way to escape from functional programming": these are just some of the quotes circulating about Dependency Injection. As for a technical pattern, it seems quite controversial. Is there any substance in Dependency Injection (DI)? Is this something you should be using in your application? Or maybe using DI makes you a bad programmer?
These are all good questions! However, what's often missing from discussions is an accurate description of what Dependency Injection exactly is. A good, precise definition might stop many arguments before they begin.
The classic article by Martin Fowler is a good start, however it's quite dated and although quite long, does not give an exact definition. Moreover, our usage of Object Orientation (OO) has evolved since 2004, and is increasingly blended with Functional Programming (FP), hence an update might be in order. Popular answers on Stack Overflow and the Wikipedia entry provide reasonable benefits on why to consider using DI in an application, but fail to succinctly capture the main goals of the pattern, or as many critics of DI point out, describe nothing else than passing a parameter to a function.
Let's try to clear things up!
The classes and objects that we create and use in our applications tend to fall in two categories:
- Service objects. These are "bags of methods" or "sets of functions", and are used to modularize the code base. Good examples of service objects might be
UserRepository
,EmailSender
,ProfileManager
, etc. - Data objects, either pure data or data combined with behavior ("proper" OO). Examples include
Product
,Invoice
,UserVisitor
, โฆ
Both types of objects are created at different times in the application and used in different ways.
Service objects tend to be created once at the start of the application. They don't change during the execution of the program. In other words, service objects are the modules from which your application logic is composed. Services might depend on one another, forming a dependency graph. Moreover, specific implementations of services might be interchangeable, depending on configuration, environment, command-line options, etc.
The crucial property of the service/module graph is that it is created statically. Only when the graph of services is wired, the application is usually ready to serve user requests. Hence the service objects/modules are static and global, as well as typically stateless. They only contain methods, without any (mutable) data, except maybe for configuration and pre-computed values. There are exceptions as well, such as cache services.
Data objects, on the other hand, are created dynamically, either in response to user interaction, API invocation, scheduled tasks, etc. They usually have a short, local lifespan. They carry and manipulate the data that the application processes. They might combine data and behavior, or be pure, "thin", data structures.
Summing up, we have the following properties of service vs data objects:
- static โ dynamic
- global โ local
- stateless โ stateful
But we were supposed to talk about dependency injection, where is it then?
Dependency Injection is the process of creating the static, stateless graph of service objects, where each service is parametrised by its dependencies.
That's it! This definition has several implications:
- First of all, a direct consequence is that DI doesn't mandate the use of any framework. It's perfectly feasible to do dependency injection "by hand", using just constructors. Whether doing manual dependency injection, compile-time dependency injection, or run-time DI, is an implementation detail.
Frameworks are of course allowed as well. For example, Spring is a DI framework, which uses annotations, xml and reflection to create the static service (component) graph, so it definitely matches the definition. Whether it make sense to sacrifice control over how you create objects and rely on runtime reflection is exchange for a little less code, is another debate.
Secondly, by requiring that the services are parametrised by their dependencies, we mandate inversion of control (IoC). This makes the dependencies interchangeable (e.g. for testing, or by configuration). Moreover, it's up to the wiring code (the code that creates the static object graph) to decide which specific implementation is used to satisfy a dependency; this responsibility is outside of the control of the service; the dependencies are provided externally by the DI process.
Dependency injection isn't just passing function parameters. Passing function parameters can be used to implement DI, but what matters here is why we are passing these parameters in the first place: to create the static graph of modules at the start of the application. Parameter passing is an implementation detail.
As I've also argued before, DI is different than other approaches for managing dependencies, such as the reader monad. The reader monad is a mechanism that is complementary to dependency injection, as it makes the dependencies explicit, while with DI the dependency graph is created upfront and hides the dependencies from normal method/function calls. Both mechanisms are useful for different purposes.
Finally, DI and FP are orthogonal, and DI is a perfectly valid and useful concept when doing Functional Programming. The functions that are used in FP also often need to be grouped in "modules", which is another way of saying that functions are grouped as methods into classes; they need to be parametrised by other sets of functions (other modules); and they need to be substituted depending on configuration, or in tests.
Nothing in the definition of DI forces us to use mutable data, side-effects, frameworks, impure functions etc.
To sum up, is DI machinery created to avoid FP constructs? No! It can use that machinery, for a clearly stated goal: creating the (usually static) module graph on application startup. It's not a mandatory part of every application: it's definitely viable to write good, readable code without it. But as the codebase grows, modularising the code and adding a startup step which wires all of the modules can improve the readability, explorability and maintainability of the project.
Top comments (0)