Introduction to dependency injection in Ruby

During one of my Java projects I stumbled upon dependency injection which is a major thing in Java development. At first I did not really see any sense why people would need dependency injection. I took some time and read about dependency injection and how to implement it.

So today I want to try to explain dependency injection and its advantages. Also I want to show how to implement dependency injection in Ruby using the dry-rb libraries.

What is dependency injection

Let?s say you have a standard web application which manages notes. We want to have a class which presents us notes grouped differently like pending notes or notes which are already done.

class NotePresenter def initialize @note_storage = NoteTextStorage.new end def pending_notes notes = @note_storage.get_all notes.select { |n| n.pending? } endend

Which problems do arise from this implementation. What if we want to change the underlining NoteTextStorage implementation. For example NoteTextStorage is using a text-file based storage but we want to switch to a database solution with a class called NoteDatabaseStorage. The problem here is that there is a strong link between the NotePresenter class and its underlying persistency. We could change our NotePresenter but wouldn?t it be much nicer if we could just pass our Storage to the presenter and it would work as before.

What dependency injection does is glueing components (in this case the presenter and the storage) together but at runtime. It gives us loose coupling between these components. Many frameworks use this concept to enable the programmer to develop for example his storage solution as he wants while maintaining flexibility.

A second advantage is that it makes the code more testable. When testing the NotePresenter we might not want to use the real NoteTextStorage implementation. We could create a mock NoteTextStorage implementation like this:

class TestStorage def pending_notes note1 = Note.new title: ‘test’, text: ‘test’, pending: true note2 = Note.new title: ‘test2’, text: ‘test2’, pending: false endend

and use it as our storage only for testing purposes.

Implementing dependency injection in Ruby

So how can we implement dependency injection in Ruby. One straightforward way would be to just pass it in the constructor:

class NotePresenter def initialize(storage) @note_storage = storage end def pending_notes notes = @note_storage.get_all notes.select { |n| n.pending? } endend

Naturally all storage objects passed to the presenter must implement the same interface for this to work. This way is simple and straight-forward and will be sufficient in many cases. But what if our storage solution is not only used by our NotePresenter but by other classes aswell. We would have to search and change every piece of code where our storage solution was passed to another object. Secondly this solution won?t really work for a framework where the user should not edit the code of the framework. A configuration-like approach would be a better fit here. This is how most dependency injection libraries work. They have a centralized configuration of all components and how they should be glued together. A developer can change the underlining implementations just by changing the configuration.

dry-rb offers two gems for this approach called dry-container and dry-auto_inject.

Let?s implement our example using these two gems:

dependency_container = Dry::Container.new

Here we instantiate an object which will serve as a container of our configuration (our glueing of components).

dependency_container.register(‘note_storage’, -> { NoteTextStorage.new })

Here we registered a new configuration with the key note_storage and bound it to our NoteTextStorage solution.

AutoInject = Dry::AutoInject(dependency_container)

After finishing our configuration we set it up using the upper line of code. Now we are ready to inject our dependencies in our classes.

class NotePresenter include AutoInject[‘note_storage’] def pending_notes notes = note_storage.get_all notes.select { |n| n.pending? } endend

Here we inject our note_storage configuration in our NotePresenter. Through this the instance passed in the configuration is accessible using the note_storage method. Now let?s say we want to change our underlining storage implementation. All we have to do is create a new implementation and pass the class in our container as the note_storage dependency.

For a test we would just take a different configuration like this:

dependency_container = Dry::Container.newdependency_container.register(‘note_storage’, -> { TestStorage.new })AutoInject = Dry::AutoInject(dependency_container)

To summarize: dependency injection gives our code a higher loose coupling and by this more flexibility for changes as well as more testable code.

12

No Responses

Write a response