The Audacious Code Experiment

Code mentoring, fun experiments, DDD and XP by Stephen Best

Ruby dependency injection

TL;DR

DI will help make your classes less complex, more flexible, easier to test and have fewer reasons to change, but achieving this goal can seem complex.

What follows is a Rails code example and refactoring demonstrating how you can remove all references to your class names from your core app code.

Dependency injection can be simple

My approach boils down to these 3 steps

  • Treat instantiating collaborators as a separate concern
  • Remove this concern from your application's core
  • Pass in the instantiated object(s)

Fruit connoisseurs anonymous

My imaginary app will search a huge fruit database with many options including

  • Country of origin
  • Sweetness
  • Colour

We will need an object that can translate the user input from the HTML form to an ORM friendly form that can be passed into ActiveRecord.

Some problem code (without DI)

class FruitsController < ApplicationController
  def search
    render(
      :search,
      locals: ComplexFruitSearch.new.call(params),
    )
  end
end

class ComplexFruitSearch
  def call(params)
    @params = params

    Fruit.where(orm_friendly_params)
  end

  private

  def orm_friendly_params
    # ommited because example
  end
end

Issues with this code:

  • ComplexFruitSearch cannot be tested in isolation, causing tests to lack focus
  • Fruit class requires that Rails is loaded making tests slow
  • Searches can only be performed on fruits and via Fruit#where

Refactored with DI


class FruitApp
  def fruit_search
    ComplexProduceSearch.new(
      type: Fruit,
    )
  end

  def vegetable_search
    ComplexProduceSearch.new(
      type: Vegetable,
    )
  end

  # Potentially one method per controller action
end

APP = FruitApp.new

class ApplicationController < ActionController::Base
  private

  # You want to expose the FruitApp to your controller actions without coupling
  # them to the constant set above.
  #
  # Rails lacks the extenstion points to inject your own objects so this is the
  # least inconvenient way I can think of.

  def app
    APP
  end
end

class FruitsController < ApplicationController
  def in_season_search
    render(
      :search,
      locals: app.in_season_fruit_search.call(
        scope:  :in_season,
        params: params,
      ),
    )
  end
end

class ComplexProduceSearch
  def initialize(dependencies)
    # Now this can even search vegetables!
    @type = dependencies.fetch(:type)
  end

  def call(args)
    @params = args.fetch(:params)
    @scope  = args.fetch(:scope, :all)

    type.public_send(scope).where(orm_friendly_params)
  end

  private
    attr_reader :type, :params, :scope

    def orm_friendly_params
      # lots of complex logic
    end
end

This refactoring of ComplexProduceSearch allows an arbitrary ORM object or scope to be passed in and is just as happy searching Fruits.all or Vegetables.in_season.

The key to getting the most out of DI is that all of your objects are instantiated for you somewhere else in the system. Objects can then concentrate on doing what they do rather than who they do it with and what their dependencies might be.

This 'other place' where objects are instantiated lives at the outer edge of the system and contains knowledge of concrete implementations and which objects collaborate with which.

Here I model my application as an object that exposes a collection of services, each method providing a service that your framework can route http messages to. This object should not be unit tested, treat it as configuration or plumbing, restrict it to trivial factory methods and a few integration tests will ensure everything is wired up correctly.

Published: