- PVSM.RU - https://www.pvsm.ru -

Эффективное внедрение зависимостей при масштабировании Ruby-приложений

Эффективное внедрение зависимостей при масштабировании Ruby-приложений - 1 [1]

В нашем блоге на Хабре мы не только рассказываем о развитии своего продукта — биллинга для операторов связи «Гидра» [2], но и публикуем материалы о работе с инфраструктурой и использовании технологий из опыта других компаний. Программист и один из руководителей австралийской студии разработки Icelab Тим Райли написал в корпоративном блоге статью [3] о внедрении зависимостей Ruby — мы представляем вашему вниманию адаптированную версию этого материала.

В предыдущей части [4] Райли описывает подход, в котором внедрение зависимостей используется для создания небольших переиспользуемых функциональных объектов, реализующих шаблон «Команда [5]». Реализация оказалась относительно простой, без громоздких кусков кода — всего три работающих вместе объекта. С помощью этого примера объясняется использование не нескольких сотен, а одной или двух зависимостей.

Для того, чтобы внедрение зависимостей работало даже при больших масштабах инфраструктуры, необходимо наличие единственной вещи — контейнера с инверсией управления [6].

В этом месте Райли приводит код команды CreateArticle, в которой используется внедрение зависимостей:

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

В этой команде используется внедрение зависимости в конструктор для работы с объектами validate_article и persist_article. Здесь [7] объясняется, как можно использовать dry-container (простой потокобезопасный контейнер, предназначенный для использования в качестве половины реализации контейнера с инверсией управления) для того, чтобы зависимости были доступны при необходимости:

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")

Тим объясняет инверсию управления с помощью аналогии — представьте один большой ассоциативный массив, который управляет доступом к объектам в приложении. В представленном ранее фрагменте кода были зарегистрированы 3 объекта с помощью блоков для их последующего создания при обращении. Отложенное вычисление блоков означает так же, что сохраняется возможность их использования для доступа к другим объектам в контейнере. Таким образом передаются зависимости при создании create_article.

Можно вызвать MyApp::Container["create_article"], и объект будет полностью сконфигурирован и готов к использованию. Имея контейнер, можно зарегистрировать объекты один раз и многократно использовать их в дальнейшем.

dry-container поддерживает объявление объектов без использования пространства имен для того, чтобы облегчить работу с большим количеством объектов. В реальных приложениях чаще всего используется пространство имен вида «articles.validate_article» и «persistence.commands.persist_article» вместо простых идентификаторов, которые можно встретить в описываемом примере.

Все хорошо, однако, в больших приложениях хотелось бы избежать большого количества шаблонного кода. Решить эту задачу можно в два этапа. Первый из них заключается в использовании системы автоматического внедрения зависимостей в объекты. Вот, как это выглядит при использовании dry-auto_inject [8] (механизм, обеспечивающий разрешение зависимостей по требованию):

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

Использование механизма автоматического внедрения позволяет уменьшить объем шаблонного кода при объявлении объектов с контейнером. Исчезает необходимость в разработке списка зависимостей для их передачи методу CreateArticle.new при его объявлении. Вместо этого можно определить зависимости непосредственно в классе. Модуль, подключаемый с помощью AutoInject[*dependencies] определяет методы .new, #initialize и attr_readers, которые «вытягивают» из контейнера зависимости, и позволяют их использовать.

Объявление зависимостей в том месте, где они будут использоваться — это очень мощная рокировка, позволяющая сделать понятными совместно используемые объекты без необходимости определения конструктора. Кроме того появляется возможность простого обновления списка зависимостей — это полезно, поскольку задачи, выполняемые объектом, со временем меняются.

Описанный метод кажется довольно изящным и эффективным, однако стоит подробнее остановиться на способе объявления контейнеров, который использовался в начале последнего примера кода. Такое объявление можно использовать с dry-component, системой, имеющей все необходимые функции управления зависимостями и основанной на dry-container и dry-auto_inject. Эта система сама управляет тем, что необходимо для использования инверсии управления между всеми частями приложения.

В своем материале Райли отдельно фокусируется на одном аспекте этой системы — автоматическом объявлении зависимостей.

Предположим, что три наших объекта определены в файлах lib/validate_article.rb, lib/persist_article.rb и lib/create_article.rb. Все их можно включить в контейнер автоматически, используя специальную настройку в файле верхнего уровня 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")

Теперь в программе больше не содержится однотипных строк кода, при этом приложение по-прежнему работает. Автоматическая регистрация использует простое преобразование файла и имени класса. Директории преобразуются в пространства имен, таким образом класс Articles::ValidateArticle в файле lib/articles/validate_article.rb будет доступен для разработчика в контейнере articles.validate_article без необходимости каких-либо дополнительных действий. Таким образом обеспечивается удобное преобразование, похожее на преобразование в Ruby on Rails, без возникновения каких-либо проблем с автоматической загрузкой классов.

dry-container, dry-auto_inject, и dry-component — это все, что необходимо для работы с небольшими отдельными компонентами, легко соединяющимися вместе с помощью внедрения зависимостей. Применение этих инструментов упрощает создание приложений и, что даже более важно, облегчает их поддержку, расширение и перепроектирование.

Другие технические статьи от «Латеры [9]»:

Автор: Латера Софтвер

Источник [16]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/ruby/121252

Ссылки в тексте:

[1] Image: https://habrahabr.ru/company/latera/blog/301338/

[2] биллинга для операторов связи «Гидра»: http://www.hydra-billing.ru/

[3] статью: http://icelab.com.au/articles/effective-ruby-dependency-injection-at-scale/

[4] предыдущей части: http://icelab.com.au/articles/functional-command-objects-in-ruby/

[5] Команда: https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BC%D0%B0%D0%BD%D0%B4%D0%B0_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F)

[6] инверсией управления: https://ru.wikipedia.org/wiki/%D0%98%D0%BD%D0%B2%D0%B5%D1%80%D1%81%D0%B8%D1%8F_%D1%83%D0%BF%D1%80%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F

[7] Здесь: http://dry-rb.org/gems/dry-container

[8] dry-auto_inject: http://dry-rb.org/gems/dry-auto_inject

[9] Латеры: http://www.latera.ru/

[10] Автоматизируем учет адресов и привязок в IPoE-сетях: http://blog.hydra-billing.ru/stories/574

[11] Судный день: К чему приводят скрытые ошибки асинхронной обработки данных при росте нагрузки: https://habrahabr.ru/company/latera/blog/283548/

[12] Работа с MySQL: как масштабировать хранилище данных в 20 раз за три недели: https://habrahabr.ru/company/latera/blog/282798/

[13] DoS своими силами: К чему приводит бесконтрольный рост таблиц в базе данных: https://habrahabr.ru/company/latera/blog/277331/

[14] Архитектура open source-приложений: Как работает nginx: https://habrahabr.ru/company/latera/blog/273283/

[15] Как повысить отказоустойчивость биллинга: Опыт «Гидры»: https://habrahabr.ru/company/latera/blog/267083/

[16] Источник: https://habrahabr.ru/post/301338/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best