Dependency Injection in 10
This is the English version of this article.
この記事の日本語版はこちらです。
Here’s the 10 minute version of my explanation of dependency injection.
What is it?
Dependency injection is a way to write code such that external resources are accepted as arguments, rather than being hard-coded within a particular construct. The primary reason to do so is to facilitate testing.
Here’s the stereotypical example.
fetch_data
One day, your boss asks you to write a piece of code that connects to a database and does something with the resulting data.
def fetch_data(criteria):
database = connect_to_real_database('1.2.3.4:5555')
# Handle a special case before querying.
if criteria.has_as_special_case():
criteria.fix_the_special_case()
return database.query(criteria)
You’re done! You go to your boss. He looks at the code and says, “Misha, I know you think this works, but the special case handling is pretty complicated. Can you write a test for this function?”
Testing fetch_data
Testing the current implementation is hard because the database connection has been hard-coded into the function. In this case, it’s the production database, and you most certainly don’t want to run your test in production!
So you decide to change the function a little bit.
def fetch_data(criteria, host):
database = connect_to_real_database(host)
# Handle a special case before querying.
if criteria.has_as_special_case():
criteria.fix_the_special_case()
return database.query(criteria)
This is much better. Now you can just set up a different environment, and test against that database instead. Crisis avoided!
You go to show the new function to your boss, but he says, “Misha, I think you need to test all sorts of criteria - maybe even tens of thousands of variations. Isn’t this function going to be too slow for that?”
Efficiently Testing fetch_data
The boss is right (this time, at least). Setting up a database and communicating with it during tests is quite slow. You’re going to need a mock, in-memory database for this, which is significantly faster.
With that, you decide to rewrite the function even further.
def fetch_data(criteria, database):
# Handle a special case before querying.
if criteria.has_as_special_case():
criteria.fix_the_special_case()
return database.query(criteria)
You show the boss the final code, and he pats you on the back and says, “Nice work!”
Conclusions
The final version of fetch_data
doesn’t really care where the database is, or whether it’s even real. The tests for this function will exercise only the logic that makes the function complex. The tests are precise, fast, and simple.
Writing code that accepts its dependencies as arguments is known as dependency injection. As we saw in the example above, it leads to more testable code.
Caution
The code that sets up the database is, admittedly, slightly more tricky to test. But the setup logic generally doesn’t change much compared to functions like fetch_data
, so you don’t need such complex tests there to begin with. I consider this an acceptable tradeoff.
There are other ways to write more testable code, too. For example, languages that offer syntax for managing side effects are implicitly “dependency injected” due to the language constructs, so there’s no real need to think about dependency injection itself.