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

Architecture of hanami applications

Architecture of hanami applications

The general part of any web application is business logic. Unforchanotly, it's really hard to find a framework with specific rules and explanations how to work with it. In hanami, we care about long-term maintenance that's why it's really important to us how to work with business logic.

In my talk, I'll share my ideas how to store and work with business logic in hanami apps. We will talk about hanami, dry and some architecture ideas, like event sourcing. This talk will be interesting for any developers. If you work with other frameworks you can take these ideas and my it to your project.

Anton Davydov

May 31, 2018
Tweet

More Decks by Anton Davydov

Other Decks in Programming

Transcript

  1. ❤


    Hello Japan!



    View full-size slide

  2. What have I learned here?

    View full-size slide

  3. Is ramen still awesome?

    View full-size slide

  4. Is Java more popular
    than ruby?

    View full-size slide

  5. Is Ruby still dying?

    View full-size slide

  6. Is RubyKaigi still
    awesome?

    View full-size slide

  7. ❤


    Hello RubyKaigi!



    View full-size slide

  8. Anton Davydov
    github.com/davydovanton

    twitter.com/anton_davydov
    davydovanton.com

    View full-size slide

  9. Architecture

    View full-size slide

  10. Trying to unify architecture
    ideas for hanami projects

    View full-size slide

  11. Getting feedback

    View full-size slide

  12. Management
    Performance
    Maintenance

    View full-size slide

  13. Management
    Performance
    Maintenance

    View full-size slide

  14. Clean code
    Good architecture
    Maintainable product
    Pretty code

    View full-size slide

  15. Clean code
    Good architecture
    Maintainable product
    Pretty code
    biased
    view

    View full-size slide

  16. What do we really
    want?

    View full-size slide

  17. To add new features

    View full-size slide

  18. Isolation
    Sequential logic
    No global state

    View full-size slide

  19. Delete (refactoring)
    old code

    View full-size slide

  20. Isolation
    Test coverage

    View full-size slide

  21. Isolation
    Sequential logic
    No global state

    Test coverage

    View full-size slide

  22. But we live in a real
    world

    View full-size slide

  23. class InstitutionsController < ApplicationController
    load_and_authorize_resource :only => [:destroy,:edit,:new,:create,:update]
    before_filter :authenticate_user!, :except =>
    [:student_registration, :show, :validate_registration_pin, :result, :admission, :buy_registration_pin,:paygate_callback_failure, :paygate_cancel, :paygate_pending, :paygate_callback_success, :pi
    nsaction_info_print]
    before_filter :find_institution, :except =>
    [:show,:index, :new, :create, :semesters_for_institute_type, :start_end_date_for_assessment_period, :courses_for_batch, :paygate_callback_failure, :paygate_cancel, :paygate_pending, :paygate_cal
    _success, :pin_transaction_info_print]
    before_filter :add_bread_crumb,:except => [:show]
    def paygate_callback_success
    @pay_gate_config = YAML::load(File.open("#{Rails.root}/config/pay_gate_config.yml"))[Rails.env]
    @payment = TransactionRecord.find_by_order_number(params[:OrderID])
    uri = URI("https://fidelitypaygate.fidelitybankplc.com/cipg/MerchantServices/UpayTransactionStatus.ashx")
    parameters = {:MERCHANT_ID => "#{@pay_gate_config['merchant_id']}", :ORDER_ID => "#{@payment.order_number}"}
    uri.query = URI.encode_www_form(parameters)
    result =open(uri).read
    result_hash = Hash.from_xml(result)
    record_payment_details(result_hash)
    if result_hash["CIPG"]["StatusCode"] == PaymentRecord::PAYMENT_SUCCESS_CODE
    if @payment.transactionable_type.eql?("PaymentRecord")
    redirect_to institution_fees_path(@payment.transactionable_type.fee.institution), :notice => "Payment transaction has been #{result_hash['CIPG']['Status']}"
    elsif @payment.transactionable_type.eql?("PinBuyerInfo")
    unless @payment.transactionable.pin_id.present?
    @registration = @payment.transactionable.registration
    @valid_registration_pin_groups = @registration.valid_registration_pin_groups
    @online_valid_registration_pin_groups = @valid_registration_pin_groups.where(:pin_available_type => 'Online')
    @offline_valid_registration_pin_groups = @valid_registration_pin_groups.where(:pin_available_type => 'Offline')
    @available_pin = nil
    @online_valid_registration_pin_groups.each do |vpg|
    if vpg.available_pins.present?
    @available_pin = vpg.available_pins.first
    break
    else
    next
    end
    end
    if !@available_pin.present?
    @offline_valid_registration_pin_groups.each do |vpg|
    if vpg.available_pins.present?
    @available_pin = vpg.available_pins.first
    break
    else
    next
    end
    end
    end
    if @available_pin.present?
    @payment.transactionable.pin_id = @available_pin.id
    @payment.transactionable.save
    @available_pin.is_available = false
    @available_pin.save
    @available_pin.reload
    if @available_pin.pin_group.pin_available_type == 'Offline'
    @message = "Your Order Number is #{@payment.order_number}. Please Print Transaction report to claim you PIN from Institution."
    else
    @assigned_pin = @available_pin.number
    @message = "Your PIN is #{@assigned_pin}. Please keep this PIN secret."
    end
    else
    @message = "No pin available for #{@registration.name}. Your order number is #{@payment.order_number}.Please contact with institution to get your money back if you have paid."
    end
    else
    pin = Pin.find @payment.transactionable.pin_id
    if(pin.present? && pin.pin_group.pin_available_type == 'Offline')
    @message = "Your Order Number is #{@payment.order_number}. Please Print Transaction report to claim you PIN from Institution."
    else
    @available_pin = Pin.find @payment.transactionable.pin_id
    @assigned_pin = @available_pin.number
    @message = "Your PIN is #{ @assigned_pin}. Please keep this PIN secret."

    View full-size slide

  24. How can we improve
    the situation?

    View full-size slide

  25. Abstraction
    Actions

    View full-size slide

  26. class InstitutionsController < ApplicationController
    load_and_authorize_resource :only => [:destroy,:edit,:new,:create,:update]
    before_filter :authenticate_user!, :except =>
    [:student_registration, :show, :validate_registration_pin, :result, :admission, :buy_registration_pin,:paygate_callback_failure, :paygate_cancel, :paygate_pending, :paygate_callback_success, :pi
    nsaction_info_print]
    before_filter :find_institution, :except =>
    [:show,:index, :new, :create, :semesters_for_institute_type, :start_end_date_for_assessment_period, :courses_for_batch, :paygate_callback_failure, :paygate_cancel, :paygate_pending, :paygate_cal
    _success, :pin_transaction_info_print]
    before_filter :add_bread_crumb,:except => [:show]
    def paygate_callback_success
    @pay_gate_config = YAML::load(File.open("#{Rails.root}/config/pay_gate_config.yml"))[Rails.env]
    @payment = TransactionRecord.find_by_order_number(params[:OrderID])
    uri = URI("https://fidelitypaygate.fidelitybankplc.com/cipg/MerchantServices/UpayTransactionStatus.ashx")
    parameters = {:MERCHANT_ID => "#{@pay_gate_config['merchant_id']}", :ORDER_ID => "#{@payment.order_number}"}
    uri.query = URI.encode_www_form(parameters)
    result =open(uri).read
    result_hash = Hash.from_xml(result)
    record_payment_details(result_hash)
    if result_hash["CIPG"]["StatusCode"] == PaymentRecord::PAYMENT_SUCCESS_CODE
    if @payment.transactionable_type.eql?("PaymentRecord")
    redirect_to institution_fees_path(@payment.transactionable_type.fee.institution), :notice => "Payment transaction has been #{result_hash['CIPG']['Status']}"
    elsif @payment.transactionable_type.eql?("PinBuyerInfo")
    unless @payment.transactionable.pin_id.present?
    @registration = @payment.transactionable.registration
    @valid_registration_pin_groups = @registration.valid_registration_pin_groups
    @online_valid_registration_pin_groups = @valid_registration_pin_groups.where(:pin_available_type => 'Online')
    @offline_valid_registration_pin_groups = @valid_registration_pin_groups.where(:pin_available_type => 'Offline')
    @available_pin = nil
    @online_valid_registration_pin_groups.each do |vpg|
    if vpg.available_pins.present?
    @available_pin = vpg.available_pins.first
    break
    else
    next
    end
    end
    if !@available_pin.present?
    @offline_valid_registration_pin_groups.each do |vpg|
    if vpg.available_pins.present?
    @available_pin = vpg.available_pins.first
    break
    else
    next
    end
    end
    end
    if @available_pin.present?
    @payment.transactionable.pin_id = @available_pin.id
    @payment.transactionable.save
    @available_pin.is_available = false
    @available_pin.save
    @available_pin.reload
    if @available_pin.pin_group.pin_available_type == 'Offline'
    @message = "Your Order Number is #{@payment.order_number}. Please Print Transaction report to claim you PIN from Institution."
    else
    @assigned_pin = @available_pin.number
    @message = "Your PIN is #{@assigned_pin}. Please keep this PIN secret."
    end
    else
    @message = "No pin available for #{@registration.name}. Your order number is #{@payment.order_number}.Please contact with institution to get your money back if you have paid."
    end
    else
    pin = Pin.find @payment.transactionable.pin_id
    if(pin.present? && pin.pin_group.pin_available_type == 'Offline')
    @message = "Your Order Number is #{@payment.order_number}. Please Print Transaction report to claim you PIN from Institution."
    else
    @available_pin = Pin.find @payment.transactionable.pin_id
    @assigned_pin = @available_pin.number

    View full-size slide

  27. MVC
    V and C MVC

    View full-size slide

  28. Abstraction
    Functional (callable)
    objects

    View full-size slide

  29. Services
    Interactors
    dry-transactions
    dry-monads (DO notations)
    Operations
    Etc.

    View full-size slide

  30. func_object = FuncObject.new
    func_object.call(params)
    # => result object with state

    View full-size slide

  31. func_object = FuncObject.new
    func_object.call(params)
    # => result object with state

    View full-size slide

  32. func_object = FuncObject.new
    func_object.call(params)
    # => result object

    View full-size slide

  33. def call(params)
    result = FuncObject.new.call(params)
    if result.successful?
    flash[:info] = INFO_MESSAGE
    redirect_to routes.tasks_path
    else
    self.body = …
    end
    end

    View full-size slide

  34. def call(params)
    result = FuncObject.new.call(params)
    if result.successful?
    flash[:info] = INFO_MESSAGE
    redirect_to routes.tasks_path
    else
    self.body = …
    end
    end

    View full-size slide

  35. def call(params)
    result = FuncObject.new.call(params)
    if result.successful?
    flash[:info] = INFO_MESSAGE
    redirect_to routes.tasks_path
    else
    self.body = …
    end
    end

    View full-size slide

  36. Logic sequence

    View full-size slide

  37. class Create
    include Dry::Transaction
    include Dry::Matcher
    Dry::Validation.load_extensions(:monads)
    VALIDATOR = Dry::Validation.JSON do
    # ...
    end
    step :extract
    step :validate
    step :create

    View full-size slide

  38. class Create
    include Dry::Transaction
    include Dry::Matcher
    Dry::Validation.load_extensions(:monads)
    VALIDATOR = Dry::Validation.JSON do
    # ...
    end
    step :extract
    step :validate
    step :create

    View full-size slide

  39. class Interactor
    def call(payload)
    data = yield extract!(payload)
    entity = yield validate!(data)
    create!(entity)
    end
    # …
    end

    View full-size slide

  40. class Interactor
    def call(payload)
    extract!
    validate!
    create!
    end
    # …
    end

    View full-size slide

  41. No global state

    View full-size slide

  42. def call(params)
    result = FuncObject.new.call(params)
    if result.successful?
    flash[:info] = INFO_MESSAGE
    redirect_to routes.tasks_path
    else
    self.body = …
    end
    end

    View full-size slide

  43. Easy testing

    View full-size slide

  44. def initialize(object: FuncObject.new)
    @object = object
    end
    def call(params)
    result = @object.call(params)
    if result.successful?
    flash[:info] = INFO_MESSAGE
    redirect_to routes.tasks_path
    else
    self.body = …
    end
    end

    View full-size slide

  45. def initialize(object: FuncObject.new)
    @object = object
    end
    def call(params)
    result = @object.call(params)
    if result.successful?
    flash[:info] = INFO_MESSAGE
    redirect_to routes.tasks_path
    else
    self.body = …
    end
    end
    Dependency Injection ❤

    View full-size slide

  46. let(:action) { Create.new(object: SuccessObject.new) }
    it { expect(action.call(params).to eq ... }


    let(:action) { Create.new(object: FailedObject.new) }
    it { expect(action.call(params).to eq ... }

    View full-size slide

  47. let(:other_object) { -> (_) { nil } }

    let(:object) { Create.new(object: other_object) }
    it { expect(action.call(params).to eq ... }

    View full-size slide

  48. Objects can call
    other objects

    View full-size slide

  49. Actions Interactors Libs
    Objects
    (public) (private)

    View full-size slide

  50. So many objects,

    how to manage it?

    View full-size slide

  51. Long names for objects — like:

    Interactors::CreateUserWithTask

    Interactors::Module::CreateUserWithTask

    Interactors::Module::Module::CreateUserWithTask

    Orders::Current::Coupons::Operations::Apply

    View full-size slide

  52. We have to initialize
    instances every time

    View full-size slide

  53. Abstraction
    Containers

    View full-size slide

  54. Dry-container

    View full-size slide

  55. When someone tries to sell me dry-rb

    View full-size slide

  56. Container = Dry::Container.new
    Container.register('interactors.create_user', Interactors::CreateUser.new)
    Container['interactors.create_user']
    # => Interactors::CreateUser instance
    Container['interactors.create_user'].call(...)

    View full-size slide

  57. Container = Dry::Container.new
    Container.register('interactors.create_user', Interactors::CreateUser.new)
    Container['interactors.create_user']
    # => Interactors::CreateUser instance
    Container['interactors.create_user'].call(...)

    View full-size slide

  58. Container = Dry::Container.new
    Container.register('interactors.create_user', Interactors::CreateUser.new)
    Container['interactors.create_user']
    # => Interactors::CreateUser instance
    Container['interactors.create_user'].call(...)

    View full-size slide

  59. def call(params)
    result = Container['interactors.create_user'].call(params)
    if result[:successful]
    flash[:info] = INFO_MESSAGE
    redirect_to routes.tasks_path
    else
    self.body = …
    end
    end

    View full-size slide

  60. Pros
    • Controlled global state (in one place)
    • Memorizing instances
    • Mocs for testing

    View full-size slide

  61. But we can use it
    better!

    View full-size slide

  62. Abstraction
    Dry-auto_inject

    View full-size slide

  63. Dependency injection
    on steroids

    View full-size slide

  64. Import = Dry::AutoInject(Container)

    View full-size slide

  65. include Import[‘interactors.create_user']


    def call(params)
    result = create_user.call(params)
    if result[:successful]
    flash[:info] = INFO_MESSAGE
    redirect_to routes.tasks_path
    else
    self.body = …
    end
    end

    View full-size slide

  66. include Import[‘interactors.create_user']


    def call(params)
    result = create_user.call(params)
    if result[:successful]
    flash[:info] = INFO_MESSAGE
    redirect_to routes.tasks_path
    else
    self.body = …
    end
    end
    Dependency Injection ❤

    View full-size slide

  67. include Import[‘interactors.create_user']


    def call(params)
    result = create_user.call(params)
    if result[:successful]
    flash[:info] = INFO_MESSAGE
    redirect_to routes.tasks_path
    else
    self.body = …
    end
    end

    View full-size slide

  68. let(:action) { Create.new(create_user:
    MockObject.new) }
    it { expect(action.call(params).to eq ... }

    View full-size slide

  69. Abstraction
    Models

    View full-size slide

  70. MVC
    V and C MVC

    View full-size slide

  71. What can we do with it?

    View full-size slide

  72. No validations

    View full-size slide

  73. Just save and load data

    View full-size slide

  74. Query objects

    View full-size slide

  75. Query objects
    Repositories

    View full-size slide

  76. Changesets

    or

    Command pattern (?)

    View full-size slide

  77. Drop callbacks

    View full-size slide

  78. Sequential logic

    View full-size slide

  79. class CreateUser
    include Import['repos.user_repo']
    def call(payload)
    user_repo.create(payload.merge(created_on: Date.today
    end
    end
    CreateUser.new.call(user_data)

    View full-size slide

  80. Abstraction
    Applications

    View full-size slide

  81. Rails App #1
    Model

    Services

    View full-size slide

  82. Rails App #2
    Model

    Services
    Rails App #1
    Model

    Services

    View full-size slide

  83. Rails App #1
    Rails App #2
    Model

    Services

    View full-size slide

  84. Rails App #1
    Rails App #2
    Model

    Services
    Rails App #3
    Model

    Services

    View full-size slide

  85. Rails App #1 Rails App #3
    Rails App #2
    Model

    Services

    View full-size slide

  86. App App App
    lib

    View full-size slide

  87. App App App
    lib
    Shared between apps

    View full-size slide

  88. Actions Interactors Libs
    (public) (private)

    View full-size slide

  89. Apps Interactors Libs
    (public) (private)

    View full-size slide

  90. web

    admin #1

    admin #2 ==> business

    api logic

    mobile

    View full-size slide

  91. App App App
    lib
    One server instance

    View full-size slide

  92. App App App
    lib
    One server instance
    App App App
    lib
    App App App
    lib
    App App App
    lib
    App App App
    lib
    App App App
    lib
    App App App
    lib
    App App App
    lib

    View full-size slide

  93. Hard not to forgot

    about other apps

    View full-size slide

  94. Sometimes

    you have to use

    another logic

    View full-size slide

  95. Warning: Advanced level
    You can build projects
    without it

    View full-size slide

  96. Abstraction
    dry-system

    View full-size slide

  97. Dependency
    management system

    View full-size slide

  98. Auto register
    dependency to
    container

    View full-size slide

  99. Dependency booting

    View full-size slide

  100. Abstraction
    Domains

    View full-size slide

  101. Separate domains
    for different business
    values

    View full-size slide

  102. Each domain
    isolated from others

    View full-size slide

  103. pros
    easy to create separated
    services: just replace code
    domains + containers = ❤

    View full-size slide

  104. cons
    hard to start
    so much files™

    View full-size slide

  105. http is a small part

    of system

    View full-size slide

  106. websockets
    rack
    background processing

    ETL


    View full-size slide

  107. They all use business
    logic

    View full-size slide

  108. System consists

    of the different events

    (Event Storming)

    View full-size slide

  109. User created
    Post created
    User updated

    Payment added
    Order completed

    View full-size slide

  110. Abstraction
    Event Sourcing

    View full-size slide

  111. But we need to
    calculate all events
    for get current state

    View full-size slide

  112. CQRS


    (Command Query
    Responsibility
    Segregation)

    View full-size slide

  113. – Martin Fowler
    “You can use a different
    model to update information
    than the model you use to
    read information”

    View full-size slide

  114. Event log
    SQL
    Elastic
    NoSql

    View full-size slide

  115. Event log
    SQL
    Elastic
    NoSql
    Current State

    View full-size slide

  116. Event log
    SQL
    Elastic
    NoSql
    Current State
    Current State

    View full-size slide

  117. Event log
    SQL
    Elastic
    NoSql
    Current State
    Current State
    Ap Ap Ap
    lib
    Ap Ap Ap
    lib

    View full-size slide

  118. Pros
    • Each service isolated
    • All processing is in the background
    • Easy to add instances for service
    • Can be written on any language and you
    can call it from any app
    • Persistance

    View full-size slide

  119. Cons
    • Not a silver bullet
    • Hard to understand the whole chain of events
    • Complicated
    • Another architecture type

    with different DB structure
    • Not popular in ruby

    View full-size slide

  120. How to start to use it

    View full-size slide

  121. github.com/RailsEventStore/
    rails_event_store

    View full-size slide

  122. github.com/zilverline/sequent

    View full-size slide

  123. hanami-events

    View full-size slide

  124. Proof of concept

    View full-size slide

  125. Just pub sub
    transport layer
    with event versions
    and types

    View full-size slide

  126. Not only rails and hanami

    View full-size slide

  127. Event Sourcing

    View full-size slide

  128. Don’t use anything

    having no idea

    why do you need it

    View full-size slide

  129. Maintenance is
    important

    View full-size slide

  130. github.com/davydovanton/
    hanami-architecture

    View full-size slide

  131. github.com/davydovanton/
    cookie_box

    View full-size slide

  132. git.io/vFxl1

    View full-size slide

  133. github.com/davydovanton

    twitter.com/anton_davydov
    davydovanton.com
    Thank you ❤

    View full-size slide