Project

dio-rails

0.0
Repository is archived
No commit activity in last 3 years
No release in over 3 years
Dependency Injection for Rails
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

Runtime

~> 5
 Project Readme

Dio

Build Status

Dio is a gem which allows you to do Dependency Injection (DI) in Ruby. This is mainly made for use with Rails.

Installation

$ gem install dio-rails

Usage

Register injectable objects

You can declare that a class is injectable.

class SomeAPI
  include Dio

  # Register this class as a injectable dependency.
  injectable

  # You can also register a factory block.
  # (If a block is omitted, simply `SomeAPI.new` is registered)
  injectable :with_special_key do
    SomeAPI.new(ENV['special-key'])
  end

  def initialize(access_key = ENV['access-key'])
    @access_key = access_key
  end

  def fetch_some
    # ...
  end

  # ...
end

Or you can define a provider class. Note that you need to load this class before injection in this case.

class DepsProvider
  include Dio

  provide :foo do |name|
    Foo.new(name: name, type: :default)
  end

  provide :bar do |*args|
    BarFactory.create(*args)
  end

  # ...
end

In this provider pattern, you must specify both of a key and a factory block.

Configure injection

In a class that has dependencies, define a inject block and load dependencies it needs. dio.load runs a factory block registered with the given key. Also you can pass arguments for a factory block like dio.load(:key, :arg1, :arg2).

class SomeAction
  include Dio

  # The given block is evaluated in each instance scope so that
  # you can store loaded dependencies as instance variables.
  inject do |dio|
    @api = dio.load(SomeAPI)
    @foo = dio.load(:foo, 'my-foo')

    # @api = dio.load([SomeAPI, :with_special_key])
    # @api = dio.load(SomeAPI, 'custom-key')
  end

  def load_some
    some = @api.fetch_some
    # ...
  end
end

Apply injection

There are two ways to apply injection manually.

# Create an instance with injection.
action = Dio.create(SomeAction)

# Or inject to an already created instance.
Dio.inject(action)

Use with Rails

Controller

Include Dio::Rails::Controller to inject dependencies automatically.

class UsersController < ApplicationController
  include Dio::Rails::Controller

  attr_reader :user_detail

  inject do |dio|
    @user_detail = dio.load(UserDetail)
  end

  def show
    @user = User.find(params[:id])
    @detail = user_detail.about(user)
  end
end

Model

Include Dio::Rails::Model to inject dependencies automatically.

class User < ApplicationModel
  include Dio::Rails::Model

  inject do |dio|
    @age = dio.load(AgeCalculator)
  end

  def age
    @age.from_birthday(birthday)
  end
end

Testing

You can easily replace depdendencies for testing.

class UsersControllerTest < ActionDispatch::IntegrationTest
  setup do
    Dio.clear_stubs
  end

  test "should get index" do
    UserDetailMock = Class.new do
      def about(user)
        UserDetail::Data.new("mock", "mock detail")
      end
    end

    # Pass a hash of key-mock pairs.
    # When you load the key that is in the hash, a registered mock
    # is returned instead of the actual dependency object.
    Dio.stub_deps(UsersController, {
      UserDetail => UserDetailMock.new
    })

    get users_url
    assert_response :success
  end
end

Motivation

  • I want to separate logics from Rails controllers.
  • I don't want to instantiate a logic class in controllers. It makes it a bit cumbersome to use mocks in a test.
  • In many cases I want to use a class rather than mixin modules because:
    • Mixin pollutes name space of an includer class.
    • It is difficult to declare a private method in mixin module that can't be seen even from an includer class.
    • A class allows us to initialize it with some dependencies it needs.

Dependency injection allows us to separate instantiation of dependencies from a class depending on them.