Don’t Hang Me Out To DRY

Don’t Hang Me Out To DRY

Close your eyes and imagine the perfect codebase to work on. I bet you’ll say it has complete test coverage. It’s fully-optimized, both in terms of performance and architectural design. And, of course, it contains only DRY code. Surely we can all agree that this is an aspirational situation. But...do we really want that?

Don’t get me wrong; these qualities are all beneficial. However, if we also think we should value everything in moderation, when should we push back on these ideals? What problems can they introduce? Let’s talk about the exceptions to some of the “rules” we all hold dear.

B2f82edebc6e840ed97c8606700d123a?s=128

Kevin Murphy

October 22, 2019
Tweet

Transcript

  1. Don’t Hang Me Out to DRY Kevin Murphy @kevin_j_m

  2. Don’t Hang Me Out to DRY Kevin Murphy @kevin_j_m

  3. @kevin_j_m vory Tower
 nnovation TechnologY

  4. @kevin_j_m T T

  5. @kevin_j_m T

  6. @kevin_j_m T our code has

  7. @kevin_j_m T Make work

  8. @kevin_j_m T Make work ☂

  9. @kevin_j_m T Make T work Make right

  10. @kevin_j_m T Make right ❌

  11. @kevin_j_m T Make T T work Make Make right fast

  12. @kevin_j_m T Make fast

  13. @kevin_j_m T Make T T work Make Make right fast

  14. @kevin_j_m

  15. None
  16. @kevin_j_m Kevin Murphy

  17. @kevin_j_m T

  18. @kevin_j_m Should see 4 testimonials today, but only 3 are

    seen.
  19. @kevin_j_m def number_testimonials if mercury_retrograde? 4 elsif full_moon? 3 elsif

    tuesday? 2 else coin_flip end end
  20. @kevin_j_m def number_testimonials if mercury_retrograde? 4 elsif full_moon? 3 elsif

    tuesday? 2 else coin_flip end end
  21. @kevin_j_m Farmer’s Almanac Mercury in Retrograde OCT 31 NOV 20

  22. @kevin_j_m Code Coverage

  23. @kevin_j_m

  24. @kevin_j_m it "shows 4 testimonials if mercury is in retrograde"

    do end
  25. @kevin_j_m it "shows 4 testimonials if mercury is in retrograde"

    do allow(Mercury).to receive(:in_retrograde?).and_return(true) end
  26. @kevin_j_m it "shows 4 testimonials if mercury is in retrograde"

    do allow(Mercury).to receive(:in_retrograde?).and_return(true) display = TestimonialDisplay.new end
  27. @kevin_j_m it "shows 4 testimonials if mercury is in retrograde"

    do allow(Mercury).to receive(:in_retrograde?).and_return(true) display = TestimonialDisplay.new expect(display.number_testimonials).to eq 4 end
  28. @kevin_j_m

  29. @kevin_j_m class Mercury def self.in_retrograde?(date) end end

  30. @kevin_j_m class Mercury def self.in_retrograde?(date) false end end

  31. @kevin_j_m

  32. @kevin_j_m Coverage is insufficient

  33. @kevin_j_m TDD

  34. @kevin_j_m Pair Programming

  35. @kevin_j_m Mutation Testing

  36. @kevin_j_m Code Review

  37. @kevin_j_m QA

  38. @kevin_j_m '

  39. @kevin_j_m Coverage is insufficient

  40. @kevin_j_m Coverage as a goal

  41. @kevin_j_m Randomized with seed 46917 .F.. Failures: 1) TestimonialDisplay#number_testimonials will

    show 0 or 1 testimonial if no other conditions are met Failure/Error: expect(results.uniq).to match_array [0, 1] expected collection contained: [0, 1] actual collection contained: [1] the missing elements were: [0]
  42. @kevin_j_m def number_testimonials if mercury_retrograde? 4 elsif full_moon? 3 elsif

    tuesday? 2 else coin_flip end end
  43. @kevin_j_m def coin_flip rand(2) end

  44. @kevin_j_m it "will show 0 or 1 testimonial" do end

  45. @kevin_j_m it "will show 0 or 1 testimonial" do waning_monday

    = Date.new(2019, 9, 16) end
  46. @kevin_j_m it "will show 0 or 1 testimonial" do waning_monday

    = Date.new(2019, 9, 16) results = [] end
  47. @kevin_j_m it "will show 0 or 1 testimonial" do waning_monday

    = Date.new(2019, 9, 16) results = [] display = TestimonialDisplay.new end
  48. @kevin_j_m it "will show 0 or 1 testimonial" do waning_monday

    = Date.new(2019, 9, 16) results = [] display = TestimonialDisplay.new travel_to waning_monday do end end
  49. @kevin_j_m it "will show 0 or 1 testimonial" do waning_monday

    = Date.new(2019, 9, 16) results = [] display = TestimonialDisplay.new travel_to waning_monday do 2.times do end end end
  50. @kevin_j_m it "will show 0 or 1 testimonial" do waning_monday

    = Date.new(2019, 9, 16) results = [] display = TestimonialDisplay.new travel_to waning_monday do 2.times do results << display.number_testimonials end end end
  51. @kevin_j_m it "will show 0 or 1 testimonial" do waning_monday

    = Date.new(2019, 9, 16) results = [] display = TestimonialDisplay.new travel_to waning_monday do 2.times do results << display.number_testimonials end end expect(results.uniq).to match_array [0, 1] end
  52. @kevin_j_m it "will show 0 or 1 testimonial" do waning_monday

    = Date.new(2019, 9, 16) results = [] display = TestimonialDisplay.new travel_to waning_monday do 2.times do results << display.number_testimonials end end expect(results.uniq).to match_array [0, 1] end
  53. @kevin_j_m it "will show 0 or 1 testimonial" do waning_monday

    = Date.new(2019, 9, 16) results = [] display = TestimonialDisplay.new travel_to waning_monday do 2.times do results << display.number_testimonials end end expect(results.uniq).to match_array [0, 1] end
  54. @kevin_j_m it "will show 0 or 1 testimonial" do waning_monday

    = Date.new(2019, 9, 16) results = [] display = TestimonialDisplay.new travel_to waning_monday do 200.times do results << display.number_testimonials end end expect(results.uniq).to match_array [0, 1] end
  55. @kevin_j_m it "will show 0 or 1 testimonial" do waning_monday

    = Date.new(2019, 9, 16) display = TestimonialDisplay.new travel_to waning_monday do expect(results.uniq).to be_in [0, 1] end end
  56. @kevin_j_m Vanity metric?

  57. @kevin_j_m

  58. @kevin_j_m Coverage is a signal

  59. @kevin_j_m

  60. @kevin_j_m

  61. @kevin_j_m

  62. @kevin_j_m

  63. @kevin_j_m

  64. @kevin_j_m Coverage Consideration Total Cost of Ownership

  65. @kevin_j_m T

  66. @kevin_j_m +

  67. @kevin_j_m ☑ Dark Dungeon API Access Keys should follow same

    convention
  68. @kevin_j_m it "does not provide a company key" do end

  69. @kevin_j_m it "does not provide a company key" do generator

    = AccessKeyGenerator.new end
  70. @kevin_j_m it "does not provide a company key" do generator

    = AccessKeyGenerator.new key = generator.access_key(accessor: :user, user_id: 0) end
  71. @kevin_j_m it "does not provide a company key" do generator

    = AccessKeyGenerator.new key = generator.access_key(accessor: :user, user_id: 0) expect(key.match?(COMPANY_REGEX)).not_to eq true end
  72. @kevin_j_m it "appends the user id if the user's id

    is odd" do generator = AccessKeyGenerator.new key = generator.access_key(accessor_type: :user, user_id: 1) expect(key).to end_with("-1") end it "does not provide a company key" do generator = AccessKeyGenerator.new key = generator.access_key(accessor: :user, user_id: 0) expect(key.match?(COMPANY_REGEX)).not_to eq true end it "raises an exception if it doesn't understand the accessor type" do generator = AccessKeyGenerator.new expect { generator.access_key(accessor_type: :foo) } .to raise_error UnknownAccessorType end end end
  73. @kevin_j_m it "appends the user id if the user's id

    is odd" do generator = AccessKeyGenerator.new key = generator.access_key(accessor_type: :user, user_id: 1) expect(key).to end_with("-1") end it "does not provide a company key" do generator = AccessKeyGenerator.new key = generator.access_key(accessor: :user, user_id: 0) expect(key.match?(COMPANY_REGEX)).not_to eq true end it "raises an exception if it doesn't understand the accessor type" do generator = AccessKeyGenerator.new expect { generator.access_key(accessor_type: :foo) } .to raise_error UnknownAccessorType end end end
  74. @kevin_j_m RSpec.describe AccessKeyGenerator do UUID_REGEX = /[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}/ COMPANY_REGEX = /CO-[0-9a-fA-F]{8}/

    describe "#access_key" do it "provides a base-64 encoded key if the key is for an acquired company" do generator = AccessKeyGenerator.new key = generator.access_key(accessor_type: :company, acquired_company: true) expect(key).to eq Base64.strict_encode64(Base64.decode64(key)) end it "creates an ivory tower company access key if it's a company not acquired" do generator = AccessKeyGenerator.new key = generator.access_key(accessor_type: :company, acquired_company: false) expect(key.match?(COMPANY_REGEX)).to eq true end it "does not provide a full UUID if the key is for a company" do generator = AccessKeyGenerator.new key = generator.access_key(accessor_type: :company)
  75. @kevin_j_m RSpec.describe AccessKeyGenerator do UUID_REGEX = /[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}/ COMPANY_REGEX = /CO-[0-9a-fA-F]{8}/

    describe "#access_key" do it "provides a base-64 encoded key if the key is for an acquired company" do generator = AccessKeyGenerator.new key = generator.access_key(accessor_type: :company, acquired_company: true) expect(key).to eq Base64.strict_encode64(Base64.decode64(key)) end it "creates an ivory tower company access key if it's a company not acquired" do generator = AccessKeyGenerator.new key = generator.access_key(accessor_type: :company, acquired_company: false) expect(key.match?(COMPANY_REGEX)).to eq true end it "does not provide a full UUID if the key is for a company" do generator = AccessKeyGenerator.new key = generator.access_key(accessor_type: :company)
  76. @kevin_j_m DRY Code Don’t Repeat Yourself

  77. @kevin_j_m DAMP Code Descriptive And Meaningful Phrases

  78. @kevin_j_m it "does not provide a company key" do generator

    = AccessKeyGenerator.new key = generator.access_key(accessor: :user, user_id: 0) expect(key.match?(COMPANY_REGEX)).not_to eq true end
  79. @kevin_j_m it "does not provide a company key" do company_regex

    = /CO-[0-9a-fA-F]{8}/ generator = AccessKeyGenerator.new key = generator.access_key(accessor: :user, user_id: 0) expect(key.match?(COMPANY_REGEX)).not_to eq true end
  80. @kevin_j_m it "does not provide a company key" do company_regex

    = /CO-[0-9a-fA-F]{8}/ generator = AccessKeyGenerator.new key = generator.access_key(accessor: :user, user_id: 0) expect(key.match?(company_regex)).not_to eq true end
  81. @kevin_j_m T

  82. @kevin_j_m class AccessKeyGenerator def access_key(accessor:, user_id: nil) if accessor ==

    :company "CO-#{SecureRandom.hex(8)}" elsif accessor == :user uuid = SecureRandom.uuid user_id.odd? ? uuid << “-#{user_id}" : uuid end end end
  83. @kevin_j_m class AccessKeyGenerator def access_key(accessor:, acquired_co: nil, user_id: nil) if

    accessor == :company "CO-#{SecureRandom.hex(8)}" elsif accessor == :user uuid = SecureRandom.uuid user_id.odd? ? uuid << “-#{user_id}" : uuid end end end
  84. @kevin_j_m class AccessKeyGenerator def access_key(accessor:, acquired_co: nil, user_id: nil) if

    accessor == :company if acquired_co SecureRandom.base64 else “CO-#{SecureRandom.hex(8)}” end elsif accessor == :user uuid = SecureRandom.uuid user_id.odd? ? uuid << “-#{user_id}" : uuid end end end
  85. @kevin_j_m DRY Code?

  86. @kevin_j_m class User < ApplicationRecord def generate_access_key SecureRandom.uuid end end

  87. @kevin_j_m class Company < ApplicationRecord def generate_access_key SecureRandom.uuid end end

  88. @kevin_j_m

  89. @kevin_j_m class AccessKeyGenerator def access_key SecureRandom.uuid end end

  90. @kevin_j_m class AccessKeyGenerator def access_key(accessor:, user_id: nil) if accessor ==

    :company "CO-#{SecureRandom.hex(8)}" elsif accessor == :user uuid = SecureRandom.uuid user_id.odd? ? uuid << “-#{user_id}" : uuid end end end
  91. @kevin_j_m WET Code Write Everything Twice

  92. @kevin_j_m

  93. @kevin_j_m class User < ApplicationRecord def generate_access_key SecureRandom.uuid end end

  94. @kevin_j_m class User < ApplicationRecord def generate_access_key SecureRandom.uuid end end

    class Company < ApplicationRecord def generate_access_key SecureRandom.uuid end end
  95. @kevin_j_m class User < ApplicationRecord def generate_access_key if id.odd? "#{SecureRandom.uuid}-#{id}"

    else SecureRandom.uuid end end end
  96. @kevin_j_m class User < ApplicationRecord def generate_access_key if id.odd? "#{SecureRandom.uuid}-#{id}"

    else SecureRandom.uuid end end end class Company < ApplicationRecord def generate_access_key if acquisition? SecureRandom.base64 else "CO-#{SecureRandom.hex(8)}" end end end
  97. @kevin_j_m DRY Consideration Flexibility

  98. @kevin_j_m T

  99. @kevin_j_m Passing “15” to API provides wrong result

  100. @kevin_j_m Performant Code

  101. @kevin_j_m Rust

  102. @kevin_j_m gem 'helix-rails', '~> 0.5.0'

  103. @kevin_j_m def generate(bound: i32)

  104. @kevin_j_m def generate(bound: i32) -> Vec<String> { }

  105. @kevin_j_m def generate(bound: i32) -> Vec<String> { let mut results

    = Vec::new(); }
  106. @kevin_j_m def generate(bound: i32) -> Vec<String> { let mut results

    = Vec::new(); for x in 1..(bound+1) { } }
  107. @kevin_j_m def generate(bound: i32) -> Vec<String> { let mut results

    = Vec::new(); for x in 1..(bound+1) { results.push(match(x % 3, x % 5) { }) } }
  108. @kevin_j_m def generate(bound: i32) -> Vec<String> { let mut results

    = Vec::new(); for x in 1..(bound+1) { results.push(match(x % 3, x % 5) { (0, 0) => "FizzBuzz".to_string(), }) } }
  109. @kevin_j_m def generate(bound: i32) -> Vec<String> { let mut results

    = Vec::new(); for x in 1..(bound+1) { results.push(match(x % 3, x % 5) { (0, 0) => "FizzBuzz".to_string(), (0, _) => "Fizz".to_string(), }) } }
  110. @kevin_j_m def generate(bound: i32) -> Vec<String> { let mut results

    = Vec::new(); for x in 1..(bound+1) { results.push(match(x % 3, x % 5) { (0, 0) => "FizzBuzz".to_string(), (0, _) => "Fizz".to_string(), (_, 0) => "Buzz".to_string(), }) } }
  111. @kevin_j_m def generate(bound: i32) -> Vec<String> { let mut results

    = Vec::new(); for x in 1..(bound+1) { results.push(match(x % 3, x % 5) { (0, 0) => "FizzBuzz".to_string(), (0, _) => "Fizz".to_string(), (_, 0) => "Buzz".to_string(), (2, 4) => "Buzz".to_string(), }) } }
  112. @kevin_j_m def generate(bound: i32) -> Vec<String> { let mut results

    = Vec::new(); for x in 1..(bound+1) { results.push(match(x % 3, x % 5) { (0, 0) => "FizzBuzz".to_string(), (0, _) => "Fizz".to_string(), (_, 0) => "Buzz".to_string(), (2, 4) => "Buzz".to_string(), (_, _) => x.to_string(), }) } }
  113. @kevin_j_m def generate(bound: i32) -> Vec<String> { let mut results

    = Vec::new(); for x in 1..(bound+1) { results.push(match(x % 3, x % 5) { (0, 0) => "FizzBuzz".to_string(), (0, _) => "Fizz".to_string(), (_, 0) => "Buzz".to_string(), (2, 4) => "Buzz".to_string(), (_, _) => x.to_string(), }) } results }
  114. @kevin_j_m FBAAS

  115. @kevin_j_m FBAAS FizzBuzz As A Service

  116. @kevin_j_m def generate(bound: i32) -> Vec<String> { let mut results

    = Vec::new(); for x in 1..(bound+1) { results.push(match(x % 3, x % 5) { (0, 0) => "FizzBuzz".to_string(), (0, _) => "Fizz".to_string(), (_, 0) => "Buzz".to_string(), (2, 4) => "Buzz".to_string(), (_, _) => x.to_string(), }) } results }
  117. @kevin_j_m def generate(bound: i32) -> Vec<String> { let mut results

    = Vec::new(); for x in 1..(bound+1) { results.push(match(x % 3, x % 5) { (0, 0) => "FizzBuzz".to_string(), (0, _) => "Fizz".to_string(), (_, 0) => "Buzz".to_string(), (2, 4) => "Buzz".to_string(), (_, _) => x.to_string(), }) } results }
  118. @kevin_j_m def generate(bound: i32) -> Vec<String> { let mut results

    = Vec::new(); for x in 1..(bound+1) { results.push(match(x % 3, x % 5) { (0, 0) => "FizzBuzz".to_string(), (0, _) => "Fizz".to_string(), (_, 0) => "Buzz".to_string(), (_, _) => x.to_string(), }) } results }
  119. @kevin_j_m Performant Consideration Weigh observed behavior & maintenance

  120. @kevin_j_m T Make T T work Make Make right fast

  121. @kevin_j_m T Make work for confidence, not metrics

  122. @kevin_j_m T Make right knowing “right” is subjective

  123. @kevin_j_m T Make fast when you know it’s warranted

  124. @kevin_j_m https://www.thegnar.co/rubyconf https://github.com/kevin-j-m/ivory-tower T

  125. @kevin_j_m https://www.thegnar.co/rubyconf