Summary for those who don’t want to read yet another angry nerd rant
Dependency Injection is still Dependency Injection even if you use an approach that’s specific to Ruby 2.1 and decide to call it “Interception Injection”. Just like incompetent developers are still incompetent even if they start inventing fake patterns.
My rant
But first
Please note that I used to love Ruby as a language, and Rails as a framework. This is slowly but surely changing as I deal with more and more of the people who use these technologies. From anonymous gem authors to people I’ve worked with on large projects, Ruby developers on average are some of the worst programmers out there. I’ve worked with a couple who were brilliant, too, but in general, it seems like Ruby folks are almost proud of their ignorance.
It’s quite depressing.
What is DI?
At a high level, Dependency Injection (DI) is “a software design pattern that allows the removal of hard-coded dependencies and makes it possible to change them, whether at run-time or compile-time.”
From my admittedly limited understanding of various software patterns, this definition actually encompasses a wide variety of algorithms.
The most common approach I’ve seen is where you just pass in an instance of the thing to the consumer, rather than having the consumer create the thing itself. For instance:
class Person def create_post(title, body) post = Post.new post.title = title post.body = body post.author = self.name post.status = :draft if self.defaults.publish_now? post.publish_date = Date.today post.status = :published end post.save return post end end
Here we have a method that simplifies creating a post for a user by setting up some data based on the user’s name and preferences.
Ignore the fact that this code is not good design, as I’m just illustrating DI, okay? Thanks.
In this case, a person’s create_post
method will always rely on a class
called “Post”. This may be fine in a simple application, and it is probably
okay to stick with this kind of design if it works and gets you up and running
faster. In fact, even if you did need decoupling, I’d suggest a service class
over DI in this case, but I digress….
So one day you realize users need to create many different post-like things. Maybe comments and posts share an interface, and you want to be able to create an “asset” without worrying about its class. You could add a method, create_comment, and tie Person to yet another class… or you could use DI and let the external code tell Person to just create an asset:
class Person def create_asset(asset, title, body) asset.title = title asset.body = body asset.author = self.name asset.status = :draft if self.defaults.publish_now? asset.publish_date = Date.today asset.status = :published end asset.save return asset end end
Again, this is pretty bad design, but for the sake of this little demo, you now have a manual dependency injection system. Somebody does something like this:
person.create_asset(Post.new, "The title", "blah blah blah...")
And you have a post. Swap Post.new
with Comment.new
and you have a comment
with the same prefilled data.
Dependency Injection from the perspective of an idiot
The blog entry I mentioned above, about the so-called “Interception Injection pattern”, starts off defining dependency injection a lot closer to my first example than actual DI. He starts with this:
# testing_file.rb module Testing def self.foo content = HTTP.get "http://mygreatservice.example.com/person/1.json" JSON.parse(content) end end
and refactors to this to demonstrate DI:
# testing_file.rb require 'activeresource' class Person < ActiveResource::Base self.site = "http://mygreatservice.example.com" end module Testing def self.foo content = Person.find(1) JSON.parse(content) end end
There's no dependency injection going on here - the Testing module is no longer directly pulling from a hard-coded URL, and it's allowing the URL to be changed by making it a class variable, but it's not injecting a dependency. It's now relying on a hard-coded class to have the site as a class variable.
It's also clear he's skipped some steps in his new code, as it no longer even does the HTTP.get, but one can presume that he meant to have Person.find use Person.site to pull data and then parse it. This would remove the direct dependency on HTTP, replacing it with a dependency on Person, but we have to assume that Person is using HTTP. Since Testing has a dependency on Person and Person has a dependency on HTTP, we still have the same problem, we've simply made the URL, and nothing else, configurable.
So then he creates what he calls "Interceptor Injection":
module TestingInterceptor; end using TestingInterceptor module Testing def self.foo content = HTTP.get "http://mygreatservice.example.com/person/1.json" JSON.parse(content) end end module TestingInterceptor refine HTTP do def self.get '{"id":1,"name":"matz"}' end end end require "testing_file.rb" # this MUST follow refinements describe Testing do describe ".foo" do it "request JSON content, and returns parsed data" expect(Testing.foo).to eql({id:1, name:"matz"}) end end end
What's interesting to me is that this "interceptor" approach, while not really DI in a common form, is a lot more like DI than his "DI" example. He's using Ruby magic to allow him to dynamically inject a different implementation of HTTP.get.
Sad Panda
I don't really like this approach, however.
- He's still coupling tightly to HTTP.get, he's just making it so you can override it in a potentially confusing way: "refining" the HTTP class rather than having a clearly defined interface
- He's got to add boilerplate code to every class or module that might need to be overridden (the "using" statement and dummy module) - this is true of DI, too, but the boilerplate code has a clearer purpose rather than just saying "I add an empty class you can use if you have something you want to do."
- That "Testing" module is presumably its own thing (and practically a service
class, in fact) - for testing, why not just stub out
Testing.foo
?- If your answer is, "he might have other logic in that method that he wants to test", then you're a fucking idiot. Retrieval of data shouldn't be in the same method as a bunch of other stuff. Go home.
- If your answer is, "he was just showing an example, you angry little man", then you're still a fucking idiot. It's a shitty example that only makes one wonder in what case his so-called pattern is a good idea vs. actual dependency injection.
So we have:
- It's not actually that different from DI, at least in terms of the net result.
- It's a pretty awful approach compared to traditional DI.
Therefore, in Ruby-land, it's an amazing technological achievement that blows away DI.
But wait, there's more!
He goes on to make this awful statement:
DI frameworks replaces all of instances which defined to be replaced in configurations. We should to consider what are replaced and what are not.
A DI framework that forcibly replaces all instances may make sense when the DI framework is configured for test-only running, but DI in general isn't about just testing. It's about being able to swap compatible interfaces when necessary. It's about keeping a class less dependent on another class, and more dependent on an interface. It's the kind of thing that lets us swap SQLite and MySQL in a Rails app. Relying on interfaces is a GOOD thing in many cases. Relying on hard-coded overridable magic that only works when the main class has declared that it uses an empty class defined at the top of itself... well that's just insane. And confusing. And sad.
...
What's really depressing is that I learned about this article from Ruby Weekly. Apparently Ruby programmers are so shitty that stupid ideas are worthwhile news.
One Reply to “Dependency injection by any other name… still means you’re an idiot”