What dependency injection framework should you use for Ruby? It Depends!
After spending a lot of my professional life with Spring Boot, I sorely missed dependency injection when I moved to Ruby. Other offerings out there didn't come close to the SpringBoot experience, so I decided to roll my own :)
Dependency injection is great. It enables you to satisfy the D (hey now!) in SOLID so that you can decouple your code from hard-coded classes to make them more composable, and make testing much less specific to the web of objects you may have coupled yourself too! Its not just good for Java, its good for any object-orientated language. Try it and see!
Add this line to your application's Gemfile:
gem 'it_depends'
And then execute:
$ bundle
Or install it yourself as:
$ gem install it_depends
You need some magic, i.e. a magic comment on the first line of any .rb
file, and some nifty
initialize
method parameter names. Here's an example of a web of classes that should be loaded up
with some dependencies expressed. It will illustrate the main concepts of It Depends!
Please note that 3 hashes ###
indicate the start of a new .rb
file :)
### app/service/guitar/dimebag_darrell.rb
# depend_on_me(id: 'dimebag', type: 'sandblasted_skin_service')
module Service
module Guitar
class DimebagDarrell
def call
puts 'GETCHA PULL!'
end
end
end
end
### app/service/singer/chesney_hawkes.rb
# depend_on_me(id: 'one_and_only', type: 'cant_take_that_away_from_me_service')
module Service
module Singer
class ChesneyHawkes
def call
puts 'I AM THE ONE AND ONLY!'
end
end
end
end
### app/service/bass/victor_wooten.rb
# depend_on_me(id: 'victor_mfkn_wooten', type: 'funkin_service')
module Service
module Bass
class VictorWooten
def call
puts 'FUNKY D!'
end
end
end
end
### app/service/bass/bootsy_collins.rb
# depend_on_me(id: 'boots', type: 'funkin_service')
module Service
module Bass
class BootsyCollins
def call
puts 'WE WANT THE FUNK!'
end
end
end
end
### app/service/band/default.rb
# depend_on_me(id: 'greatest_band_ever', type: 'awesome_band_service')
module Service
module Band
class Default
def initialize(sandblasted_skin_service, the_one_and_only, every_funkin_service)
@guitarist = sandblasted_skin_service
@singer = the_one_and_only
@bassists = every_funkin_service
end
def shred!
@guitarist.call # 'GETCHA PULL!'
end
def wail!
@singer.call # 'I AM THE ONE AND ONLY!'
end
def slap!
@bassists.each(:call) # 'FUNKY D!'
# 'WE WANT THE FUNK!'
end
end
end
end
I mean, how awesome would this band be? Any way, what's happening here? To declare that
a class is a dependency, or requires other dependencies, we need to mark it.
That's what the # depends_on_me
magic comment is all about! More about it below:
-
The
id
parameter in the magic comment is intended to act as a unique identifier for your dependency. If you specify two classes with the sameid
value, you're gonna have a bad time. -
The
type
parameter in the magic comment is intended to allow you to group dependencies together. This unlocks the power of polymorphism; use it wisely! There's no uniqueness validation for atype
value, you can specify duplicates all over the place, but with great power comes great responsibility. More on that in a moment... -
Note that the
id
andtype
values do not relate to a file's location or the namespace defined in the file itself. This is intentional and prevents hard-coded dependencies; thank me later when you get to refactoring ;)
How do you specify that your dependency has dependencies? Glad you asked:
-
If you want to declare a dependency using a
type
value, just specify thetype
value as aninitialize
parameter. This is like using an interface for an@Autowired
constructor in Spring Boot. The concept is demonstrated by thesandblasted_skin_service
parameter forService::Band::Default.initialize()
above. Remember when I said "More on that in a moment..." above? Well, if It Depends! finds more than one dependency with thattype
value when it tries to resolve yourinitialize
parameter, you're gonna have a bad time. -
If you want to declare a specific dependency (this is akin to hard-coding another class, but I know you won't abuse it...) you can use an
id
value. To do so, pre-pend theid
value of the intended dependency withthe_
and use the resulting value as aninitialize
parameter. This is demonstrated bythe_one_and_only
parameter forService::Band::Default.initialize()
above. -
If you want the POWAH of polymorphism, you can get all dependencies tagged with a particular
type
value! This was one of the coolest features I found in Spring Boot, but its not immediately obvious that its available. So, I'm giving you the goods explicitly here! To do this, in It Depends!, pre-prend the intendedtype
value withevery_
and use the resulting value as aninitialize
parameter. This is demonstrated by theevery_funkin_service
parameter forService::Band::Default.initialize()
above. BOOM, HEADSHOT!
- It Depends! doesn't like dependency cycles. If it finds one, you're gonna have a bad time.
- It Depends! uses Zeitwerk as a code loader, so make sure you have a file structure that matches your namespace declarations. Shame on you if you don't already!
- It Depends! looks recursively through the directory you specify to
ItDepends.setup
for classes to work with so. - It Depends! will try to resolve all of your
initialize
parameters as dependencies. So, if you have aninitialize
parameter that isn't a dependency, move it to another class, s'il vous plais!
So far, I've used It Depends! in Rack applications so, in your config.ru
invoke
ItDepends.setup(path_to_your_app_directory)
at the appropriate time. This will return
a hash of all the classes with their namespace as a key, and the actual object as a value,
should you need it.
Note: path_to_your_app_directory
should be a string that defines the absolute path
to your application directory where dependencies need to be resolved. Directories in this
path need to be separated using Linux path separators, i.e. /
, and there should be no
trailing path separators!
After checking out the repo, run bin/setup
to install dependencies. Then, run rake test
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 tags, and push the .gem
file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/mlk5060/it_depends.
The gem is available as open source under the terms of the MIT License.