For more than 2 years we were using dry-rb libraries (and a bit of Trailblazer) in production.
I have about 10 years of Ruby on Rails experience and within all this time nothing changed in "official" ROR approach of managing and organising codebase. We still have fat models, somebody even have business logic in them.
In my speech I tried to show when this approach does not work, how we came to this, how we managed to implement dry-rb in existing projects and how it finally helps us
21. • Единый интерфейс сервисов
• Частично избавились от колбеков
• Тестирование функциональных объектов – проще
• Композиция сервисов
Что стало лучше?
22. Проблемы
• Валидации остались в моделях:
document.save(validate: false)
# OR
document.from_crm = true
document.save
• Композиция сервисов получалась не всегда,
получалось дублирование
• Тестирование не стало проще из-за
невозможности управлять зависимостями
• Не получалось сделать композицию объектов
(Domain)
26. class CreateDocument
def call(document, params)
form = CreateDocumentForm.new(document)
if form.validate(params)
document = form.sync
rename_document(document)
end
document.save!
end
end
27. class CreateDocument
def call(document, params)
form = CreateDocumentForm.new(document)
if form.validate(params)
rename_document(document)
end
document.save!
end
end
30. class CreateDocument
def call(document, params)
form = form_class.new(document)
if form.validate(params)
document = form.sync
rename_document(document)
document.save!
end
document
end
end
31. # Initialize
command = CreateDocument.new(
form_class: Document::CreateDocumentForm,
rename_document: Document::RenameDocument.new
)
# Use several times
command.call(passport, params[:passport])
command.call(visa, params[:visa])
32. # создание документа от заёмщика
command = CreateDocument.new(
form_class: Document::CreateDocumentForm,
rename_document: Document::RenameDocument.new
)
# создания документа из CRM
command = CreateDocument.new(
form_class: Crm::Document::CreateDocumentForm.new,
rename_document: Document::RenameDocument.new
)
39. • IoC Container
• Позволяет реализовать Dependency inversion
principle
• Thread safe
dry-container
40. dry-container
# app/containers/global_container.rb
module GlobalContainer
extend Dry::Container::Mixin
namespace('services') do
register('create_document') do
Document::CreateDocument.new(
self[‘forms.create_document_form_class']
)
end
end
namespace('forms') do
register('create_document_form_class') do
Document::DocumentForm
end
end
end
41. dry-container
# создание документа от заёмщика
command = GlobalContainer[‘services.create_document’]
command.form # Document::CreateDocumentForm
command.class # Document::CreateDocument
# создания документа из CRM
command = GlobalContainer[‘crm.services.create_document’]
command.form # Crm::Document::CreateDocumentForm
command.class # Document::CreateDocument
42. dry-container
# создание документа от заёмщика
command = GlobalContainer[‘services.create_document’]
command.form # Document::CreateDocumentForm
command.class # Document::CreateDocument
# создания документа из CRM
command = GlobalContainer[‘crm.services.create_document’]
command.form # Crm::Document::CreateDocumentForm
command.class # Document::CreateDocument
43. dry-monads
• Монада – контейнер, в котором есть результат с
объектом
• Either монада (классическая) – это Result монада в dry-rb
• [паттерн] позволяет делать chaining
• Синтаксический сахар
44. Maybe
# corporate_action/charts_mapper.rb
class CorporateAction::ChartsMapper
include Dry::Monads::Maybe::Mixin
def fetch_mapping(corporate_action, fund, entry)
Maybe(
# some calculation that may return nil
)
end
end
# fund/calculate_share_price.rb
mapping = mapper.fetch_mapping(action, fund, entry)
mapping.bind do |m|
calculate_price_by_valuation_amount(fund, m))
end.or(issue_price_of_account(account)))
45. Result
class CreateDocument
def call(document, params)
form = form_class.new(document)
if form.validate(params)
document = form.sync
rename_document(document)
document.save!
end
document
end
end
46. Result
class CreateDocument
def call(document, params)
form = form_class.new(document)
if form.validate(params)
document = form.sync
rename_document(document)
document.save!
else
return false
end
document
end
end
52. dry-matcher (custom)
class CreateDocument
include Dry::Matcher.for(:call, with: TripleMatcher)
…
def call(document, params)
form = form_class.new(document)
if form.validate(params)
…
document.save!
[:success, document]
else
[:failure, :validation, form]
end
rescue => e
[:failure, e]
end
end
53. dry-matcher (custom)
command = GlobalContainer[‘services.create_document’]
command.call(document, params[:document]) do |m|
m.success do |document|
…
end
m.failure :validation do |form|
render_failure_json(
422, document: form.react_errors_hash
)
end
m.failure do |e|
render_failure_json(500, document: e.message)
end
end
54. • Валидации основаны на логике предикатов
• Работают с любыми типами входных данных
• Можно валидировать формы, HTTP параметры, JSON
документы, конфигурацию из YAML
• Расширяемость
• Широко используются в других библиотеках
dry-validation & dry-types
59. RSpec / Stubs
require ‘dry/container/stub'
describe "#call" do
before(:all) do
GlobalContainer.enable_stubs!
end
…
GlobalContainer.stub(
‘support.services.exception_notifier’,
ExceptionRaiser
)
end
61. Плюсы
• тестирование функциональных объектов
• простое расширение логики
• композиция – основа моделирования процессов
• масштабирование и слабая связанность
компонентов приложения
62. Минусы
• имитация функций объектами
• концептуально множатся сущности (которые не
являются сущностями по факту)
• IoC с доступом по строкам
• можно написать монадический метод и вернуть не
монадическое значение