Skip to content


Dependency Injection in Sinatra

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!

Posted in Tutorials.


0 Responses

Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.



Some HTML is OK

or, reply to this post via trackback.

 



Log in here!