📜 ⬆️ ⬇️

Effective dependency injection when scaling Ruby applications



In our blog on Habré, we not only talk about the development of our product - billing for Hydra telecom operators , but also publish materials about working with infrastructure and using technologies from the experience of other companies. Tim Riley, a programmer and one of the leaders of the Australian development studio Icelab, wrote an article in the corporate blog about introducing Ruby dependencies - 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 " pattern. 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 hundred, but one or two dependencies.
')
In order for dependency injection to work even with large-scale infrastructure, it is necessary to have a single thing - a container with inversion of control .

At this point, Riley lists the CreateArticle command code, 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 for working with validate_article and persist_article . It explains how dry-container can be used (a simple thread-safe container designed to be used as half the container implementation with inversion 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 by analogy - imagine one large associative array that controls access to objects in an application. In the previously presented code fragment, 3 objects were registered using blocks for their subsequent creation upon access. Delayed calculation of blocks also means that it is still possible to use them to access other objects in the container. Thus dependencies are transferred when creating 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 later.

dry-container supports declaring objects without using a namespace in order to facilitate working with a large number of objects. In real-world applications, the most commonly used namespace is “articles.validate_article” and “persistence.commands.persist_article” instead of simple identifiers, which can be found in the example being described.

All is well, however, in large applications I would like to avoid a large number of sample code. This problem can be solved in two stages. The first one is to use the system of automatic dependency injection 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 template code when declaring objects with a container. There is no need to develop a list of dependencies for their transfer to the CreateArticle.new method when it is declared. Instead, you can define dependencies directly in the class. A plug-in using AutoInject[*dependencies] defines the .new , #initialize and attr_readers that “pull” dependencies out of the container, and allow them to be used.

Declaring dependencies in the place where they will be used is a very powerful castling that allows you to make shared objects understandable without the need to define a constructor. In addition, you can easily 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 is worthwhile to dwell in greater detail on the container declaration method used at the beginning of the last code example. Such an announcement can be used with the dry-component , a 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 his material, Riley focuses separately on one aspect of this system - the automatic declaration of dependencies.

Suppose that our three objects are defined in the 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 the special settings 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 lines of code of the same type, while the application is still running. Automatic registration uses simple file and class name conversion. Directories are converted to namespaces, so the Articles::ValidateArticle class in the lib/articles/validate_article.rb file will be available to the developer in the articles.validate_article container without the need for any additional actions. This provides a convenient conversion that is similar to the conversion in Ruby on Rails, without any problems with the automatic loading of classes.

dry-container , dry-auto_inject , and dry-component is all that is needed to work with small separate components that are easily connected together using dependency injection. Application of these tools simplifies the creation of applications and, even more importantly, facilitates their support, expansion and redesign.

Other technical articles from Latera :


Source: https://habr.com/ru/post/301338/


All Articles