Dependency Injection is NOT Just for Testing
There was a recent bit of noise in the Ruby community as DHH posted about the supposed evils of using Dependency Injection (DI for short). For DHH, DI is just too Java-ey, which is supposed to be a diss, I think. This brought out some agreement as well as some strong criticisms. The problem with this entire argument, though, is that it’s poised upon a dangerously faulty premise:
DI is just for testing (?)
The entire arguments, both for and against, are based on the assumption that because there are different ways to test things in Ruby, DI is awkward to use; therefore you should never use it. This is shown through the framing of all of the above posts as though DI only exists to answer the question of, "okay, now how do I test it?".
Unfortunately this premise is faulty and wrong, and thus the arguments fall flat on both sides. Yes, we don’t need to use DI to test in Ruby, but that does not mean DI has no value in the language. Quite simply, DHH is attempting to throw out the baby with the bath water on this issue.
DI is not a magical concept
Let’s start from the top with a simple revisiting of what dependency injection is:
DI is a way to expose relationships made through composition in order to influence (or control) those composed objects. Put simply, DI is basically when you pass objects into a constructor or method instead of creating them inside the method itself. That’s it. It sounds insanely simple, right? It is.
We actually do this all the time, and it’s not special. DI is just a fancy name for this concept when the compositions we influence happen to be special, like databases, or dates, or, you know, heavy external components that need to be stubbed out in testing. From the start, this does sound very useful for testing in statically typed languages where you cannot hook into any method that’s already been defined. However, that behaviour you immediately think about is a side-effect of DI, not its inherent purpose.
DI is for reuse
The real purpose of DI is that of reuse. DI allows us to take a component’s logic and substitute different compositions so that we can reuse the logic in different scenarios. Some of those compositions we might substitute might be mocks or stubs, but they might also be used in production code too, like:
- A different UI backend. A good example of this is testing frameworks (not tests themselves) which might have different test runners for different UIs—a console runner, a JUnit style XML output runner, a fancy GUI runner, and more. Because this is typically abstracted inside of a generic global "run my tests" controller class/method, there has to be a good way to inject our new backend into this controller (if we’re looking at this from MVC). Yes, we could explicitly pick our backend and run that directly with our tests, but now we’ve inverted our API; the UI is no longer a detail of the controller, it’s the primary interface with which we access the program logic. In other words, instead of asking the Controller setup the View, we’re asking the View to setup the Controller.
- A different transport backend. Tim Bray talks about the example of substituting
FakeWebin his testing example, but this may as well be an example of a real world application and not just a testing scenario. Maybe we want to take our monolithic Request class and instead run our requests over some HTTP-like protocol over UDP, or maybe even a serial port. If calling
Net::HTTP.newis buried somewhere in an internal
#sendmethod, there’s not much we can do without DI. We can monkeypatch or redefine methods directly on an instance, but these are not thread-safe or elegant solutions to the problem. DI is the elegant solution, here.
- A different database? I’m not sure why this example was lost on DHH, of all people, as DI is being used inside of his own framework and he completely forgot about it. Arguably, I’m no expert on the implementation of ActiveRecord, but conceptually speaking, almost any database-agnostic library like AR handles selecting the DB backend through DI. I’ve roughly tracked this behaviour down to AR’s
#establish_connectionmethod, which allows you to inject, via arguments, values that will influence which backend database drivers are being used in AR. That is basically DI, right there (it’s not a great form of DI, since you can only influence, not fully control, the composition, but I’m sure deeper down in the framework is a method that exposes more control).
TL;DR: you can’t hate DI because you already use it
If you use ActiveRecord and have a bunch of settings in your database.yml file, you are making use of dependency injection. You are influencing a composition through arguments passed into the connection manager. You probably use this all over your own code, too—whenever you defer the creation of an object and allow that object to be overridden by something passed in via arguments. These are all cases of DI.
In other words: DI is not magical. Don’t hate it.
If you’re in Ruby-land, or any dynamic language that lets you swap out method implementations, you shouldn’t use DI solely for testability of your APIs. And that, my friends, is really the only real point about DI being made in these posts.
To qualify the argument a little more: you should be thinking about using DI in your APIs if there are interesting compositions you want to be able to influence or substitute out—specifically large components, like databases, UIs, etc.