Both DHH and Tim Bray make great points about "dependency injection" and its issues regarding Ruby and testing. My colleague Adam Keys makes a similar point, though doesn't call his anti-pattern "dependency injection".
The scare quotes are because neither DHH nor Tim are accurately representing the purpose of dependency injection. Dependency Injection is not about a framework, XML, factories, or testing. It's about simplifying code. Let's see how.
I'm going to ignore Rails for the moment, and just talk about designing classes in Ruby1. Let's take the example domain from my previous post, and expand it a bit. We start with a class that implements purchasing logic:
class PurchaseProcess def purchase_product(customer,product) customer.charge(product.price) if product.price > 0 customer.invoices << Invoice.new(product) end end
I've simplified things a bit, but basically we charge the customer, create an invoice, and add it to their invoices. This class gets used as part of the regular purchase flow of our website. Suppose that we want to run a promotion such that, whenever someone signs up for our mailing list, they get a free keychain. They'll get this keychain the same as if they bought it, but it's a special product with a price of zero, so they get it for free.
Let's make a class for this.
class KeychainPromotion def sign_up_for_mailing_list(customer) MailingList.add(customer) keychain = Product.find("promo-keychain") PurchaseProcess.new.purchase_product(customer,keychain) end end
This is straightforward, simple code. We add the customer to the mailing list, find the promotional keychain, and "purchase" it.
What are the dependencies of this class?
- An instance of the
Of those four dependencies, only three are directly related to the business process of our promotion. The
object is only needed to create an instance of that class so we can call
purchase_product on it. Let's inject this dependency
and see what happens.
class KeychainPromotion def initialize(purchase_process) @purchase_process = purchase_process end def sign_up_for_mailing_list(customer) MailingList.add(customer) keychain = Product.find("promo-keychain") @purchase_process.purchase_product(customer,keychain) end end
Our class is a bit longer, but it has fewer dependencies. It also does fewer, things, making it more cohesive. It's only about this promotion, and no longer about creating instances of a shared class. If the way in which
PurchaseProcess instances get created needs to change, we will not have to change this class, meaning its fan out is lower. It is simpler, by any objective definition.
Other than simplicity, what are some advantages of this approach?
- We can use an alternate purchase process if we want. Swapping purchase processes is certainly YAGNI, but it's not hurting anything and it's a nice benefit.
- Flexibility in testing. Since we no longer depend on the
PurchaseProcessobject, we have more options regarding how we test
KeychainPromotion. Before, our only option was to mock/stub
new, but now we can just pass in anything that responds to
purchase_product. Our test will be simpler because of this.
Note that these are side benefits, not ends unto themselves. Code that's easier to test isn't better because of that fact - it's the other way around. Code that's well designed is easier to test. We have some objective measures of the quality of code, and many of them lead to simpler testing.
This is the mistake that both DHH and Tim make in their posts - they assume that the reason for using dependency injection is to make your code "easier" to test. In DHH's (and Adam's) case, they rightly call out method-level injection as bad. I would agree, and, outside of the mind-bending Scala collections framework, you don't see it much. Tim asserts that DI is nothing but needless indirection, but that's not it at all.
Suppose we want to inject more dependencies into our
KeychainPromotion class. Let's replace the "hard-coded" dependency on
Product object with an injected finder and see what happens.
class KeychainPromotion def initialize(purchase_process,product_finder) @purchase_process = purchase_process @product_finder = product_finder end def sign_up_for_mailing_list(customer) MailingList.add(customer) keychain = @product_finder.find("promo-keychain") @purchase_process.purchase_product(customer,keychain) end end
Is this better? We haven't reduced the number of dependencies on this class, even though we increased the complexity of the
sign_up_for_mailing_list method is also more complex because it now depends on a new ivar,
which has a higher scope than a direct use of
Product. While it's true that
KeychainPromotion is more flexible and we have more flexibility in testing it, we've made the code itself more complex.
I would argue that injecting this particular dependency is not an improvement, and that this code is worse than before.
Is that the fault of the concept of Dependency Injection? Of course not. Dependency Injection, like any other pattern, technique, or tool, is only useful when it's used properly. It's true that dynamic languages like Ruby provide many other tools that solve problems that Dependency Injection can also solve, and they often do it better. But it doesn't mean that the entire concept is worthless.