В нашем блоге на Хабре мы не только рассказываем о развитии своего продукта — биллинга для операторов связи «Гидра», но и публикуем материалы о работе с инфраструктурой и использовании технологий из опыта других компаний. Программист и один из руководителей австралийской студии разработки 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
— это все, что необходимо для работы с небольшими отдельными компонентами, легко соединяющимися вместе с помощью внедрения зависимостей. Применение этих инструментов упрощает создание приложений и, что даже более важно, облегчает их поддержку, расширение и перепроектирование.
Другие технические статьи от «Латеры»:
- Автоматизируем учет адресов и привязок в IPoE-сетях
- Судный день: К чему приводят скрытые ошибки асинхронной обработки данных при росте нагрузки
- Работа с MySQL: как масштабировать хранилище данных в 20 раз за три недели
- DoS своими силами: К чему приводит бесконтрольный рост таблиц в базе данных
- Архитектура open source-приложений: Как работает nginx
- Как повысить отказоустойчивость биллинга: Опыт «Гидры»
Автор: Латера Софтвер