Effective Dependency Injection When Scaling Ruby Applications
- 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_article
and 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-container
supports 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.new
when it is declared. Instead, you can define dependencies directly in the class. The module, which connects via AutoInject[*dependencies]
defines methods .new
, #initialize
and attr_readers
that 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-component
system that has all the necessary dependency management functions and is based on dry-container
and 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.rb
and 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::ValidateArticle
in the file lib/articles/validate_article.rb
will be available to the developer in the container articles.validate_article
without 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_inject
anddry-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 :
- We automate the accounting of addresses and bindings in IPoE networks
- Judgment Day: What Hidden Errors of Asynchronous Data Processing Lead to Increasing Load
- Work with MySQL: how to scale data storage 20 times in three weeks
- DoS in the home: Where does the uncontrolled growth of tables in a database lead?
- Open Source Application Architecture: How nginx Works
- How to increase resiliency of billing: Hydra experience