Pull to refresh

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

Reading time 5 min
Views 12K
Original author: Tim Riley


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

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

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

В этом месте Райли приводит код команды 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. Здесь объясняется, как можно использовать 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 (механизм, обеспечивающий разрешение зависимостей по требованию):

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 — это все, что необходимо для работы с небольшими отдельными компонентами, легко соединяющимися вместе с помощью внедрения зависимостей. Применение этих инструментов упрощает создание приложений и, что даже более важно, облегчает их поддержку, расширение и перепроектирование.

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


Tags:
Hubs:
+16
Comments 25
Comments Comments 25

Articles

Information

Website
www.latera.ru
Registered
Founded
Employees
Unknown
Location
Россия