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.