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.

E7dfc74cd6e3194a22ef6fac2c51f0ae?s=128

Anton Davydov

May 31, 2018
Tweet

Transcript

  1. -> mail@davydovanton.com title: RubyKaigi2k18

  2. ❤
 
 Hello Japan!
 
 ❤

  3. What have I learned here?

  4. Is ramen still awesome?

  5. None
  6. Is Java more popular than ruby?

  7. None
  8. Is Ruby still dying?

  9. None
  10. Is RubyKaigi still awesome?

  11. None
  12. ❤
 
 Hello RubyKaigi!
 
 ❤

  13. Anton Davydov github.com/davydovanton
 twitter.com/anton_davydov davydovanton.com

  14. None
  15. Stickers

  16. None
  17. ~200 slides

  18. -> mail@davydovanton.com title: RubyKaigi2k18

  19. Architecture

  20. None
  21. Why?

  22. Trying to unify architecture ideas for hanami projects

  23. Getting feedback

  24. Problems

  25. Management Performance Maintenance

  26. Management Performance Maintenance

  27. Clean code Good architecture Maintainable product Pretty code

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

  29. What do we really want?

  30. To add new features

  31. Isolation Sequential logic No global state

  32. Delete (refactoring) old code

  33. Isolation Test coverage

  34. Conclusions

  35. Isolation Sequential logic No global state
 Test coverage

  36. But we live in a real world

  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 !@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."
  38. How can we improve the situation?

  39. None
  40. Abstraction Actions

  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 !@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
  42. MVC V and C MVC

  43. Solutions?

  44. Abstraction Functional (callable) objects

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

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

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

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

  49. Isolation

  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
  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
  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
  53. Logic sequence

  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
  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
  56. class Interactor def call(payload) data = yield extract!(payload) entity =

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

    end
  58. No global state

  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
  60. Easy testing

  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
  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 ❤
  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 ... }
  64. let(:other_object) { -> (_) { nil } }
 let(:object) {

    Create.new(object: other_object) } it { expect(action.call(params).to eq ... }
  65. Objects can call other objects

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

  67. Problems

  68. So many objects,
 how to manage it?

  69. Long names for objects — like: 
 Interactors::CreateUserWithTask
 Interactors::Module::CreateUserWithTask
 Interactors::Module::Module::CreateUserWithTask


    Orders::Current::Coupons::Operations::Apply
  70. We have to initialize instances every time

  71. Containers!

  72. Abstraction Containers

  73. Dry-container

  74. When someone tries to sell me dry-rb

  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(...)
  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(...)
  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(...)
  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
  79. Pros • Controlled global state (in one place) • Memorizing

    instances • Mocs for testing
  80. But we can use it better!

  81. Abstraction Dry-auto_inject

  82. None
  83. Why?

  84. Dependency injection on steroids

  85. Import = Dry::AutoInject(Container)

  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
  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 ❤
  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
  89. let(:action) { Create.new(create_user: MockObject.new) } it { expect(action.call(params).to eq ...

    }
  90. Abstraction Models

  91. MVC V and C MVC

  92. What can we do with it?

  93. No logic

  94. No validations

  95. Just save and load data

  96. Query objects

  97. Query objects Repositories

  98. None
  99. Changesets
 or
 Command pattern (?)

  100. Drop callbacks

  101. Sequential logic

  102. Examples

  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)
  104. None
  105. Abstraction Applications

  106. Rails App #1 Model
 Services

  107. Rails App #2 Model
 Services Rails App #1 Model
 Services

  108. Rails App #1 Rails App #2 Model
 Services

  109. Rails App #1 Rails App #2 Model
 Services Rails App

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


    Services
  111. None
  112. App App App lib

  113. App App App lib Shared between apps

  114. None
  115. Actions Interactors Libs (public) (private)

  116. Apps Interactors Libs (public) (private)

  117. web
 admin #1
 admin #2 ==> business
 api logic
 mobile

  118. App App App lib One server instance

  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
  120. Problems?

  121. Hard not to forgot
 about other apps

  122. Sometimes
 you have to use
 another logic

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

  125. Abstraction dry-system

  126. Dependency management system

  127. Auto register dependency to container

  128. None
  129. None
  130. None
  131. None
  132. Dependency booting

  133. None
  134. Abstraction Domains

  135. DDD

  136. None
  137. None
  138. Separate domains for different business values

  139. Each domain isolated from others

  140. pros easy to create separated services: just replace code domains

    + containers = ❤
  141. cons hard to start so much files™

  142. None
  143. http is a small part
 of system

  144. None
  145. websockets rack background processing
 ETL
 …

  146. They all use business logic

  147. None
  148. System consists
 of the different events
 (Event Storming)

  149. None
  150. User created Post created User updated
 Payment added Order completed

  151. Abstraction Event Sourcing

  152. None
  153. None
  154. None
  155. Events

  156. Event
 log

  157. Event
 log

  158. Event
 log

  159. None
  160. None
  161. None
  162. But we need to calculate all events for get current

    state
  163. CQRS
 
 (Command Query Responsibility Segregation)

  164. – Martin Fowler “You can use a different model to

    update information than the model you use to read information”
  165. Event log

  166. Event log SQL Elastic NoSql

  167. Event log SQL Elastic NoSql Current State

  168. Event log SQL Elastic NoSql Current State Current State

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

    Ap Ap lib Ap Ap Ap lib
  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
  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
  172. How to start to use it

  173. None
  174. github.com/RailsEventStore/ rails_event_store

  175. github.com/zilverline/sequent

  176. hanami-events

  177. Proof of concept

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

  179. Not only rails and hanami

  180. Event Sourcing

  181. Conclusions

  182. Isolate it

  183. Don’t use anything
 having no idea
 why do you need

    it
  184. Maintenance is important

  185. github.com/davydovanton/ hanami-architecture

  186. github.com/davydovanton/ cookie_box

  187. git.io/vFxl1

  188. -> mail@davydovanton.com title: RubyKaigi2k18

  189. github.com/davydovanton
 twitter.com/anton_davydov davydovanton.com Thank you ❤