Improving on idiocy

I pointed out in my last post the idiot trying to improve dependency injection by reinventing it. During my rip on his ignorance, I pointed out a lot about what was wrong and mentioned a few times that I didn’t like his approach, but I didn’t provide anything better for his specific situation. I also mentioned that a service class might be a good idea… but again, I didn’t even hint at what that would look like.

So how would I solve his problem with true DI? Well, a naive approach would simply let the caller decide what class or object to use for pulling the data and parsing it:

# lib/testing.rb or whatever
module Testing
  def self.foo(klass)
    content = klass.get "http://mygreatservice.example.com/person/1.json"
    JSON.parse(content)
  end
end

# Normal use:
Testing.foo(HTTP)

# In a test:
class FakeHTTP
  def self.get(uri)
    return '{"id":1,"name":"matz"}'
  end
end

Testing.foo(FakeHTTP)

We can immediately see some benefits here over his “Injector” bullshit. For starters, there’s no dependency on HTTP at all. We just depend on some object that responds to get and returns a hash. In a truly simple case, this is a pretty good example of DI to decouple a minor dependency.

But let’s assume his Testing module is more complex, and Testing.foo does more work. In that case, we can do far better! The naive approach requires that our DI classes return a string that can be interpreted as JSON. The logic to get data and parse data is very coupled to the Testing module, which might be a problem in itself. So here’s what I’d recommend for refactoring:

# lib/service.rb
class DataProcessor
  def initialize(args = {})
    @url = args[:url]
  end

  def data_hash
    content = HTTP.get @url
    JSON.parse(content)
  end
end

# lib/testing.rb
module Testing
  def self.foo(service)
    self.do_something_with(service.data_hash)
    self.do_other_stuff
  end
end

# Normal use:
Testing.foo(DataProcessor.new(:url => "http://mygreatservice.example.com/person/1.json"))

This looks kind of awkward at first glance, and I could certainly be persuaded to change class and/or method names in a real-world scenario, but what I’m aiming for gives us a truly isolated data service that can be injected into the Testing module.

Remember, I’m assuming that the Testing module is bigger and does more stuff, hence my dummy function calls.

So now we have a central class to process data. It pulls data from an external service and parses it as JSON, returning a Ruby hash that gives us something that’s important for the rest of the module, and probably other parts of the application. Having a class for that data gathering means we can keep the “give me compatible data from some service” logic separate from whatever else this weird Testing module does.

We’re also keeping the hard-coded URL out of the service as well as the module, allowing us to configure that from whatever source we need. If we’re truly certain that the URL should be hard-coded, it’s easy enough to make that live in the service, I’m just showing that we don’t need any of the system to rely on higher-level knowledge of configurable elements.

This approach gives us some significant benefits:

  • We have a general-purpose json-service-to-hash class which may be useful elsewhere in the application
  • We can very easily inject something that reads from a file instead of pulling from a service
    • We can do the same thing with other endpoints, too – databases, console input, etc. As long as the service class returns a simple hash from its data_hash method, the Testing module doesn’t care HOW the data got there, just that it got data
  • For creating unit tests, we can stub DataProcessor or build a test-specific service class
    • Usually stubbing is easier, but we have the flexibility if we need it

Hopefully this explains DI as well as the value of service classes a little bit. And hopefully the original author of the “Interceptor Injection” pattern reads it and realizes he’s way too inexperienced to be writing about programming on the web.

Leave a Reply

Your email address will not be published.

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