Payload
¶ ↑
Payload
is a lightweight framework for specifying, injecting, and using dependencies in Ruby on Rails applications. It facilitates run-time assembly of dependencies and makes it plausible to use inversion of control beyond the controller level. It also attempts to remove boilerplate code for common patterns such as defining factories and applying decorators.
Overview¶ ↑
The framework makes it easy to define dependencies from application code without resorting to singletons, constants, or globals. It also won't cause any class-reloading issues during development.
The central object in the framework is a “container.” Dependencies are specified using Ruby configuration files. Configured dependencies are then available anywhere a reference to the container is available.
Define simple dependencies in config/dependencies.rb
using services:
service :payment_client do |container| PaymentClient.new(ENV['PAYMENT_HOST']) end
The configuration block receives a reference to the container, which will contain all configured dependencies. This makes it possible to define dependencies without knowing how or when their sub-dependencies will be defined.
Controllers receive a reference named dependencies
, so you can easily inject dependencies into controllers.
For example, in app/controllers/payments_controller.rb
:
class PaymentsController < ApplicationController def create payment = Payment.new(params[:payment], dependencies[:payment_client]) receipt = payment.process redirect_to receipt end end
You can easily test this dependency in a controller spec:
describe PaymentsController do describe '#create' do it 'processes a payment' do payment_params = { product_id: '123', amount: '25' } client = stub_service(:payment_client) payment = double('payment', process: true) Payment.stub(:new).with(payment_params, client).and_return(payment) post :create, payment_params expect(payment).to have_received(:process) end end end
You can further invert control and use factories to hide low-level dependencies from controllers entirely.
In config/dependencies.rb
:
factory :payment do |container, attributes| Payment.new(attributes, container[:payment_client]) end
In app/controllers/payments_controller.rb
:
class PaymentsController < ApplicationController def create payment = dependencies[:payment].new(params[:payment]) payment.process redirect_to payment end end
You can also stub factories in tests:
describe PaymentsController do describe '#create' do it 'processes a payment' do payment_params = { product_id: '123', amount: '25' } payment = stub_factory_instance(:payment, payment_params) post :create, payment_params expect(payment).to have_received(:process) end end end
The controller and its tests are now completely ignorant of payment_client
, and deal only with the collaborator they need: payment
.
Setup¶ ↑
Add payload to your Gemfile:
gem 'payload'
To access dependencies from controllers, include the Controller
module:
class ApplicationController < ActionController::Base include Payload::Controller end
Specifying Dependencies¶ ↑
Edit config/dependencies.rb
to specify dependencies.
Use the service
method to define dependencies which can be fully instantiated when the application boots:
service :payment_client do |container| PaymentClient.new(ENV['PAYMENT_HOST']) end
Other dependencies are accessible from the container:
service :payment_notifier do |container| PaymentNotifier.new(container[:mailer]) end
Use the factory
method to define dependencies which require dependencies from the container as well as runtime state which varies per-request:
factory :payment do |container, attributes| Payment.new(attributes, container[:payment_client]) end
Any additional arguments passed to new
when instantiating the factory will also be passed to the factory definition block.
Use the decorate
method to extend or replace a previously defined dependency:
decorate :payment do |payment, container| NotifyingPayment.new(payment, container[:payment_notifier]) end
Decorated dependencies have access to other dependencies through the container, as well as the current definition for that dependency.
Using Dependencies¶ ↑
The Railtie inserts middleware into the stack which will inject a container into the Rack environment for each request. This is available as dependencies
in controllers and env[:dependencies]
in the Rack stack.
Use []
to access services:
class PaymentsController < ApplicationController def create dependencies[:payment_client].charge(params[:amount]) redirect_to payments_path end end
Use new
to instantiate dependencies from factories:
class PaymentsController < ApplicationController def create payment = dependencies[:payment].new(params[:payment]) payment.process redirect_to payment end end
All arguments to the new
method will be passed to the factory definition block.
Grouping Dependencies¶ ↑
You can enforce simplicity in your dependency graph by grouping dependencies and explicitly exporting only the dependencies you need to expose to the application layer.
For example, you can specify payment dependencies in config/dependencies/payments.rb
:
service :payment_client do |container| PaymentClient.new(ENV['PAYMENT_HOST']) end service :payment_notifier do |container| PaymentNotifier.new(container[:mailer]) end factory :payment do |container, attributes| Payment.new(attributes, container[:payment_client]) end decorate :payment do |payment, container| NotifyingPayment.new(payment, container[:payment_notifier]) end export :payment
In this example, the final, decorated :payment
dependency will be available in controllers, but :payment_client
and :payment_notifier
will not.
You can use this approach to hide low-level dependencies behind a facade and only expose the facade to the application layer.
Testing¶ ↑
To activate testing support, require and mix in the Testing
module:
require 'payload/testing' RSpec.configure do |config| config.include Payload::Testing end
During integration tests, the fully configured container will be used. During controller tests, an empty container will be initialized for each test. Tests can inject the dependencies they need for each interaction.
This module provides two useful methods:
-
stub_service
: Injects a stubbed service into the test container and returns it. -
stub_factory_instance
: Finds or injects a stubbed factory into the test container and expects an instance to be created with the given attributes.
Contributing¶ ↑
Please see the contribution guidelines.
License¶ ↑
Payload
is Copyright © 2014 Joe Ferris and thoughtbot. It is free software, and may be redistributed under the terms specified in the LICENSE file.
Payload
is maintained and funded by thoughtbot, inc.
The names and logos for thoughtbot are trademarks of thoughtbot, inc.