Thursday, March 22, 2012

Injecting dependencies in Ruby

Although I have read various times that dependency injection is not needed in Ruby, it is a practice that I am very much used to thanks to the great Spring Framework and can't seem to make sense of any other way of doing things :).

In my current spare time application for example I needed to have a Recommender in my controller for recommending movies for the current user.

It works simply like this recommender.recommendations_for(user). I know that I could probably make a module and enrich my domain object with it, so I would say something like user.recommendations, and that would give me the recommendations for that user.

I will probably do that later on, but for now, I want to keep the concept of a separate recommendations engine or service.

The Recommender constructor looked something like this:


def initialize(extractor, similarity_data)



Where extractor and similarity_data are involved objects to build themselves.

I obviously didn't want to build it in the controller itself. Also I wanted a unique instance to be shared (singleton) everywhere I used the recommender, but I didn't want to use the class itself as the singleton. I wanted an actual instance of the class.

I could then create a factory method on the class to have the singleton like Recommender.getInstance, which is ok, or an independent factory, but I didn't want to do this for all the dependencies and control them individually. And it still doesn't feel as nice as the @Autowired feature in Spring.

So I decided to make a very simple way of injecting new properties in my objects. I would be depending on a central object container (Which I called ObjectContainer) that will have all the objects I intend to use for injection.

I created My ObjectContainer:

class ObjectContainer

  @objects = {}
  def self.init
    recommender = Recommender.new(FileRatingsExtractor.new, MongoBackedSimilarityData.new)

    @objects[:recommender]=recommender

    #..... more objects and stuff .......

  end


  def self.get(name)
    @objects[name]
  end

end



ObjectContainer.init




And then I created my inject support:



require_relative "object_container"

module Inject

  def method_missing(meth, *args)
    return super unless meth == :inject
    args.each do |to_inject|
       self.class_eval do
         define_method(to_inject) do
            ObjectContainer.get(to_inject.to_sym)
         end
       end
    end
  end

end



class Object
  extend Inject
end




I created a Spec like this:



require 'spec_helper'

describe "Inject Support" do

  it "should inject properties in my objects" do
    class AnyClass
      inject :recommender
    end
    a = AnyClass.new
    a.recommender.should_not be_nil
  end

end


This is in a Rails application I'm working on. The files init_object_container.rb and init_inject_support.rb are in the initializers folder. When I ran my test it was passing, showing that I have a value on the recommender property of an instane of my class AnyClass.

Now in my controller (haven't tested it yet :)) I should be able to do something like this:

class HomeController < ApplicationController
  inject :recommender

   .....

end

And doing that I would have the recommender accesible as a property in my controller.

I don't know if this is completely anti-Ruby, or anti-Rails. But it just feels very nice and unubtrusive to work like this. Not to say familiar to someone very used to DI and Spring.

2 comments:

psyho said...

I have implemented something quite similar in Dependor. Only in Dependor I also inspect the objects constructor to identify it's dependencies, so most of the time the "object container" has very little code. You can check it out here: https://github.com/psyho/dependor

Carlo on Data/Software engineering said...

I psycho thanks for leaving a comment. I will definetely take a look at your tool. Regards.