Effective Dependency Injection When Scaling Ruby Applications

Original author: Tim Riley
  • Transfer


In our blog on Habré, we not only talk about the development of our product - billing for Hydra telecom operators , but also publish materials on working with infrastructure and using technologies from the experience of other companies. Tim Riley, a programmer and one of the leaders of Icelab, an Australian development studio, wrote an article on implementing Ruby dependencies on a corporate blog - we present to you an adapted version of this material.

In the previous part, Riley describes an approach in which dependency injection is used to create small reusable functional objects that implement the “ Team ” template.". The implementation turned out to be relatively simple, without bulky pieces of code - only three objects working together. This example explains the use of not one or two hundred, but one or two dependencies.

In order for dependency injection to work even with a large infrastructure, the only thing you need is a container with inverse control .

At this point, Riley gives the code for the CreateArticle command, which uses dependency injection:

class CreateArticle
  attr_reader :validate_article, :persist_article
  def initialize(validate_article, persist_article)
    @validate_article = validate_article
    @persist_article = persist_article
  end
  def call(params)
    result = validate_article.call(params)
    if result.success?
      persist_article.call(params)
    end
  end
end

This command uses dependency injection in the constructor to work with validate_articleand persist_article. This explains how you can use dry-container (a simple, thread-safe container designed to be used as a half implementation of a container with inverse control) so that dependencies are available when needed:

require "dry-container"
# Создаем контейнер
class MyContainer
  extend Dry::Container::Mixin
end
# Регистрируем наши объекты
MyContainer.register "validate_article" do
  ValidateArticle.new
end
MyContainer.register "persist_article" do
  PersistArticle.new
end
MyContainer.register "create_article" do
  CreateArticle.new(
    MyContainer["validate_article"],
    MyContainer["persist_article"],
  )
end
# Теперь объект `CreateArticle` доступен к использованию 
MyContainer["create_article"].("title" => "Hello world")

Tim explains the inversion of control using an analogy - imagine one large associative array that controls access to objects in an application. In the code snippet presented earlier, 3 objects were registered using blocks for their subsequent creation upon handling. Delayed calculation of blocks also means that it remains possible to use them to access other objects in the container. In this way, dependencies are transmitted at creation create_article.

You can call MyApp::Container["create_article"], and the object will be fully configured and ready to use. Having a container, you can register objects once and reuse them in the future.

dry-containersupports the declaration of objects without using a namespace in order to facilitate the work with a large number of objects. In real applications, the namespace “articles.validate_article” and “persistence.commands.persist_article” is most often used instead of the simple identifiers that can be found in the described example.

All is well, however, in large applications, I would like to avoid a lot of boilerplate code. This problem can be solved in two stages. The first one is to use a system for automatically injecting dependencies into objects. Here's what it looks like when using dry-auto_inject (a mechanism for resolving dependencies on demand):

require "dry-container"
require "dry-auto_inject"
# Создаем контейнер
class MyContainer
  extend Dry::Container::Mixin
end
# В этот раз регистрируем объекты без передачи зависимостей
MyContainer.register "validate_article", -> { ValidateArticle.new }
MyContainer.register "persist_article", -> { PersistArticle.new }
MyContainer.register "create_article", -> { CreateArticle.new }
# Создаем модуль AutoInject для использования контейнера
AutoInject = Dry::AutoInject(MyContainer)
# Внедряем зависимости в CreateArticle
class CreateArticle
  include AutoInject["validate_article", "persist_article"]
  # AutoInject делает доступными объекты `validate_article` and `persist_article` 
  def call(params)
    result = validate_article.call(params)
    if result.success?
      persist_article.call(params)
    end
  end
end

Using the automatic implementation mechanism allows you to reduce the amount of boilerplate code when declaring objects with a container. There is no need to develop a list of dependencies for passing them to a method CreateArticle.newwhen it is declared. Instead, you can define dependencies directly in the class. The module, which connects via AutoInject[*dependencies]defines methods .new, #initializeand attr_readersthat is "pulled" out of the container, depending, and allow you to use them.

Declaring dependencies where they will be used is a very powerful castling that makes shared objects understandable without the need for a constructor definition. In addition, it becomes possible to simply update the list of dependencies - this is useful because the tasks performed by the object change over time.

The described method seems rather elegant and efficient, but it’s worthwhile to dwell in more detail on the method of declaring containers, which was used at the beginning of the last code example. This declaration can be used with a dry-componentsystem that has all the necessary dependency management functions and is based on dry-containerand dry-auto_inject. This system itself controls what is needed to use control inversion between all parts of the application.

In her article, Riley focuses separately on one aspect of this system - automatic dependency declaration.

Suppose our three objects are defined in files lib/validate_article.rb, lib/persist_article.rband lib/create_article.rb. All of them can be included in the container automatically using a special setting in the top-level file my_app.rb:

require "dry-component"
require "dry/component/container"
class MyApp < Dry::Component::Container
  configure do |config|
    config.root = Pathname(__FILE__).realpath.dirname
    config.auto_register = "lib"
  end
  # Добавляем "lib/" в $LOAD_PATH
  load_paths! "lib"
end
# Запускаем автоматическую регистрацию
MyApp.finalize!
# И теперь все готово к использованию
MyApp["validate_article"].("title" => "Hello world")

Now the program no longer contains the same lines of code, while the application still works. Auto registration uses a simple file and class name conversion. Directories are converted to namespaces, so the class Articles::ValidateArticlein the file lib/articles/validate_article.rbwill be available to the developer in the container articles.validate_articlewithout the need for any additional actions. This provides a convenient conversion, similar to the conversion to Ruby on Rails, without any problems with the automatic loading of classes.

dry-container,, dry-auto_injectanddry-component- This is all that is needed to work with small individual components that are easily connected together through dependency injection. The use of these tools simplifies the creation of applications and, even more importantly, facilitates their support, expansion and redesign.

Other technical articles from Latera :



Also popular now: