Ruby - Dependency Injection
- 23 October 2016
- Patterns Ruby Design
- Ruby - Dependency Injection
Hi there. Today we will discover Dependency Injection (DI). To understand all benefits of dependency injection we should understand why dependencies are bad.
As Sandi Metz says (you should read her book Practical Object-Oriented Design) - each dependency in your class it's a glue which holds together parts of app. Sometimes you can't do anything without it. But if you use too much glue you will get a rigid monolithic app which hard to extend and maintain.
Many people say that main idea of Object-Oriented design is not an Inheritance, Polymorphism and Encapsulation. They say that main idea of OO design is a management of dependencies.
Let's see how dependencies look in code:
class User
attr_reader :email
def initialize(email)
@email = email
end
def send_notification
UserMailer.send_email(email)
end
end
From this example we see that class User
depends on UserMailer
class. Class User
rely on UserMailer
existence and expect it to support send_email
message.
For small applications such approach will not be too bad. But apps grow and we get more and more requirements. So we should be able to change shape of app easily.
Dependencies in code - it's something we should care of and something we should avoid.
Today, by example, I want to show how to go from explicit dependency in code to Dependency Injection. Also, we will consider benefits of DI.
So our task to write report generator. At the beginning we were told that csv format will be more than enough.
Possible implementation will use data from DataSource
:
class DataSource
def data
[
{ user: 'Adrian Lewis', email: 'adrian@lewis.com' },
{ user: 'Phil Taylor', email: 'phil@taylor.com' }
]
end
end
Just to simplify things I created structure of data in array. In real app it might be data from database or third-party API. It's not that important since all we need - some data to generate report from.
We will write small class which formats data in csv:
class CsvDataFormatter
def format(data)
return "" if data.empty?
csv = data.first.keys.join(",") + "\n"
data.each { |hsh| csv += hsh.values.join(",") + "\n" }
csv
end
end
So we have two classes with data and with possibility to format it.
If we pass data from DataSource
to CsvDataFormatter
, we will get valid csv:
user,email
Adrian Lewis,adrian@lewis.com
Phil Taylor,phil@taylor.com
Now we can move forward and write report generator, which will get data from DataSource
and pass to formatting to CsvDataFormatter
:
class ReportGenerator
attr_reader :data_source, :formatter
def initialize
@data_source = DataSource.new
@formatter = CsvDataFormatter.new
end
def generate
formatter.format(data_source.data)
end
end
report_generator = ReportGenerator.new
report_generator.generate # => returns csv data
Check method initialize
in class ReportGenerator
. It's tied to two classes DataSource
and CsvDataFormatter
. It creates explicit dependency on those two classes.
Let's think how we can improve that code. How to avoid explicit usage of classes we want to work with?
We can use Dependency Injection! Code of report generator could look like this:
class ReportGenerator
attr_reader :data_source, :formatter
def initialize(formatter = CsvDataFormatter.new, data_source = DataSource.new)
@formatter = formatter
@data_source = data_source
end
def generate
formatter.format(data_source.data)
end
end
See that small change in initialize
? It looks like a minor change in code, but it makes that code much more flexible.
In this case names of classes are not hard coded inside method. Instead, we inject dependency into initialize
method.
So if we call ReportGenerator.new
without any arguments, it will generate csv by default. But if we want to generate report in JSON format, we could easily create formatter and pass it into ReportGenerator.new
.
require 'json'
class JsonDataFormatter
def format(data)
data.to_json
end
end
ReportGenerator.new(JsonDataFormatter.new).generate # [{"user":"Adrian Lewis","email":"adrian@lewis.com"},{"user":"Phil Taylor","email":"phil@taylor.com"}]
Looks good, huh? We easily added ability to export data to JSON! Using Dependency Injection we can pass any object which responds to format
and get formatted data.
The same idea we can use with DataSource
. We can pass any object which responds to data
and get a report based on those data.
One small change allowed us to make report generator much flexible. We got rid from explicit dependency. Instead we made that dependency flexible. It's based on interfaces, rather than names of classes.
Speaking about interfaces, I mean that any class which has method format
can be used as a formatter for a report. And class with method data
can be used as a DataSource
.
By implementing report generator that way we prepare our system to future changes. If we need new format of report - we know what to do. We just write new class which responds to format
method. If customer wants to generate reports from another data source - we will create new class which will work as DataSource
and has method data
. That's a part of Open/Closed principle from SOLID rules of object-oriented design. We can add support of new formats for generator without editing its code.
System is flexible enough and can be extended by Dependency Injection.
I hope this idea will help you to get rid from dependencies and write more flexible code.
Happy coding!