Installation
Install the gem and add to the application's Gemfile by executing:
$ bundle add active_dry_deps
Dependency Injection
Dependency injection helps to break explicit dependencies between objects making it much easier to maintain a single responsibility and reduce coupling in our class designs. This leads to more testable code and code that is more resilient to change.
For a deeper background on Dependency Injection consider the Wikipedia article on the subject.
Usage
Basic
Dependencies are injected by listing their names: Deps['Warehouse::CreateDepartureService.call']. This notation is familiar to Ruby developers. It helps to find code in the project (compares to abstract container keys), and simplifies the migration from constants in code to defining dependencies
class CreateOrderService
  include Deps[
    'Warehouse::CreateDepartureService.call',
    'Warehouse::ReserveJob.perform_later',
    'OrderMailer',
    'redis',
    track: 'StatsApi.message',
  ]
  def call(params)
    order = Order.create(params)
    ReserveJob(order)
    track(order.id, order.created_at)
    redis.with do |conn|
      conn.incr('order_count')
    end
    OrderMailer().with(user: user).deliver_later
    CreateDepartureService(order.slice(:id, :departure_at))
  end
endRspec matcher deps allows to isolate dependencies in tests. It simplifies unit testing
Rspec.describe CreateOrderService do
  it 'success create order' do
    service = described_class.new(user: create(:user), zip_code: 67_345)
    expect(service).to deps(CreateDepartureService: double(success?: true), ReserveJob: spy, track: spy)
    expect(service.call.success?).to be true
  end
endRegister custom dependency
You can define an arbitrary object as a dependency with method Deps.register
class OrderMailer
  def send_mail = 'email sent'
end
Deps.register('mailer') { OrderMailer.new }
class CreateOrderService
  include Deps['mailer']
  def call
    mailer.send_mail
  end
end
CreateOrderService.new.call # => email sentImport methods
You can inject any method from constant as dependency
class OrderRepository
  def self.overdue_order_ids = [1, 2, 3]
end
include Deps['OrderRepository.overdue_order_ids']
overdue_order_ids # => [1, 2, 3]Import callable methods
There is a special convention for naming some methods. By default, when call or perform_later methods are imported, the name of the dependency is taken from the name of the constant, not by method name
include Deps[
  'Warehouse::CreateDepartureService.call', # callable
  'Warehouse::ReserveJob.perform_later', # callable
  'Warehouse::ReserveJob.perform_now',
  'Warehouse::ProductActivateQuery',
]
# use as
CreateDepartureService() # Warehouse::CreateDepartureService.call
ReserveJob() # Warehouse::ReserveJob.perform_later
perform_now # Warehouse::ReserveJob.perform_now
ProductActivateQuery().run # Warehouse::ProductActivateQuery.runRecommends using suffixes (Service, Job, Query) in the name of the constant for easy reading of the dependency type.
Aliases
Dependency can have an alias for more intuitive access. Keep in mind that dependencies with aliases should go at the end of the list (this is Ruby feature)
include Deps['OrderMailer', product_repo: 'Warehouse::ProductRepository']
product_repo # Warehouse::ProductRepository
OrderMailer() # OrderMailerTests (Rspec)
setup
For dependency testing, add the following to Rspec setup
spec/rails_helper.rb
# ...
require 'active_dry_deps/rspec'
require 'active_dry_deps/stub'
Deps.enable_stubs!
RSpec.configure do |config|
  config.after(:each) { Deps.reset }
enddeps
The gem adds Rspec matcher deps for stub dependency
Deps.register('order.dependency', Class.new { def self.call = 'failure' })
let(:service_klass) do
  Class.new do
    include Deps['Order::Dependency.call']
    def call = Dependency()
  end
end
it 'failure' do
  expect(service_klass.new.call).to be 'failure'
end
it 'success' do
  service = service_klass.new
  expect(service).to deps(Dependency: 'success')
  expect(service.call).to be 'success'
endstub, unstub, reset
Dependency can be stubbed at the container level. This allows to override all calls to it
it 'stub' do
  Deps.stub('Order::Dependency', double(call: 'success'))
  expect(service_klass.new.call).to be 'success'
  Deps.unstub('Order::Dependency') # or Deps.reset for unsub all keys
  expect(service_klass.new.call).to be 'failure'
endglobal_stub, global_unstub
Sometimes it is necessary to stub dependencies for all or almost all tests
spec/rails_helper.rb
# ...
Deps.enable_stubs!
Deps.global_stub('PushService', Class.new { def self.call = 'global-stub-push' })Dependency stubbed with global_stub may be restored only with global_unstub. You can unstub dependency when it really needed and ignore in all other cases
it 'sends webpush' do
  Deps.global_unstub('PushService')
  
  # expect(PushService.call).to ...
endDeps.global_stub should not be used within examples
Configuration
The gem is auto-configuring, but you can override settings
# config/initializers/active_dry_deps.rb
ActiveDryDeps.configure do |config|
  config.inflector = ActiveSupport::Inflector
  config.inject_global_constant = 'Deps'
endDevelopment
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/corp-gp/active_dry_deps.
License
The gem is available as open source under the terms of the MIT License.