Just when you thought you couldn't refactor anymore…

0722f1ff8d0a69bce57ebdb93dafc395?s=47 Claudio B.
November 16, 2017

Just when you thought you couldn't refactor anymore…

Presented at RubyConf, November 2017
http://rubyconf.com/program#session-218

Ruby's philosophy is to provide more than one way to do the same thing. Faced with choice, we are left wondering which methods to use.

In this talk, we will travel together on a refactoring journey. We will start from code that is easy to write but hard to read, and gradually advance to a level where the 4 C's of good code are satisfied: Correctness, Completeness, Clearness, and Compactness.

On the way, we will learn new Ruby 2.4 methods (String#match?, MatchData#named_captures) and review old methods (Enumerable#find, Regexp#===) that are more powerful than they seem at a first glance.

0722f1ff8d0a69bce57ebdb93dafc395?s=128

Claudio B.

November 16, 2017
Tweet

Transcript

  1. 4.

    The problem parse 'youtube.com/watch?v=Cx6aGMC6MjU' => {type: :video, id: 'Cx6aGMC6MjU'} parse

    'not-a-youtube-url' => {type: :unknown} parse 'youtube.com/confreaks' => {type: :channel, name: 'confreaks'} Correct Complete Clear Compact Correct Complete Clear Compact & & &
  2. 6.

    String#start_with?([prefixes]+) → true or false # Introduced in Ruby 1.8.7

    # Returns true if str starts with one of the prefixes given. 'hello'.start_with? 'hell' # => true # Returns true if one of the prefixes matches. 'hello'.start_with? 'heaven', 'hell' # => true 'hello'.start_with? 'heaven', 'paradise' # => false
  3. 7.

    parse 'youtube.com/watch?v=Cx6aGMC6MjU' => {type: :video, id: 'Cx6aGMC6MjU'} parse 'not-a-youtube-url' =>

    {type: :unknown} parse 'youtube.com/confreaks' => {type: :channel, name: 'confreaks'} Step 1: parsing the type
  4. 8.

    Using String#start_with? if text.start_with? 'youtube.com/watch?v=' {type: :video} elsif text.start_with? 'youtube.com/'

    {type: :channel} else {type: :unknown} end Correct Complete Clear Compact Correct Complete Clear Compact & & &
  5. 9.

    The problem parse 'youtube.com/watch?v=(11 letters, digits, _ or -)' =>

    {type: :video, id: …} parse '(anything else)' => {type: :unknown} parse 'youtube.com/(at least 1 letter, digit, _ or -)' => {type: :channel, name: …}
  6. 11.

    String#match?(pattern) → true or false # Introduced in Ruby 2.4

    # Converts pattern to a Regexp (if it isn’t already one), then returns true or false indicating whether the regexp is matched without updating $~ and other related variables. 'Ruby'.match? %r{R...} # => true 'Ruby'.match? %r{R..} # => false 'Ruby'.match? %r{P...} # => false
  7. 12.

    Using String#match? if text.match? %r{youtube\.com/watch\?v=[\w-]{11}} {type: :video} elsif text.match? %r{youtube\.com/[\w-]+}

    {type: :channel} else {type: :unknown} end Correct Complete Clear Compact Correct Complete Clear Compact & & &
  8. 14.

    rxp === str → true or false # Following a

    regular expression literal with the === operator allows you to compare against a String. # Case Equality — Used in case statements. %r{R...} === 'Ruby' # => true case 'Ruby' when %r{P...} then 'Starts with P' when %r{R...} then 'Starts with R' end # => 'Starts with R'
  9. 15.

    Using String#match? if text.match? %r{youtube\.com/watch\?v=[\w-]{11}} {type: :video} elsif text.match? %r{youtube\.com/[\w-]+}

    {type: :channel} else {type: :unknown} end Correct Complete Clear Compact Correct Complete Clear Compact & & &
  10. 16.

    Using Regexp#=== and case statement case text when %r{youtube\.com/watch\?v=[\w-]{11}} {type:

    :video} when %r{youtube\.com/[\w-]+} {type: :channel} else {type: :unknown} end Correct Complete Clear Compact Correct Complete Clear Compact & & &
  11. 17.

    Step 2: parsing everything else parse 'youtube.com/watch?v=Cx6aGMC6MjU' => {type: :video,

    id: 'Cx6aGMC6MjU'} parse 'not-a-youtube-url' => {type: :unknown} parse 'youtube.com/confreaks' => {type: :channel, name: 'confreaks'}
  12. 18.

    case text when %r{youtube\.com/watch\?v=[\w-]{11}} {type: :video} when %r{youtube\.com/[\w-]+} {type: :channel}

    else {type: :unknown} end Using Regexp#=== and case statement How can we capture and name these matches?
  13. 20.

    Regexp captures # Introduced in Ruby 1.9 # Parentheses can

    be used for capturing. Capture groups can be referred to by name when defined with the (?<name>) or (?'name') constructs. %r{\$(?<dollars>\d+)\.(?<cents>\d+)}.match '$3.67' # => #<MatchData "$3.67" dollars:"3" cents:"67"> $~['dollars'] # => "3" $~['cents'] # => "67"
  14. 21.

    case text when %r{youtube\.com/watch\?v=(?<id>[\w-]{11})} {type: :video, id: $~['id']} when %r{youtube\.com/(?<name>[\w-]+)}

    {type: :channel, name: $~['name']} else {type: :unknown} end Using Regexp captures Correct Complete Clear Compact Correct Complete Clear Compact & & &
  15. 23.

    MatchData#named_captures → hash # Introduced in 2.4 # Returns a

    Hash using named capture. A key of the hash is a name of the named captures. A value of the hash is a string of last successful capture of corresponding group. %r{\$(?<dollars>\d+)\.(?<cents>\d+)}.match '$3.67' # => #<MatchData "$3.67" dollars:"3" cents:"67"> $~.named_captures # => {"dollars" => "3", "cents"=> "67"}
  16. 25.

    case text # e.g. "youtube.com/confreaks" when %r{youtube\.com/(?<name>[\w-]+)} $~.named_captures # =>

    {name: 'confreaks'} $~.named_captures.merge type: :channel # => {id: 'confreaks', type: :channel} Using MatchData#named_captures
  17. 26.

    case text when %r{youtube\.com/watch\?v=(?<id>[\w-]{11})} $~.named_captures.merge type: :video when %r{youtube\.com/(?<name>[\w-]+)} $~.named_captures.merge

    type: :channel else {type: :unknown} end Using MatchData#named_captures Correct Complete Clear Compact Correct Complete Clear Compact & & &
  18. 28.

    Enumerable#find(ifnone = nil) {|obj| block} → obj or nil #

    Passes each entry in enum to block. Returns the first for which block is not false. If no object matches, calls ifnone and returns its result when it is specified, or nil otherwise. (18..99).find {|i| i % 17 == 0} # => 34 (18..29).find {|i| i % 17 == 0} # => nil (18..29).find(-> {0}) {|i| i % 17 == 0} # => 0
  19. 29.

    PATTERNS = { %r{youtube\.com/watch\?v=(?<id>[\w-]{11})} => :video, %r{youtube\.com/(?<name>[\w-]+)} => :channel, }

    PATTERNS.find do |regex, type| if text.match(regex) …return the Regexp captures and the matched type… end end Using Enumerable#find
  20. 31.

    # Introduced before Ruby 1.8.7 # Use break to leave

    a block early. break accepts a value that supplies the result of the expression it is "breaking" out of: (18..99).find {|i| i % 17 == 0} # => 34 (18..99).find {|i| break("Found #{i}!") if i % 17 == 0} # => "Found 34!" break statement
  21. 32.

    PATTERNS = { %r{youtube\.com/watch\?v=(?<id>[\w-]{11})} => :video, %r{youtube\.com/(?<name>[\w-]+)} => :channel, }

    PATTERNS.find do |regex, type| if text.match(regex) …return the Regexp captures and the matched type… end end Using Enumerable#find and break
  22. 33.

    PATTERNS = { %r{youtube\.com/watch\?v=(?<id>[\w-]{11})} => :video, %r{youtube\.com/(?<name>[\w-]+)} => :channel, }

    PATTERNS.find do |regex, type| if text.match(regex) break $~.named_captures.merge(type: type) end end Using Enumerable#find and break
  23. 34.

    PATTERNS = { %r{youtube\.com/watch\?v=(?<id>[\w-]{11})} => :video, %r{youtube\.com/(?<name>[\w-]+)} => :channel, }

    PATTERNS.find(-> { {type: :unknown} }) do |regex, type| if text.match(regex) break $~.named_captures.merge(type: type) end end Using Enumerable#find and break
  24. 35.

    PATTERNS = { %r{youtube\.com/watch\?v=(?<id>[\w-]{11})} => :video, %r{youtube\.com/(?<name>[\w-]+)} => :channel, }

    PATTERNS.find(-> { {type: :unknown} }) do |regex, type| break $~.named_captures.merge(type: type) if text.match(regex) end Using Enumerable#find and break Correct Complete Clear Compact Correct Complete Clear Compact & & &
  25. 37.

    PATTERNS = { %r{youtube\.com/watch\?v=(?<id>[\w-]{11})} => :video, %r{youtube\.com/(?<name>[\w-]+)} => :channel, }

    PATTERNS.find(-> { {type: :unknown} }) do |regex, type| break $~.named_captures.merge(type: type) if text.match(regex) end Using Enumerable#find and break
  26. 38.

    PATTERNS = { %r{youtube\.com/watch\?v=(?<id>[\w-]{11})} => :video, %r{youtu\.be/(?<id>[\w-]{11})} => :video, %r{youtube\.com/channel/(?<id>UC[\w-]{22})}

    => :channel, %r{youtube\.com/(?<name>[\w-]+)} => :channel, %r{youtube\.com/playlist/\?list=(?<id>UC[\w-]+)} => :playlist, } PATTERNS.find(-> { {type: :unknown} }) do |regex, type| break $~.named_captures.merge(type: type) if text.match(regex) end Using Enumerable#find and break
  27. 39.

    VIDEO_PATTERNS = [%r{youtube\.com/watch\?v=(?<id>[\w-]{11})}, %r{youtu\.be/(?<id>[\w-]{11})}] CHANNEL_PATTERNS = [%r{youtube\.com/channel/(?<id>UC[\w-]{22})}, %r{youtube\.com/(?<name>[\w-]+)}] PLAYLIST_PATTERNS =

    [%r{youtube\.com/playlist/\?list=(?<id>UC[\w-]+)}] patterns = …first iterate through video patterns, then channel patterns… patterns.find(-> { {type: :unknown} }) do |regex, type| break $~.named_captures.merge(type: type) if text.match(regex) end Using Enumerable#find and break
  28. 41.

    VIDEO_PATTERNS = [%r{youtube\.com/watch\?v=(?<id>[\w-]{11})}, %r{youtu\.be/(?<id>[\w-]{11})}] CHANNEL_PATTERNS = [%r{youtube\.com/channel/(?<id>UC[\w-]{22})}, %r{youtube\.com/(?<name>[\w-]+)}] PLAYLIST_PATTERNS =

    [%r{youtube\.com/playlist/?\?list=(?<id>UC[\w-]+)}] patterns = …first iterate through video patterns, then channel patterns… patterns.find(-> { {type: :unknown} }) do |regex, type| break $~.named_captures.merge(type: type) if text.match(regex) end Using Enumerable#find and break
  29. 42.

    VIDEO_PATTERNS = [%r{youtube\.com/watch\?v=(?<id>[\w-]{11})}, %r{youtu\.be/(?<id>[\w-]{11})}] CHANNEL_PATTERNS = [%r{youtube\.com/channel/(?<id>UC[\w-]{22})}, %r{youtube\.com/(?<name>[\w-]+)}] PLAYLIST_PATTERNS =

    [%r{youtube\.com/playlist/\?list=(?<id>UC[\w-]+)}] patterns = Enumerator.new do |patterns| VIDEO_PATTERNS.each {|regex| patterns << [regex, :video]} PLAYLIST_PATTERNS.each {|regex| patterns << [regex, :playlist]} CHANNEL_PATTERNS.each {|regex| patterns << [regex, :channel]} end patterns.find(-> { {type: :unknown} }) do |regex, type| break $~.named_captures.merge(type: type) if text.match(regex) end Using Enumerator.new
  30. 43.

    VIDEO_PATTERNS = [%r{youtube\.com/watch\?v=(?<id>[\w-]{11})}, %r{youtu\.be/(?<id>[\w-]{11})}] CHANNEL_PATTERNS = [%r{youtube\.com/channel/(?<id>UC[\w-]{22})}, %r{youtube\.com/(?<name>[\w-]+)}] PLAYLIST_PATTERNS =

    [%r{youtube\.com/playlist/\?list=(?<id>UC[\w-]+)}] patterns = Enumerator.new do |patterns| VIDEO_PATTERNS.each {|regex| patterns << [regex, :video]} PLAYLIST_PATTERNS.each {|regex| patterns << [regex, :playlist]} CHANNEL_PATTERNS.each {|regex| patterns << [regex, :channel]} end patterns.find(-> { {type: :unknown} }) do |regex, type| break $~.named_captures.merge(type: type) if text.match(regex) end The final result Correct Complete Clear Compact Correct Complete Clear Compact & & &
  31. 44.

    The final result VIDEO_PATTERNS = [%r{youtube\.com/watch\?v=(?<id>[\w-]{11})}, %r{youtu\.be/(?<id>[\w-]{11})}] CHANNEL_PATTERNS = [%r{youtube\.com/channel/(?<id>UC[\w-]{22})},

    %r{youtube\.com/(?<name>[\w-]+)}] PLAYLIST_PATTERNS = [%r{youtube\.com/playlist/\?list=(?<id>UC[\w-]+)}] patterns = Enumerator.new do |patterns| VIDEO_PATTERNS.each {|regex| patterns << [regex, :video]} PLAYLIST_PATTERNS.each {|regex| patterns << [regex, :playlist]} CHANNEL_PATTERNS.each {|regex| patterns << [regex, :channel]} end patterns.find(-> { {type: :unknown} }) do |regex, type| break $~.named_captures.merge(type: type) if text.match %r{\A(?:https?://)?(?:www\.)?#{regex}\Z} end Correct Complete Clear Compact Correct Complete Clear Compact & & &
  32. 45.

    VIDEO_PATTERNS = [%r{youtube\.com/watch\?v=(?<id>[\w-]{11})}, %r{youtu\.be/(?<id>[\w-]{11})}] CHANNEL_PATTERNS = [%r{youtube\.com/channel/(?<id>UC[\w-]{22})}, %r{youtube\.com/(?<name>[\w-]+)}] PLAYLIST_PATTERNS =

    [%r{youtube\.com/playlist/\?list=(?<id>UC[\w-]+)}] patterns = Enumerator.new do |patterns| VIDEO_PATTERNS.each {|regex| patterns << [regex, :video]} PLAYLIST_PATTERNS.each {|regex| patterns << [regex, :playlist]} CHANNEL_PATTERNS.each {|regex| patterns << [regex, :channel]} end patterns.find(-> { {type: :unknown} }) do |regex, type| break $~.named_captures.merge(type: type) if text.match %r{\A(?:https?://)?(?:www\.)?#{regex}\Z} end %r Regexp captures \w quantifiers Enumerator.new break Enumerable#find \A…\Z Optional captures $~ named_captures String#match << The final result