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.
Under the hood active_dry_deps
uses a container like dry-container and convert key to underscore for fetch from container.
For auto-registration dependencies use dry-system.
MyApp::Container.register('warehouse.create_departure_service', Class.new { def self.call = 'failure' })
include Deps['Warehouse::CreateDepartureService.call']
Deps['Warehouse::CreateDepartureService.call']
this notation is familiar to Ruby developers, helps to find code in the project, and simplifies the migration from constants in code to defining dependencies.
class CreateOrderService < ServiceObject
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
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 inject any method from object in your container
MyApp::Container.register(:str, 'str')
MyApp::Container.register(:service, Module.new { def self.success? = true } )
include Deps['str.reverse', 'service.success?']
reverse # => "rts"
success? # => true
By default, when call
or perform_later
methods are imported, the name of the dependency is taken from the name of the constant:
include Deps[
'Warehouse::CreateDepartureService.call', # callable
'Warehouse::ReserveJob.perform_later', # callable
'Warehouse::ReserveJob.perform_now',
'Warehouse::ProductActivateQuery',
]
# use as
CreateDepartureService()
ReserveJob()
perform_now
ProductActivateQuery().run
Recommends using prefixes (Service
, Job
, Query
) in the name of the constant for easy reading of the dependency type.
include Deps[string: 'str.reverse', m: 'module']
string # => "rts"
m # => "success"
gem adds rspec matcher for stub dependency, put require 'active_dry_deps/rspec'
to rspec setup
GpApp::Container.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
it 'stub' do
Deps.stub('Order::Dependency', double(call: 'success'))
expect(service_klass.new.call).to be 'success'
Deps.unstub('Order::Dependency') # or simple Deps.unstub for unsub all keys
expect(service_klass.new.call).to be 'failure'
end
gem auto-configuring, but you can override settings
# config/initializers/active_dry_deps.rb
ActiveDryDeps.configure do |config|
config.container = 'MyApp::Container'
config.inflector = ActiveSupport::Inflector
config.inject_global_constant = 'Deps'
end
Recommended container setup with dry-system for Rails
config/initializers/system.rb
require 'dry/system/container'
module GpApp
class ContainerRailtie < Rails::Railtie
LOADER =
Class.new(Dry::System::Loader) do
def self.call(component, *args)
constant = self.constant(component)
if singleton?(constant)
constant.instance(*args)
else
constant # constant.new(*args) - THIS LINE REWRITED from Dry::System::Loader
end
end
end
# https://api.rubyonrails.org/classes/Rails/Railtie.html
# Add a to_prepare block which is executed once in production
# and before each request in development.
config.to_prepare do
ContainerRailtie.finalize
end
def finalize
container = create_container
set_or_reload(:Container, container)
if (providers_path = Pathname(__dir__).join("../system/providers")).exist?
Dry::System.register_provider_sources(providers_path.realpath)
end
container.finalize!(freeze: !Rails.env.local?)
end
def create_container
Class.new(Dry::System::Container) do
configure do |config|
config.inflector = ActiveSupport::Inflector
config.root = Rails.root.join('app')
%w[domains jobs queries services mailers].each do |dir_name|
config.component_dirs.add dir_name do |dir|
dir.loader = LOADER
dir.memoize = true
end
end
config.component_dirs.add '../lib' do |dir|
dir.loader = LOADER
dir.memoize = true
end
end
end
end
def set_or_reload(const_name, const)
remove_constant(const_name)
GpApp.const_set(const_name, const)
end
def remove_constant(const_name)
if GpApp.const_defined?(const_name, false)
GpApp.__send__(:remove_const, const_name)
end
end
end
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.