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

Hopscotch

 Hopscotch

Simplifying complex business logic

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