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.

A9704266587836f7e784235e5073b93e?s=128

Joseph Mastey

July 12, 2019
Tweet

Transcript

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

    YOU NEED TO
  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
  3. not a criticism

  4. WE HAVE TO CHANGE

  5. ▸ Hard to Maintain ▸ New Features ▸ Better Performance

    ▸ Security ▸ Ergonomics ▸ Hiring
  6. RAILS 5 AS A CASE STUDY

  7. it’s a common upgrade

  8. ▸ 1.2 ➡ 4.2 ▸ 3.0 ➡ 5.0 ▸ 4.2

    ➡ 5.0 ▸ 4.2 ➡ 5.0 success success failure ongoing!
  9. this works for other big changes

  10. WHY WE DON’T CHANGE

  11. the changes are ambiguous

  12. the risk is high

  13. it’s a lot of work

  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
  15. but there is hope!

  16. 1. REDUCE AMBIGUITY

  17. try to get a list of changes

  18. ▸ changelogs ▸ upgrade guide ▸ gem dependency graph ▸

    try changes and see what explodes
  19. None
  20. None
  21. uncover sneaky problems

  22. # old class FactoryGirl # new class FactoryBot

  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
  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
  25. keep strong test coverage

  26. 2. REDUCE RISK

  27. take tiny steps

  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*
  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
  30. None
  31. update code, then dependency

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

  33. # warning: BigDecimal.new is deprecated; # use BigDecimal() method instead.

    (BigDecimal.new('-50.00')..BigDecimal.new('50.00')) => 'cold',
  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
  35. Make the change easy, Then make the easy change. -

    Kent Beck
  36. surface errors quickly, in the right place

  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'
  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)’
  39. 3. REDUCE EFFORT

  40. it’s okay to be a bit clever

  41. # in rails 4, this is optional # in rails

    5, it’s required belongs_to :packing_facility
  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
  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
  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
  45. namespaces and shims

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

    super end end
  47. None
  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
  49. do the hard work

  50. None
  51. MOVING TARGETS: PEOPLE RISKS

  52. it's hard to relearn everything

  53. short lived changes (again)

  54. make it easier to do the right thing

  55. TO SUM THINGS UP

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

  57. None
  58. THANKS!