Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Not the Rails Way

Not the Rails Way

Ruby on Rails architecture practices

Avatar for Igor Aleksandrov

Igor Aleksandrov

August 23, 2018
Tweet

More Decks by Igor Aleksandrov

Other Decks in Technology

Transcript

  1. • Пишу на Rails c 2008 года • Начал с

    Rails 2.2 • Пришел в Ruby из SmallTalk • Последние 5 лет слышу “Ruby/Rails мертв”
  2. Ruby on Rails разработчики валят с проекта George Catlin, 1832

    after_save… after_commit on:… accepts_nested_attributes_for… validates_presence_of…
  3. • Единый интерфейс сервисов • Частично избавились от колбеков •

    Тестирование функциональных объектов – проще • Композиция сервисов Что стало лучше?
  4. Проблемы • Валидации остались в моделях: document.save(validate: false) # OR

    document.from_crm = true document.save • Композиция сервисов получалась не всегда, получалось дублирование • Тестирование не стало проще из-за невозможности управлять зависимостями • Не получалось сделать композицию объектов (Domain)
  5. 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
  6. class CreateDocument attr_reader :form_class attr_reader :rename_document def initialize(form_class:, rename_document:) @form_class

    = form_class @rename_document = rename_document end def call(document, params) … end end
  7. 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
  8. # 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])
  9. # создание документа от заёмщика 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 )
  10. class CreateDocument attr_reader :form_class attr_reader :rename_document attr_reader :upload_document_to_perfect_audit attr_reader :update_expiration_dates

    attr_reader :convert_document_to_pdf attr_reader :create_affiliations attr_reader :update_cpl_records attr_reader :update_application_status # hundreds of other stuff here … end
  11. 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
  12. 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
  13. 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
  14. dry-monads • Монада – контейнер, в котором есть результат с

    объектом • Either монада (классическая) – это Result монада в dry-rb • [паттерн] позволяет делать chaining • Синтаксический сахар
  15. 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)))
  16. 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
  17. 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
  18. Result class CreateDocument include Dry::Monads::Result::Mixin def call(document, params) form =

    form_class.new(document) if form.validate(params) … document.save! else return Failure(form) end Success(document) rescue => e Failure(e) end end
  19. Try class CreateDocument include Dry::Monads::Result::Mixin include Dry::Monads::Try::Mixin … def call(document,

    params) form = form_class.new(document) if form.validate(params) Try do document.save! … document end.to_either else Failure(form) end end end
  20. dry-matcher class CreateDocument include Dry::Matcher.for(:call, with: Dry::Matcher::ResultMatcher) … def call(document,

    params) form = form_class.new(document) if form.validate(params) Try do document.save! … document end.to_either else Failure(form) end end end
  21. dry-matcher command = GlobalContainer[‘services.create_document’] command.call(document, params[:document]) do |m| m.success do

    |document| render json: { document: DocumentRepresenter.new.call(document) } end m.failure do |form| render_failure_json( 422, document: form.react_errors_hash ) end end
  22. 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
  23. 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
  24. • Валидации основаны на логике предикатов • Работают с любыми

    типами входных данных • Можно валидировать формы, HTTP параметры, JSON документы, конфигурацию из YAML • Расширяемость • Широко используются в других библиотеках dry-validation & dry-types
  25. dry-initializer • param и option • default values • type

    constraints • private/protected readers
  26. dry-initializer class CreateDocument attr_reader :form_class attr_reader :rename_document def initialize(form_class:, rename_document:)

    @form_class = form_class @rename_document = rename_document end def call(document, params) … end end
  27. dry-initializer class CreateDocument extend Dry::Initializer param :form_class param :rename_document def

    initialize(form_class:, rename_document:) @form_class = form_class @rename_document = rename_document end def call(document, params) … end end
  28. RSpec / Stubs require ‘dry/container/stub' describe "#call" do before(:all) do

    GlobalContainer.enable_stubs! end … GlobalContainer.stub( ‘support.services.exception_notifier’, ExceptionRaiser ) end
  29. Плюсы • тестирование функциональных объектов • простое расширение логики •

    композиция – основа моделирования процессов • масштабирование и слабая связанность компонентов приложения
  30. Минусы • имитация функций объектами • концептуально множатся сущности (которые

    не являются сущностями по факту) • IoC с доступом по строкам • можно написать монадический метод и вернуть не монадическое значение