A quick history on Dependency Injection (DI) because I’ve experienced 2 extremely visceral reactions to it by experienced developers in the past year. I’m concerned either there are more than 2 devs who have a bad impression of it, or some are spreading misinformation which can negatively affect how you build & test your software.
One developer I debated with this on has a training course, and it looks quite thorough and good based on a 5 minute scan. That is also worrisome because in our debate he said “DI injection has nothing to do with testing” and “adding DI in just so it’s ‘easier’ to test is a terrible choice. DI doesn’t make testing easier. It makes testing systems that use DI easier, and that’s a big distinction.”
Given the amount of AI psychosis going around currently, I’ve been introspective to ensure it’s not “me” that’s confused. When they say “AI does all coding”, no it doesn’t. When they say “AI is worthless”, no, it can be helpful. I’ve used various models, tools, and practices to learn. I’m left with either I’m a rational centrist… or both of them are lying or crazy… wait, am I the one who is crazy?
For DI, I have no insecurities. The industry has been doing this for 40+ years, and for good reasons. Over half to do with testing, the rest with design despite design being the origins. It’s long and documented. To say it has nothing to do with testing, or is as bad choice is just wrong. To build a training course without using DI at this point is willful ignorance.
Introduction
There is some negative cultural stigma associated with DI because in early Java, there was a large exodus of developers to other languages. While Java development & industry use remains strong, so to does the stigma of “Java esque” connotations.
I hate when YouTube videos dive into history for 7 minutes instead “just tell me why DI is good/bad!”. DI is good, for both Object-Oriented Programming and Functional Programming. However, to understand why it’s so tainted, understanding the history explains why you get completely different reactions from from developers when it “seems so obvious to just use it”.
I get it, but you’ll need to have this context if you ever start “just coding, DI is a normal part of that” and you have a new co-worker or client who freaks out, or the reverse, you see they use no DI in their code, and their tests are… “interesting”. (That’s tact for “I need a new job, I can’t work with this code base”).
Java/.NET Egress
Many Java developers in the 2000’s fled to Ruby and Rails with it’s simplicity and basically one way to do things. A lot less code, a lot less configuration (hence the convention over configuration mantra they have).
Years later, many did the same to Node.js. Specifically, JavaScript on the server, not the client. While it may seem normal to lump JavaScript server-side developers with web developers, they are definitely two different communities and do not always agree.
Understand that many in this world WERE OOP devs at heart, they just disagreed with the approach Java took, and were heavily prejudiced against the complexity. Understand, too, the age differences. Some were from a C++ background from the 80’s and 90’s, and had similiar feelings. Others were much younger, but after a short stint in both languages, without all the experience, and context, many were quick to say “This seems much simpler than Java”. So you had a type of cargo culting happening as well to varying degree’s, but it was encouraged, sometimes unknowingly.
The same with Python and Django. What all 3 had in common was they still supported Object-Oriented Programming (OOP) paradigms, but without the low return on investment type ceremony in early Java, and all the configuration complexity. This meant getting up to speed, from a variety of skill levels, was much easier, but you could still pick and choose to bring what you knew. The same applied to early .NET/C# as well. Want to use the same class keyword, and a few OOP design patterns? Works about the same. What to build a lighter weight Inversion of Control framework? Much easier to do in dynamic languages.
These are the people who taught me.
Why DI?
DI was created in the 80’s to make configuring OOP code bases easier. Whether using a concrete class, or an interface, you could make your classes more flexible by injecting the class or interface instead of having the class build itself. Classes are often a black box; that’s the whole point of OOP, encapsulation & abstraction (I’m ignoring message passing because that is a depressing path).
Instead of a class instantiating it’s own logger:
class MyGame {
constructor() {
this.logger = new Logger()
}
movePlayer() {
this.logger.info("Moved to new position.")
}
}Code language: JavaScript (javascript)
You’d instead inject a logger instance in the class constructor:
class MyGameDI {
constructor(logger) {
this.logger = logger
}
movePlayer() {
this.logger.info("Moved to new position.")
}
}Code language: JavaScript (javascript)
This was often called “Inversion of Control”. Before, the class you created had control of what it created and when, but now, it’s done by classes higher up the class tree. The advantages were it was easier to swap out a new class that did something different (in-memory logger for profiling vs disk), or multiple implementations (decoding mp3, ogg, wav, etc files for an audio player).
Around the same time (in non-Java circles) there was a small rejection of inheritance being a good idea. It took awhile, but many in OOP felt composition, having classes inside of classes instead of extending a base class, was a better idea for easier to maintain code. DI helped make composition easier and throughout the late 90’s and early 2000’s, the “Composition Over Inheritance” phrase grew in popularity. The cargo culting here was less because all a developer had to do was create large inheritance hierarchies, then maintain the mess they created. So the general idea that classes should not create their dependencies, but instead receive them from the outside became a lot more standardized (but still not widely accepted).
Years later it was also discovered this made testing a whole lot easier because the tests could inject fake versions and the real code could inject the real versions, but your main source code didn’t need to change. This really helped software quality and speed, while still retaining the design & architecture benefits.
Testing
During the 90’s, developers from many backgrounds started recognizing DI’s usefulness in testing. At the same time, a ground swell was happening in both how you test, and how you build software. Kent Beck’s XP book, which talked about Test Driven Development, came out in 1999, and then 2 years later in 2001, the Agile Manifesto came out. A year later, the Spring Framework for Java came out, utilizing the “DI Container”.
Despite the $500,000,000 Sun marketing budget for Java and OOP, there was also this growing sense of software craftsmanship culture growing. 3 years later in 2004, Martin Fowler wrote the seminal “Inversion of Control Containers and the Dependency Injection pattern” that further popularized and cemented DI as the term.
From then on, you had this perception that DI was strictly for testing, and had nothing to do with design despite DI’s original purpose being for design.
Containers
What never finalized was “who creates the dependencies” in production code. In tests it was straightforward; the test. However, with the advent of Spring, DI Containers, e.g. “things to make the dependencies” became the norm in multiple languages, namely Java, .NET, and even in niche communities like ActionScript in Flash. These started as a class with no state; just 2 static methods that “make the production dependencies” and “make the test dependencies”. They weren’t all bad; having a global way to inject different behaviors was powerful, no doubt, but they never stayed simple.
The myriad of type systems, and their continued low value also complicated approaches. Some used interfaces “because that’s what the Java OOP people do”, whereas in the dynamic languages, you didn’t need any of that.
The complexity just spread from there, eventually infecting the tests. Around the same time, JUnit and other frameworks inadvertently created confusion around which test-double to use, when. To this day, even with Gerard Meszaros’s definitions, no one can agree on what they mean.
The Python, Ruby, JavaScript, and other communities rejected wholesale any type of DI Containers, but oddly imported the test-double confusion.
They keep trying to make a comeback in various languages with various success and failures. .NET continues to have many projects happily using DI containers, whereas in Node.js NestJS (Nest, not Next)does have some traction, but goes against the core, original Node.js rejection of overly complicated Java DI setups and design pattern soup.
Functional Programming and DI
In the background, Functional Programming was still practiced in niches and dark shadows. Many intelligent programmers visited, and some of those came back with good ideas, most of the smarter ones not explaining where they got them to ensure better adoption.
Two particular things that helped popularize DI in the FP world was:
- some FP languages had side-effects
- the type systems removed the need for interfaces
And, a 1b, those practicing FP ideas in non-FP languages also had to deal with side-effects.
If you’re not an FP developer, I cannot underscore how much FP developers detest side-effects and spend every waking moment attempting to avoid side-effects. This is because the draw to FP is determinism. If you base all your code on math, and math is always right, and always wrong, then you feel super comfortable and excited to build an entire program out of it… because your program will be predictable.
Except, that’s not how the real world works; at a minimum, you have side-effects, things that aren’t deterministic like reading a file, connecting to a server, or accessing environment variables. Will they work? Who knows.
Since everything in functional programming (Scala/OCAML being weird) is usually functions, that means dependencies are passed in as parameters, just like you’d do it in class constructors.
function movePlayer(logger) { ... }Code language: JavaScript (javascript)
There is one thing you can do, though, even in non-FP languages, and that’s build pure functions that always work, or always fail. That means, using stubs, you can use DI to inject happy path stubs and unhappy path stubs for testing, just like you’d do in OOP.
stubInfo = (...rest) => {}
stubLogger = { info: stubInfo }
result = movePlayer(stubLogger)Code language: JavaScript (javascript)
That’s the reason you never see “DI Containers” in FP languages because why would you build a framework around passing a parameter to a function?
The last part is types. Type systems traditionally have been much better in FP languages. This means creating those stubs in FP tests is easier because the function has to match a type. Interfaces in classes typically start with 1 method, but never stay that way because it’s too easy to add more, which is why it gets harder and harder to create test-doubles for things in older OOP projects. In FP, a function type is 1 function type; that’s it. You can easily create a function that returns a particular value in a test. The types ensure they match; no need for interfaces, importing those interfaces, then creating a fake class to implement that interface. This varies between languages; some are easier, but less type safe, and some are more type safe, but harder to stub.
Either way, while one could say DI exists naturally FP, it’s quite the opposite; passing parameters to functions has been normal since functions and methods were created. OOP became so complicated, they had to invent Inversion of Control, then rebrand to Dependency Injection, cut their teeth on the mostly bad parts of DI containers, then learn that “huh, you know, instead of Constructor Injection, you could just pass the dependencies in on the method that needs it, we could call it Parameter Injection, like… passing a parameter to a function”.
Conclusions
DI has a 40+ year history from starting as a code design tool to eventually becoming a core testing tool for both OOP and FP languages over 20 years ago. It is a powerful design tool, and powerful testing tool, and it’s normal, and expected to use it. If someone says it shouldn’t be used for testing, they don’t know what they’re talking about.
Leave a Reply