This technique is widely used in many programming languages, from Java to PHP and JavaScript. In brief, it removes direct dependencies from your classes, replacing them with dependencies upon abstractions instead. Concrete instances of the abstractions are “injected” in the initializer as parameters. This is Inversion of Control – objects do not instantiate their own dependencies, the dependencies are provided from the outside. Alas, it can be very messy handling all the injected dependencies manually, especially when you have a cascade of dependencies. An IoC Container is that mysterious “someone” who instantiates all objects in the app. It uses class annotations to instantiate the correct dependencies to inject. The concrete classes for each abstraction are often specified in the IoC config. Interfaces are natural abstraction identifiers, in languages that support them, but Ruby does not have interfaces. That is why dependency-injected classes require a synthetic dependency specification in Ruby.
Some time ago, DHH wrote a critique of DI usage in Ruby, but Piotr Solnica demonstrated reasonable examples of good DI usage. Also, Sandi Metz provides good arguments and examples in her POODR book.
With the dry-container
and dry-auto_inject
libs, DI/IoC in Ruby could look something like this:
my_container = Dry::Container.new
my_container.register(:data_store, -> { DataStore.new })
my_container.register(:user_repository, -> { container[:data_store][:users] })
my_container.register(:persist_user, -> { PersistUser.new })
# set up your auto-injection function
AutoInject = Dry::AutoInject(my_container)
# then simply include it in your class, specifying the dependencies that
# should be injected automatically from the configured container
class PersistUser
include AutoInject[:user_repository]
def call(user)
user_repository << user
end
end
- David Heinemeier Hansson: Dependency injection is not a virtue
- Piotr Solnica: The World Needs Another Post About Dependency Injection in Ruby
- Martin Fowler: Inversion of Control Containers and the Dependency Injection pattern
- dry-container
- dry-auto_inject
- Effective Ruby dependency injection at scale