Ruby - Dependency Injection

Subscribe to receive new articles. No spam. Quality content.

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!

Subscribe to receive new articles. No spam. Quality content.

Comments