Install the gem and add to the application's Gemfile by executing:
$ bundle add active_dry_deps
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.
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
end
Rspec 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
end
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 sent
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]
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.run
Recommends using suffixes (Service
, Job
, Query
) in the name of the constant for easy reading of the dependency type.
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() # OrderMailer
For dependency testing, add the following to Rspec setup
# ...
require 'active_dry_deps/rspec'
require 'active_dry_deps/stub'
Deps.enable_stubs!
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'
end
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.unstub() for unsub all keys
expect(service_klass.new.call).to be 'failure'
end
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'
end
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.
Bug reports and pull requests are welcome on GitHub at https://github.com/corp-gp/active_dry_deps.
The gem is available as open source under the terms of the MIT License.