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

Hanami Architecture

Hanami Architecture

Anton Davydov

October 21, 2017
Tweet

More Decks by Anton Davydov

Other Decks in Programming

Transcript

  1. ❤


    Hello Minsk!



    View Slide

  2. Anton Davydov
    github.com/davydovanton

    twitter.com/anton_davydov
    davydovanton.com

    View Slide

  3. Stickers

    View Slide

  4. View Slide

  5. View Slide

  6. Architecture

    View Slide

  7. View Slide

  8. Why?

    View Slide

  9. Trying to unify architecture
    ideas for hanami projects

    View Slide

  10. Getting feedback

    View Slide

  11. Problems

    View Slide

  12. Management
    Performance
    Maintenance

    View Slide

  13. Management
    Performance
    Maintenance

    View Slide

  14. Clean code
    Good architecture
    Maintainable product
    Pretty code

    View Slide

  15. View Slide

  16. Clean code
    Good architecture
    Maintainable product
    Pretty code

    View Slide

  17. What do we really want?

    View Slide

  18. To add new features

    View Slide

  19. Isolation
    Sequential logic
    No global state

    View Slide

  20. Delete (refactoring) old code

    View Slide

  21. Isolation
    Test coverage

    View Slide

  22. Conclusions

    View Slide

  23. Isolation
    Sequential logic
    No global state

    Test coverage

    View Slide

  24. But we live in a real world

    View Slide

  25. 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, :pin_transaction_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_callback_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')

    View Slide

  26. How can we improve
    the situation?

    View Slide

  27. View Slide

  28. Abstraction
    Actions

    View Slide

  29. 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, :pin_transaction_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_callback_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')

    View Slide

  30. MVC
    V and C MVC

    View Slide

  31. Solutions?

    View Slide

  32. Abstraction
    Functional (callable) objects

    View Slide

  33. Services
    Interactors
    dry-transactions
    Operations
    Etc.

    View Slide

  34. t.me/pepegramming

    View 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 Slide

  36. 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

  37. def call(params)
    FuncObject.new.call(params) do |m|
    m.successful? do |value|
    flash[:info] = INFO_MESSAGE
    redirect_to routes.tasks_path
    end
    m.failed? do |value|
    self.body = …
    end
    end
    end

    View Slide

  38. Isolation

    View Slide

  39. 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

  40. 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

  41. 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

  42. Logic sequence

    View Slide

  43. class Create
    include Dry::Transaction
    include Platform::Matcher
    Dry::Validation.load_extensions(:monads)
    VALIDATOR = Platform::Validation.JSON do
    # ...
    end
    step :extract
    step :validate
    step :create
    def extract(attributes)
    # ...
    end
    def validate(attributes)
    VALIDATOR.call(attributes).to_either
    end

    View Slide

  44. class Create
    include Dry::Transaction
    include Platform::Matcher
    Dry::Validation.load_extensions(:monads)
    VALIDATOR = Platform::Validation.JSON do
    # ...
    end
    step :extract
    step :validate
    step :create
    def extract(attributes)
    # ...
    end
    def validate(attributes)
    VALIDATOR.call(attributes).to_either
    end

    View Slide

  45. class Interactor
    def call(paload)
    extract!
    validate!
    create!
    end
    # …
    end

    View Slide

  46. No global state

    View Slide

  47. 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

  48. Easy testing

    View Slide

  49. 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

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

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

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

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

    View Slide

  53. But objects

    can call other objects

    View Slide

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

    View Slide

  55. Problems

    View Slide

  56. So many objects,

    how to manage it?

    View Slide

  57. Long names for objects —
    like:

    Interactors::CreateUserWithTask

    Interactors::Module::CreateUserWithTask

    Interactors::Module::Module::CreateUserWithTask

    View Slide

  58. We have to initialize
    instances every time

    View Slide

  59. Containers!

    View Slide

  60. Abstraction
    Containers

    View Slide

  61. Dry-container

    View Slide

  62. When someone tries to sell me dry-rb

    View Slide

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

  64. 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

  65. 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

  66. 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

  67. Pros
    Controlled global state

    (in one place)
    Memorizing instances
    Mocs for testing
    Resolving deps on booting project

    View Slide

  68. t.me/pepegramming

    View Slide

  69. But we can use it better!

    View Slide

  70. Abstraction
    Dry-auto_inject

    View Slide

  71. When someone tries to sell me dry-rb

    View Slide

  72. Why?

    View Slide

  73. Dependency injection
    on steroids

    View Slide

  74. Import = Dry::AutoInject(Container)

    View Slide

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

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

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

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

    View Slide

  79. Abstraction
    Models

    View Slide

  80. MVC
    V and C MVC

    View Slide

  81. What can we do with it?

    View Slide

  82. No logic

    View Slide

  83. No validations

    View Slide

  84. Just save and load data

    View Slide

  85. Query objects

    View Slide

  86. Query objects
    Repositories

    View Slide

  87. View Slide

  88. Changesets

    or

    Command pattern (?)

    View Slide

  89. Drop callbacks

    View Slide

  90. Sequential logic

    View Slide

  91. Examples

    View Slide

  92. 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

  93. View Slide

  94. Abstraction
    Applications

    View Slide

  95. Rails App #1
    Model

    Services

    View Slide

  96. Rails App #2
    Model

    Services
    Rails App #1
    Model

    Services

    View Slide

  97. Rails App #1
    Rails App #2
    Model

    Services

    View Slide

  98. Rails App #1
    Rails App #2
    Model

    Services
    Rails App #3
    Model

    Services

    View Slide

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

    Services

    View Slide

  100. View Slide

  101. App App App
    lib

    View Slide

  102. App App App
    lib
    Shared between apps

    View Slide

  103. View Slide

  104. Actions Interactors Libs
    (public) (private)

    View Slide

  105. Apps Interactors Libs
    (public) (private)

    View Slide

  106. web

    admin #1

    admin #2 ==> business

    api logic

    mobile

    View Slide

  107. App App App
    lib
    One server instance

    View Slide

  108. 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

  109. Problems?

    View Slide

  110. Hard

    not to forgot

    about other apps

    View Slide

  111. Sometimes

    you have to use

    another logic

    View Slide

  112. View Slide

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

    View Slide

  114. Abstraction
    Domains

    View Slide

  115. DDD

    View Slide

  116. View Slide

  117. Separate domains
    for different business values

    View Slide

  118. Each domain
    isolated from others

    View Slide

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

    View Slide

  120. cons
    hard to start
    so much files™

    View Slide

  121. View Slide

  122. http is a small part

    of system

    View Slide

  123. View Slide

  124. websockets
    rack
    background processing

    ETL


    View Slide

  125. They all use business logic

    View Slide

  126. View Slide

  127. System consists

    of the different events

    View Slide

  128. View Slide

  129. User created
    Post created
    User updated

    Payment added
    Order completed

    View Slide

  130. Abstraction
    Event Sourcing

    View Slide

  131. View Slide

  132. View Slide

  133. View Slide

  134. View Slide

  135. Events

    View Slide

  136. Event

    log

    View Slide

  137. Event

    log

    View Slide

  138. Event

    log

    View Slide

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

    View Slide

  140. CQRS


    (Command Query
    Responsibility Segregation)

    View Slide

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

    View Slide

  142. Event log

    View Slide

  143. Event log
    SQL
    Elastic
    NoSql

    View Slide

  144. Event log
    SQL
    Elastic
    NoSql
    Current State

    View Slide

  145. Event log
    SQL
    Elastic
    NoSql
    Current State
    Current State

    View Slide

  146. Event log
    SQL
    Elastic
    NoSql
    Current State
    Current State
    App App App
    lib
    App App App
    lib

    View Slide

  147. 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

  148. Cons
    • Hard to understand the whole
    chain of events
    • Complicated
    • Another architecture type

    with different DB structure
    • Not popular in ruby

    View Slide

  149. How to start to use it

    View Slide

  150. View Slide

  151. github.com/RailsEventStore/rails_event_store

    View Slide

  152. hanami-events

    View Slide

  153. Proof of concept

    View Slide

  154. Just transport layer
    with event versions
    and types

    View Slide

  155. Conclusions

    View Slide

  156. Isolate it

    View Slide

  157. Don’t use anything

    having no idea

    why do you need it

    View Slide

  158. Maintenance is important

    View Slide

  159. github.com/davydovanton/hanami-architecture

    View Slide

  160. t.me/pepegramming

    View Slide

  161. github.com/davydovanton

    twitter.com/anton_davydov
    davydovanton.com
    Thank you ❤

    View Slide