Dependency injection (DI) is a very common development practice in many languages, but its never been huge in Ruby. Part of that is because Ruby is dynamic enough that it doesn’t really need dependency injection like, say, Java. But I argue that Ruby can greatly benefit from DI. Do you use a singleton configuration object? Or worse, other singleton objects, especially those with mutable state?
def some_method(*args) foo = MyApp.configuration.foo end
Mutable singletons have ripple effects across the app and make it very difficult (and scary) to evolve. Even mostly-read configuration objects introduce tight and often invisible/forgotten coupling between objects. Have you ever written a Rackup file that conditionally decides which app to mount based on some configuration?
if configuration1 use AppMode1 else use AppMode2 end
The issue here is coupling between the routes/resources and the underlying services. Both apps must respond to the same requests, parse the arguments in the same manner, but may have inherently different behaviors. By the single responsibility principle, a standard resource layer should handle the routing and delegate the different service behaviors to a service layer. All of these are common in the Ruby community and, alas, poor development practices. But the beauty of Ruby is that the heavyweight DI containers that many languages need are overkill for most Ruby apps. Standard DI will solve these problems beautifully. So now you’re hopefully convinced that DI is the right thing to do. You may initially try
class MyApp < Stathub::Base def initialize(service) @service = service end end
but this will fail. If you realize how Sinatra is built atop Rack (e.g., by looking at the source), you realize that Sinatra apps are passed an “app” object as the first argument to the initializer. So, instead, you should do something like this:
class MyApp < Stathub::Base def initialize(app, service) super app @service = service end get("/some/route/:arg1/:arg2") do |arg1, arg2| @service.some_service(arg1, arg2).to_json end end
If you prefer to use a raw Rack app:
class MyRackApp def initialize(app, service) @app = app @service = service end end
Now your Rackup file will look more like
service = MyService.configure(MyApp.configuration) use MyApp, service
And that’s all there is to it. Go forth and inject your dependencies!