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

Just when you thought you couldn't refactor anymore…

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.

Claudio B.

November 16, 2017
Tweet

More Decks by Claudio B.

Other Decks in Programming

Transcript

  1. Just when you thought you couldn’t refactor any more…
    speakerdeck.com/claudiob
    Slides available!

    View Slide

  2. String
    #start_with?
    MatchData
    #named_captures
    String
    #match?
    Enumerable
    #find
    Regexp
    #===
    break
    statement
    Regexp
    captures
    Enumerator
    .new

    View Slide

  3. What’s in a YouTube URL?

    View Slide

  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
    & & &

    View Slide

  5. String#start_with?

    View Slide

  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

    View Slide

  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

    View Slide

  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
    & & &

    View Slide

  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: …}

    View Slide

  10. String#match?

    View Slide

  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

    View Slide

  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
    & & &

    View Slide

  13. Regexp#===

    View Slide

  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'

    View Slide

  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
    & & &

    View Slide

  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
    & & &

    View Slide

  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'}

    View Slide

  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?

    View Slide

  19. Regexp captures

    View Slide

  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 (?) or (?'name')
    constructs.
    %r{\$(?\d+)\.(?\d+)}.match '$3.67'
    # => #
    $~['dollars'] # => "3"
    $~['cents'] # => "67"

    View Slide

  21. case text
    when %r{youtube\.com/watch\?v=(?[\w-]{11})}
    {type: :video, id: $~['id']}
    when %r{youtube\.com/(?[\w-]+)}
    {type: :channel, name: $~['name']}
    else
    {type: :unknown}
    end
    Using Regexp captures
    Correct Complete Clear Compact
    Correct Complete Clear Compact
    & & &

    View Slide

  22. MatchData#named_captures

    View Slide

  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{\$(?\d+)\.(?\d+)}.match '$3.67'
    # => #
    $~.named_captures # => {"dollars" => "3", "cents"=> "67"}

    View Slide

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

    View Slide

  25. case text
    # e.g. "youtube.com/confreaks"
    when %r{youtube\.com/(?[\w-]+)}
    $~.named_captures
    # => {name: 'confreaks'}
    $~.named_captures.merge type: :channel
    # => {id: 'confreaks', type: :channel}
    Using MatchData#named_captures

    View Slide

  26. case text
    when %r{youtube\.com/watch\?v=(?[\w-]{11})}
    $~.named_captures.merge type: :video
    when %r{youtube\.com/(?[\w-]+)}
    $~.named_captures.merge type: :channel
    else
    {type: :unknown}
    end
    Using MatchData#named_captures
    Correct Complete Clear Compact
    Correct Complete Clear Compact
    & & &

    View Slide

  27. Enumerable#find

    View Slide

  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

    View Slide

  29. PATTERNS = {
    %r{youtube\.com/watch\?v=(?[\w-]{11})} => :video,
    %r{youtube\.com/(?[\w-]+)} => :channel,
    }
    PATTERNS.find do |regex, type|
    if text.match(regex)
    …return the Regexp captures and the matched type…
    end
    end
    Using Enumerable#find

    View Slide

  30. break statement

    View Slide

  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

    View Slide

  32. PATTERNS = {
    %r{youtube\.com/watch\?v=(?[\w-]{11})} => :video,
    %r{youtube\.com/(?[\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

    View Slide

  33. PATTERNS = {
    %r{youtube\.com/watch\?v=(?[\w-]{11})} => :video,
    %r{youtube\.com/(?[\w-]+)} => :channel,
    }
    PATTERNS.find do |regex, type|
    if text.match(regex)
    break $~.named_captures.merge(type: type)
    end
    end
    Using Enumerable#find and break

    View Slide

  34. PATTERNS = {
    %r{youtube\.com/watch\?v=(?[\w-]{11})} => :video,
    %r{youtube\.com/(?[\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

    View Slide

  35. PATTERNS = {
    %r{youtube\.com/watch\?v=(?[\w-]{11})} => :video,
    %r{youtube\.com/(?[\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
    & & &

    View Slide

  36. What’s in a YouTube URL?

    View Slide

  37. PATTERNS = {
    %r{youtube\.com/watch\?v=(?[\w-]{11})} => :video,
    %r{youtube\.com/(?[\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

    View Slide

  38. PATTERNS = {
    %r{youtube\.com/watch\?v=(?[\w-]{11})} => :video,
    %r{youtu\.be/(?[\w-]{11})} => :video,
    %r{youtube\.com/channel/(?UC[\w-]{22})} => :channel,
    %r{youtube\.com/(?[\w-]+)} => :channel,
    %r{youtube\.com/playlist/\?list=(?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

    View Slide

  39. VIDEO_PATTERNS = [%r{youtube\.com/watch\?v=(?[\w-]{11})}, %r{youtu\.be/(?[\w-]{11})}]
    CHANNEL_PATTERNS = [%r{youtube\.com/channel/(?UC[\w-]{22})}, %r{youtube\.com/(?[\w-]+)}]
    PLAYLIST_PATTERNS = [%r{youtube\.com/playlist/\?list=(?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

    View Slide

  40. Enumerator.new

    View Slide

  41. VIDEO_PATTERNS = [%r{youtube\.com/watch\?v=(?[\w-]{11})}, %r{youtu\.be/(?[\w-]{11})}]
    CHANNEL_PATTERNS = [%r{youtube\.com/channel/(?UC[\w-]{22})}, %r{youtube\.com/(?[\w-]+)}]
    PLAYLIST_PATTERNS = [%r{youtube\.com/playlist/?\?list=(?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

    View Slide

  42. VIDEO_PATTERNS = [%r{youtube\.com/watch\?v=(?[\w-]{11})}, %r{youtu\.be/(?[\w-]{11})}]
    CHANNEL_PATTERNS = [%r{youtube\.com/channel/(?UC[\w-]{22})}, %r{youtube\.com/(?[\w-]+)}]
    PLAYLIST_PATTERNS = [%r{youtube\.com/playlist/\?list=(?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

    View Slide

  43. VIDEO_PATTERNS = [%r{youtube\.com/watch\?v=(?[\w-]{11})}, %r{youtu\.be/(?[\w-]{11})}]
    CHANNEL_PATTERNS = [%r{youtube\.com/channel/(?UC[\w-]{22})}, %r{youtube\.com/(?[\w-]+)}]
    PLAYLIST_PATTERNS = [%r{youtube\.com/playlist/\?list=(?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
    & & &

    View Slide

  44. The final result
    VIDEO_PATTERNS = [%r{youtube\.com/watch\?v=(?[\w-]{11})}, %r{youtu\.be/(?[\w-]{11})}]
    CHANNEL_PATTERNS = [%r{youtube\.com/channel/(?UC[\w-]{22})}, %r{youtube\.com/(?[\w-]+)}]
    PLAYLIST_PATTERNS = [%r{youtube\.com/playlist/\?list=(?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
    & & &

    View Slide

  45. VIDEO_PATTERNS = [%r{youtube\.com/watch\?v=(?[\w-]{11})}, %r{youtu\.be/(?[\w-]{11})}]
    CHANNEL_PATTERNS = [%r{youtube\.com/channel/(?UC[\w-]{22})}, %r{youtube\.com/(?[\w-]+)}]
    PLAYLIST_PATTERNS = [%r{youtube\.com/playlist/\?list=(?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

    View Slide

  46. Thanks!
    speakerdeck.com/claudiob

    View Slide