
В нашем блоге на Хабре мы не только рассказываем о развитии своего продукта — биллинга для операторов связи «Гидра», но и публикуем материалы о работе с инфраструктурой и использовании технологий из опыта других компаний. Программист и один из руководителей австралийской студии разработки 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
- Как повысить отказоустойчивость биллинга: Опыт «Гидры»

