Ruby dependency injection
DI will help make your classes less complex, more flexible, easier to test and have fewer reasons to change, but achieving this goal can seem complex.
What follows is a Rails code example and refactoring demonstrating how you can remove all references to your class names from your core app code.
Dependency injection can be simple
My approach boils down to these 3 steps
- Treat instantiating collaborators as a separate concern
- Remove this concern from your application's core
- Pass in the instantiated object(s)
Fruit connoisseurs anonymous
My imaginary app will search a huge fruit database with many options including
- Country of origin
We will need an object that can translate the user input from the HTML form to an ORM friendly form that can be passed into ActiveRecord.
Some problem code (without DI)
class FruitsController < ApplicationController def search render( :search, locals: ComplexFruitSearch.new.call(params), ) end end class ComplexFruitSearch def call(params) @params = params Fruit.where(orm_friendly_params) end private def orm_friendly_params # ommited because example end end
Issues with this code:
- ComplexFruitSearch cannot be tested in isolation, causing tests to lack focus
Fruitclass requires that Rails is loaded making tests slow
- Searches can only be performed on fruits and via
Refactored with DI
class FruitApp def fruit_search ComplexProduceSearch.new( type: Fruit, ) end def vegetable_search ComplexProduceSearch.new( type: Vegetable, ) end # Potentially one method per controller action end APP = FruitApp.new class ApplicationController < ActionController::Base private # You want to expose the FruitApp to your controller actions without coupling # them to the constant set above. # # Rails lacks the extenstion points to inject your own objects so this is the # least inconvenient way I can think of. def app APP end end class FruitsController < ApplicationController def in_season_search render( :search, locals: app.in_season_fruit_search.call( scope: :in_season, params: params, ), ) end end class ComplexProduceSearch def initialize(dependencies) # Now this can even search vegetables! @type = dependencies.fetch(:type) end def call(args) @params = args.fetch(:params) @scope = args.fetch(:scope, :all) type.public_send(scope).where(orm_friendly_params) end private attr_reader :type, :params, :scope def orm_friendly_params # lots of complex logic end end
This refactoring of
ComplexProduceSearch allows an arbitrary ORM object or
scope to be passed in and is just as happy searching
The key to getting the most out of DI is that all of your objects are instantiated for you somewhere else in the system. Objects can then concentrate on doing what they do rather than who they do it with and what their dependencies might be.
This 'other place' where objects are instantiated lives at the outer edge of the system and contains knowledge of concrete implementations and which objects collaborate with which.
Here I model my application as an object that exposes a collection of services, each method providing a service that your framework can route http messages to. This object should not be unit tested, treat it as configuration or plumbing, restrict it to trivial factory methods and a few integration tests will ensure everything is wired up correctly.