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

How to Eat a Whale

How to Eat a Whale

You know you need to make changes. Upgrade your software, pull a Humpy Dumpty on the test suite, tame your production emergencies. And yet you never quite get there.

Let's talk through the strategies that are most effective, and how I've succeeded - and often failed - to get to the other side.

Joseph Mastey

July 12, 2019
Tweet

More Decks by Joseph Mastey

Other Decks in Programming

Transcript

  1. HOW TO EAT A WHALE
    MAKING THE CHANGES YOU KNOW YOU NEED TO

    View Slide

  2. Have you heard of tiny Melinda Mae,
    Who ate a monstrous whale?
    She thought she could,
    She said she would,
    So she started in right at the tail.
    - Shel Silverstein

    View Slide

  3. not a criticism

    View Slide

  4. WE HAVE
    TO CHANGE

    View Slide

  5. ▸ Hard to Maintain
    ▸ New Features
    ▸ Better Performance
    ▸ Security
    ▸ Ergonomics
    ▸ Hiring

    View Slide

  6. RAILS 5 AS
    A CASE STUDY

    View Slide

  7. it’s a common
    upgrade

    View Slide

  8. ▸ 1.2 ➡ 4.2
    ▸ 3.0 ➡ 5.0
    ▸ 4.2 ➡ 5.0
    ▸ 4.2 ➡ 5.0
    success
    success
    failure
    ongoing!

    View Slide

  9. this works
    for other big changes

    View Slide

  10. WHY WE
    DON’T CHANGE

    View Slide

  11. the changes
    are ambiguous

    View Slide

  12. the risk
    is high

    View Slide

  13. it’s a lot
    of work

    View Slide

  14. source: git log -L 5,6:Gemfile
    Date Rails Version
    Jun ‘19 4.2.11
    Aug ‘17 4.2.7.1
    Dec ‘16 4.2.7
    Mar ‘16 4.1.13
    Mar ‘16 4.2.5.2
    Feb ‘16 4.2.5.1
    Aug ‘15 4.1.13
    Jan ‘15 4.1.9
    Dec ‘14 4.0.0
    … …
    May ‘13 3.2.8

    View Slide

  15. but there
    is hope!

    View Slide

  16. 1. REDUCE AMBIGUITY

    View Slide

  17. try to get a
    list of changes

    View Slide

  18. ▸ changelogs
    ▸ upgrade guide
    ▸ gem dependency graph
    ▸ try changes and see what explodes

    View Slide

  19. View Slide

  20. View Slide

  21. uncover sneaky
    problems

    View Slide

  22. # old
    class FactoryGirl
    # new
    class FactoryBot

    View Slide

  23. class Money::Arithmetic
    def ==(other)
    # don’t allow comparison w/ non-Money objects
    unless other.is_a? Money || other.zero?
    raise ArgumentError
    end
    # ... do comparison
    end
    end

    View Slide

  24. class Money::Arithmetic
    def ==(other)
    # warn about problems
    if other.is_a?(Numeric) && other.nonzero?
    trigger_warning_and_rollbar
    end
    super # run actual comparison
    end
    end

    View Slide

  25. keep strong
    test coverage

    View Slide

  26. 2. REDUCE RISK

    View Slide

  27. take tiny steps

    View Slide

  28. ‣ rails 5

    ‣ factory_bot

    ‣ belongs_to

    ‣ protected_attributes

    ‣ strong params

    ‣ activeadmin

    ‣ devise

    ‣ upgrade ruby

    ‣ rails 4.2.11

    ‣ sass

    ‣ coffee

    ‣ money

    ‣ etc…

    ‣ quiet_assets*

    View Slide

  29. ‣ factory bot

    ‣ add shim

    ‣ use shim for existing cases

    ‣ upgrade factory girl to most recent version

    ‣ switch to factory bot and remove shim
    lots of changes, low risk
    tiny change, highest risk

    View Slide

  30. View Slide

  31. update code,
    then dependency

    View Slide

  32. FactoryBot.define AtlRollout do
    # ...
    end

    View Slide

  33. # warning: BigDecimal.new is deprecated;
    # use BigDecimal() method instead.
    (BigDecimal.new('-50.00')..BigDecimal.new('50.00')) => 'cold',

    View Slide

  34. # in rails 4, this is optional
    # in rails 5, it’s required
    belongs_to :packing_facility
    # this works in both!
    belongs_to :packing_facility, required: false

    View Slide

  35. Make the change easy,
    Then make the easy change.
    - Kent Beck

    View Slide

  36. surface errors quickly,
    in the right place

    View Slide

  37. NoMethodError:
    undefined method 'sellout_limit' for nil:NilClass
    # ./app/models/meal.rb:531:in 'sellout_limit_for'
    # ./app/models/menu.rb:254:in 'meal_sold_out?'
    # ./app/models/meal_selection.rb:208:in 'validate_meal_selection'

    View Slide

  38. ArgumentError:
    Couldn’t find 3rd meal for plan 'Standard' in menu ’15-jul-2019'
    (2 meals for plan)
    # ./spec/factories/weekly_baskets.rb:131:in `validate_selection!'
    # ./spec/factories/weekly_baskets.rb:61:in `block (5 levels)’
    # ./spec/factories/weekly_baskets.rb:58:in `times'
    # ./spec/factories/weekly_baskets.rb:58:in `block (4 levels)’

    View Slide

  39. 3. REDUCE EFFORT

    View Slide

  40. it’s okay to be
    a bit clever

    View Slide

  41. # in rails 4, this is optional
    # in rails 5, it’s required
    belongs_to :packing_facility

    View Slide

  42. WeeklyBasket.reflect_on_all_associations.each do |association|
    next if association.macro != :belongs_to
    foreign_key_field = klass.reflections[association.name.to_s].foreign_key
    column = klass.columns.find { |c| c.name == foreign_key_field.to_s }
    required = association.options.fetch(:required, true) &&
    !association.options.fetch(:optional, false)
    if required && column.null
    exceptions << sprintf(“%-50s required, but column is nullable",
    "#{klass}##{association.name}")
    elsif !required && !column.null
    exceptions << sprintf("%-50s not required, but column is not nullable",
    "#{klass}##{association.name}")
    end
    end

    View Slide

  43. WeeklyBasket.reflect_on_all_associations.each do |association|
    end
    next if association.macro != :belongs_to
    foreign_key_field = klass.reflections[association.name.to_s].foreign_key
    column = klass.columns.find { |c| c.name == foreign_key_field.to_s }
    required = association.options.fetch(:required, true) &&
    !association.options.fetch(:optional, false)
    if required && column.null
    exceptions << sprintf(“%-50s required, but column is nullable",
    "#{klass}##{association.name}")
    elsif !required && !column.null
    exceptions << sprintf("%-50s not required, but column is not nullable",
    "#{klass}##{association.name}")
    end

    View Slide

  44. WeeklyBasket#address will be required, but column is nullable
    WeeklyBasket#meal_plan will be required, but column is nullable
    WeeklyBasket#shipping_box will be required, but column is nullable
    WeeklyBasket#shipping_region will be required, but column is nullable

    View Slide

  45. namespaces
    and shims

    View Slide

  46. class Money::Arithmetic
    def ==(other)
    if other.is_a?(Numeric) && other.nonzero?
    trigger_warning_and_rollbar
    end
    super
    end
    end

    View Slide

  47. View Slide

  48. namespace :rails_5 do
    task check_belongs_to_associations: :environment do
    silence_warnings do
    print "Eager Loading..."
    Rails.application.eager_load!
    puts “done.”
    # ... do comparison
    end
    end
    end

    View Slide

  49. do the
    hard work

    View Slide

  50. View Slide

  51. MOVING TARGETS:
    PEOPLE RISKS

    View Slide

  52. it's hard to
    relearn everything

    View Slide

  53. short lived
    changes (again)

    View Slide

  54. make it easier
    to do the right thing

    View Slide

  55. TO SUM THINGS UP

    View Slide

  56. ambiguity happens,
    risk happens,
    effort happens.
    but you
    minimize them

    View Slide

  57. View Slide

  58. THANKS!

    View Slide