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. -> [email protected]
    title: RubyKaigi2k18

    View Slide

  2. ❤


    Hello Japan!



    View Slide

  3. What have I learned here?

    View Slide

  4. Is ramen still awesome?

    View Slide

  5. View Slide

  6. Is Java more popular
    than ruby?

    View Slide

  7. View Slide

  8. Is Ruby still dying?

    View Slide

  9. View Slide

  10. Is RubyKaigi still
    awesome?

    View Slide

  11. View Slide

  12. ❤


    Hello RubyKaigi!



    View Slide

  13. Anton Davydov
    github.com/davydovanton

    twitter.com/anton_davydov
    davydovanton.com

    View Slide

  14. View Slide

  15. Stickers

    View Slide

  16. View Slide

  17. ~200 slides

    View Slide

  18. -> [email protected]
    title: RubyKaigi2k18

    View Slide

  19. Architecture

    View Slide

  20. View Slide

  21. Why?

    View Slide

  22. Trying to unify architecture
    ideas for hanami projects

    View Slide

  23. Getting feedback

    View Slide

  24. Problems

    View Slide

  25. Management
    Performance
    Maintenance

    View Slide

  26. Management
    Performance
    Maintenance

    View Slide

  27. Clean code
    Good architecture
    Maintainable product
    Pretty code

    View Slide

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

    View Slide

  29. What do we really
    want?

    View Slide

  30. To add new features

    View Slide

  31. Isolation
    Sequential logic
    No global state

    View Slide

  32. Delete (refactoring)
    old code

    View Slide

  33. Isolation
    Test coverage

    View Slide

  34. Conclusions

    View Slide

  35. Isolation
    Sequential logic
    No global state

    Test coverage

    View Slide

  36. But we live in a real
    world

    View Slide

  37. 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 [email protected]_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 Slide

  38. How can we improve
    the situation?

    View Slide

  39. View Slide

  40. Abstraction
    Actions

    View Slide

  41. 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 [email protected]_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 Slide

  42. MVC
    V and C MVC

    View Slide

  43. Solutions?

    View Slide

  44. Abstraction
    Functional (callable)
    objects

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  49. Isolation

    View Slide

  50. 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 Slide

  51. 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 Slide

  52. 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 Slide

  53. Logic sequence

    View Slide

  54. 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 Slide

  55. 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 Slide

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

    View Slide

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

    View Slide

  58. No global state

    View Slide

  59. 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 Slide

  60. Easy testing

    View Slide

  61. 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 Slide

  62. 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 Slide

  63. 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 Slide

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

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

    View Slide

  65. Objects can call
    other objects

    View Slide

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

    View Slide

  67. Problems

    View Slide

  68. So many objects,

    how to manage it?

    View Slide

  69. Long names for objects — like:

    Interactors::CreateUserWithTask

    Interactors::Module::CreateUserWithTask

    Interactors::Module::Module::CreateUserWithTask

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

    View Slide

  70. We have to initialize
    instances every time

    View Slide

  71. Containers!

    View Slide

  72. Abstraction
    Containers

    View Slide

  73. Dry-container

    View Slide

  74. When someone tries to sell me dry-rb

    View Slide

  75. 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 Slide

  76. 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 Slide

  77. 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 Slide

  78. 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 Slide

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

    View Slide

  80. But we can use it
    better!

    View Slide

  81. Abstraction
    Dry-auto_inject

    View Slide

  82. View Slide

  83. Why?

    View Slide

  84. Dependency injection
    on steroids

    View Slide

  85. Import = Dry::AutoInject(Container)

    View Slide

  86. 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 Slide

  87. 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 Slide

  88. 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 Slide

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

    View Slide

  90. Abstraction
    Models

    View Slide

  91. MVC
    V and C MVC

    View Slide

  92. What can we do with it?

    View Slide

  93. No logic

    View Slide

  94. No validations

    View Slide

  95. Just save and load data

    View Slide

  96. Query objects

    View Slide

  97. Query objects
    Repositories

    View Slide

  98. View Slide

  99. Changesets

    or

    Command pattern (?)

    View Slide

  100. Drop callbacks

    View Slide

  101. Sequential logic

    View Slide

  102. Examples

    View Slide

  103. 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 Slide

  104. View Slide

  105. Abstraction
    Applications

    View Slide

  106. Rails App #1
    Model

    Services

    View Slide

  107. Rails App #2
    Model

    Services
    Rails App #1
    Model

    Services

    View Slide

  108. Rails App #1
    Rails App #2
    Model

    Services

    View Slide

  109. Rails App #1
    Rails App #2
    Model

    Services
    Rails App #3
    Model

    Services

    View Slide

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

    Services

    View Slide

  111. View Slide

  112. App App App
    lib

    View Slide

  113. App App App
    lib
    Shared between apps

    View Slide

  114. View Slide

  115. Actions Interactors Libs
    (public) (private)

    View Slide

  116. Apps Interactors Libs
    (public) (private)

    View Slide

  117. web

    admin #1

    admin #2 ==> business

    api logic

    mobile

    View Slide

  118. App App App
    lib
    One server instance

    View Slide

  119. 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 Slide

  120. Problems?

    View Slide

  121. Hard not to forgot

    about other apps

    View Slide

  122. Sometimes

    you have to use

    another logic

    View Slide

  123. View Slide

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

    View Slide

  125. Abstraction
    dry-system

    View Slide

  126. Dependency
    management system

    View Slide

  127. Auto register
    dependency to
    container

    View Slide

  128. View Slide

  129. View Slide

  130. View Slide

  131. View Slide

  132. Dependency booting

    View Slide

  133. View Slide

  134. Abstraction
    Domains

    View Slide

  135. DDD

    View Slide

  136. View Slide

  137. View Slide

  138. Separate domains
    for different business
    values

    View Slide

  139. Each domain
    isolated from others

    View Slide

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

    View Slide

  141. cons
    hard to start
    so much files™

    View Slide

  142. View Slide

  143. http is a small part

    of system

    View Slide

  144. View Slide

  145. websockets
    rack
    background processing

    ETL


    View Slide

  146. They all use business
    logic

    View Slide

  147. View Slide

  148. System consists

    of the different events

    (Event Storming)

    View Slide

  149. View Slide

  150. User created
    Post created
    User updated

    Payment added
    Order completed

    View Slide

  151. Abstraction
    Event Sourcing

    View Slide

  152. View Slide

  153. View Slide

  154. View Slide

  155. Events

    View Slide

  156. Event

    log

    View Slide

  157. Event

    log

    View Slide

  158. Event

    log

    View Slide

  159. View Slide

  160. View Slide

  161. View Slide

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

    View Slide

  163. CQRS


    (Command Query
    Responsibility
    Segregation)

    View Slide

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

    View Slide

  165. Event log

    View Slide

  166. Event log
    SQL
    Elastic
    NoSql

    View Slide

  167. Event log
    SQL
    Elastic
    NoSql
    Current State

    View Slide

  168. Event log
    SQL
    Elastic
    NoSql
    Current State
    Current State

    View Slide

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

    View Slide

  170. 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 Slide

  171. 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 Slide

  172. How to start to use it

    View Slide

  173. View Slide

  174. github.com/RailsEventStore/
    rails_event_store

    View Slide

  175. github.com/zilverline/sequent

    View Slide

  176. hanami-events

    View Slide

  177. Proof of concept

    View Slide

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

    View Slide

  179. Not only rails and hanami

    View Slide

  180. Event Sourcing

    View Slide

  181. Conclusions

    View Slide

  182. Isolate it

    View Slide

  183. Don’t use anything

    having no idea

    why do you need it

    View Slide

  184. Maintenance is
    important

    View Slide

  185. github.com/davydovanton/
    hanami-architecture

    View Slide

  186. github.com/davydovanton/
    cookie_box

    View Slide

  187. git.io/vFxl1

    View Slide

  188. -> [email protected]
    title: RubyKaigi2k18

    View Slide

  189. github.com/davydovanton

    twitter.com/anton_davydov
    davydovanton.com
    Thank you ❤

    View Slide