Dependency injection by any other name… still means you’re an idiot

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:

  1. It's not actually that different from DI, at least in terms of the net result.
  2. 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”

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.