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

Igor Alexandrov

August 23, 2018
Tweet

More Decks by Igor Alexandrov

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 с доступом по строкам • можно написать монадический метод и вернуть не монадическое значение