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

Hopscotch

 Hopscotch

Simplifying complex business logic

Avatar for Garrett Heinlen

Garrett Heinlen

November 10, 2015
Tweet

More Decks by Garrett Heinlen

Other Decks in Technology

Transcript

  1. KILLER SICK OBJECT class User < ActiveRecord::Base def full_name "#{first_name}

    #{last_name}" end alias_method :to_s, :full_name def fourty_two 42 end end
  2. class User < ActiveRecord::Base # ... before_validation(on: :create) do create_password

    end def create_password return if plain_password.present? # I'm creating.. why would this already exist? w1 = Word.randomised.simple_word.first.word # adding dependent object to creation. self.plain_password = "#{ w1 }#{ "%02d" % rand(100) }" # !" set_password # I call another method..? end def set_password return if !changed.include?("plain_password") # active record dirty? maybe? self.password = plain_password self.password_confirmation = plain_password end # ... end
  3. !

  4. IT STARTS SMALL before_validation(on: :create) do create_password end - Adds

    complexity to the `Student` model. - Makes the process of creating a `Student` depend on other objects (hard to know from the outside) - Can make things very hard to test. - Makes this type of functionality hard to reuse.
  5. IT GROWS before_validation(on: :create) do create_password set_login_from_name create_info end before_validation

    do set_info_student set_password end after_create do initialize_student set_access_all_areas if demo_only? end before_save do set_school_from_teacher set_inactive_if_no_teacher set_active_if_teacher end after_save do add_trial_if_home_school_linked update_country_code! flag_for_update_parent clear_cart_items if parent_id_changed? end after_destroy do flag_for_update_parent clear_cart_items end after_commit do update_parent end
  6. !

  7. WHY SO BAD? > Hard to test > Hard to

    reason about (wtf object?) > Debugging == !" > Explicit > Magic (almost always!)
  8. WHAT IS IT EVEN? - A way to simplify interactions

    within a system - A way to reuse similar steps in a complex system - Very declarative way to express business rules - Safe! Helps ensure everything is atomic*
  9. HOPSCOTCH::STEP - TL;DR: function returns either `success!` or `failure!` -

    function protocol (must conform to the pattern) - a type specification Step1: !
  10. SIMPLE EXAMPLE -> { Hopscotch::Step.success! } abc = -> (number)

    do if number >= 1 Hopscotch::Step.success! else Hopscotch::Step.failure! end end # abc.call(100) => Hopscotch::Step.success! # abc.(0) => Hopscotch::Step.failure!
  11. SIMPLE EXAMPLE def update_record(record:, attributes:) if record.validate(attributes) && record.save Hopscotch::Step.success!

    # must return this else Hopscotch::Step.failure!(record) # or return this end end
  12. SIMPLE EXAMPLE def move_teacher(teachers, new_school) teacher_ids = teachers.pluck(:id) if ::Teacher.where(id:

    teacher_ids).update_all(school_id: new_school.id) Hopscotch::Step.success! else Hopscotch::Step.failure! end end
  13. BIT MORE INVOLVED def award_trading_card(student, realm) return Hopscotch::Step.failure!('Not eligible for

    trading card') unless student.eligible_for_trading_card return Hopscotch::Step.failure!('No trading cards for realm') unless trading_card = random_trading_card(realm) if student_trading_card = create_student_card(student, trading_card) Hopscotch::Step.success!(student_trading_card) else Hopscotch::Step.failure!('Error creating student trading card') end end private def random_trading_card(realm) TradingCard.random_from_realm(realm) end def create_student_card(student, trading_card) student_trading_card = StudentTradingCard.create(student: student, trading_card: trading_card) student.eligible_for_trading_card = false student.save student_trading_card end
  14. !

  15. HOPSCOTCH::STEPCOMPOSER - TL;DR: combines steps - Composes a list of

    functions into a single function - Checks errors via "typechecking" the Step's return value Step1: ! Step2: " StepComposer: #
  16. HUH? success_step = -> { Hopscotch::Step.success! } fail_step = ->

    { Hopscotch::Step.failure!("bad") } success_reduced_fn = Hopscotch::StepComposer.compose_with_error_handling( success_step, success_step, success_step ) error_reduced_fn = Hopscotch::StepComposer.compose_with_error_handling( success_step, fail_step, # will catch the failure here success_step )
  17. SIMPLE EXAMPLE def setup_teacher_trial(teacher) Hopscotch::StepComposer.call_each( -> { update_email_flags(teacher) }, ->

    { create_teacher_subscription(teacher) }, -> { create_demo_student(teacher) }, -> { create_school_class(teacher) } ) end
  18. !

  19. HOPSCOTCH::RUNNER - Wraps a function in a transaction block -

    Calls a success or failure callback depending on the function result - Is the pipeline that ties everything together Runner: !, success: ", failure: #
  20. SIMPLE CONTROLLER EXAMPLE class TeachersController < ApplicationController def setup_trial Hopscotch::Runner.call(

    setup_teacher_trial, success: -> { redirect_to dashboard_path, notice: "Howdy." }, failure: -> (teacher) { render :new } ) end private def setup_teacher_trial(teacher) Hopscotch::StepComposer.call_each( -> { update_email_flags(teacher) }, -> { create_teacher_subscription(teacher) }, -> { create_demo_student(teacher) }, -> { create_school_class(teacher) } ) end end
  21. CSV EXAMPLE def import(file, success:, failure:) ::Hopscotch::Runner.call_each( # convenience method

    -> { validate_student_file(file) }, -> { upload_student_file(file) }, success: success, failure: failure ) end
  22. THERE IS NO MAGIC Hopscotch::Runner.call_each(steps, success: success, failure: failure) #

    is just calling Hopscotch::Runner.call( Hopscotch::StepComposer.compose_with_error_handling(steps), success: success, failure: failure )
  23. ...

  24. SERVICES module Service::AwardEggs extend self EGGS = 10 def call(student,

    eggs = EGGS) if EggLedger.new(student).add(eggs) Hopscotch::Step.success!("Eggs were awarded successfully.") else Hopscotch::Step.failure!("Could not award eggs.") end end end
  25. SERVICES module Service::AwardStudent extend self def call(student) Hopscotch::StepComposer.call_each( -> {

    Service::AwardEggs.call(student) }, -> { Service::AwardTradingCard.call(student) }, -> { Service::AwardTrophy.call(student) } ) end end
  26. WHAT IS A SERVICE - Wrapper for a Hopscotch::Step -

    (or Hopscotch::StepComposer) - Conform to 1 public `#call` method - Does 1 small thing - Great for reuse
  27. WORKFLOWS module Workflow::School::MigrateToRex extend self def call(school, success:, failure:) ::Hopscotch::Runner.call(

    rex_migration_steps(school), success: -> { success.call("Migrated school: #{school.name} to the new version of REX") }, failure: -> { failure.call("Migration failed! Please contact support") } ) end def rex_migration_steps(school) ::Hopscotch::StepComposer.call_each( -> { ::Service::School::StartRexMigration.call(school) }, -> { ::Service::School::MarkEnabledForRex.call(school) }, ) end end # Call'em success = -> (message) { puts message } failure = -> (error) { puts error } Workflow::School::MigrateToRex.call(school, success: success, failure: failure)
  28. WHAT IS A WORKFLOW - Wrapper for a Hopscotch::Runner -

    Used to handle control flow for the entire process - Makes callbacks fun again! Passed in. Declarative. Awesome.
  29. RECAP - Hopscotch::Step - simple function that returns success! or

    failure! - Hopscotch::StepComposer - composes steps together to create 1 function - Hopscotch::Runner - calls 1 function and calls failure/success depending on functions results