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



В нашому блозі на Хабре ми не тільки розповідаємо про розвиток свого продукту — білінгу для операторів зв'язку «Гідра», але і публікуємо матеріали про роботу з інфраструктурою і використанні технологій з досвіду інших компаній. Програміст і один з керівників австралійської студії розробки 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
— це все, що необхідно для роботи з невеликими окремими компонентами, легко поєднуються разом з допомогою впровадження залежностей. Застосування цих інструментів спрощує створення програм і, що навіть більш важливо, полегшує їх підтримку, розширення та перепроектування.

Інші технічні статті від «Латеры»:

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.