Skip to content

Instantly share code, notes, and snippets.

@theotherzach
Last active August 29, 2015 13:57
Show Gist options
  • Save theotherzach/9608465 to your computer and use it in GitHub Desktop.
Save theotherzach/9608465 to your computer and use it in GitHub Desktop.

Bottles Challenge 2

Hand Cranked Human Pattern Matching

This is not a challenge so much as a new (to me) way of looking at refactoring. As always, complete in your language of choice. First, some background:

The ruby example below maximizes simplicity at the expense of flexibility.

class Bottles
  def sing
    verses(99, 0)
  end

  def verses(upper_bound, lower_bound)
    upper_bound.downto(lower_bound).map {|n| verse(n) + "\n"}.join
  end

  def verse(number)
    case number
    when 0
      "No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n"
    when 1
      "1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"
    when 2
      "2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n"
    else
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} bottles of beer on the wall.\n"
    end
  end
end

We have a change request, though. When there are 6 bottles of beer left, we will need to display "1 six pack" instead of "6 bottles" in the song. The challenge is not to implement this change yet. We're going to Kent Beck this shit up.

for each desired change, make the change easy (warning: this may be hard), then make the easy change

— Kent Beck (@KentBeck) September 25, 2012
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script> https://twitter.com/KentBeck/status/250733358307500032

Ultimately we want to make the Bottles class open closed to the type of change that would allow us to arbitrarily say 1 six pack instead of 6 bottles. (I had to ask what open closed meant in this context in class.) Open closed here means open to the change but closed to modification. That's going to be too much for a single refactor in the style I'm about to describe so please allow me to enumerate what I would like for the next submission.

###Challenge Goal

  • No hard coded strings left in the verse method.

###Refactor Under Green Restrictions

  • Tests may never go red.
  • Tests must be run at every file save.
  • Try to save every time you make a change. Only change 2+ "things" without saving if you're stuck.
  • If your tests go red, hit undo until they're back to green.

###The Refactor Under Green Process

  • Look for similar structures.
  • Look for the smallest possible change that can be made to make them look more similar.
  • Change your system so making that change is easy. (Almost always additive. Tests can't go red, right?)
  • Make the easy change.
  • Delete dead code.
@theotherzach
Copy link
Author

Refactor Example

File change and tests run between each code block.

def verse(number)
    case number
    when 0
      "No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n"
    when 1
      "1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"
    when 2
      "2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n"
    else
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} bottles of beer on the wall.\n"
    end
  end

when 2 and else seem to be almost identical already, let's tackle that first. Using the number variable instead of hard coded 2 seems to be the lowest hanging fruit.

def verse(number)
    case number
    when 0
      "No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n"
    when 1
      "1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"
    when 2
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} bottle of beer on the wall.\n"
    else
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} bottles of beer on the wall.\n"
    end
  end

The pluralization of bottles is the next issue.

def verse(number)
    case number
    when 0
      "No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n"
    when 1
      "1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"
    when 2
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} bottle of beer on the wall.\n"
    else
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} bottles of beer on the wall.\n"
    end
  end

  def container(number)
  end

Lets start calling the container method to make sure it's for reals.

def verse(number)
    case number
    when 0
      "No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n"
    when 1
      "1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"
    when 2
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} bottle of beer on the wall.\n"
    else
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} #{container(number-1)} of beer on the wall.\n"
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} bottles of beer on the wall.\n"
    end
  end

  def container(number)
  end

Maybe I should install nodemon to automatically run my tests...

def verse(number)
    case number
    when 0
      "No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n"
    when 1
      "1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"
    when 2
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} bottle of beer on the wall.\n"
    else
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} #{container(number-1)} of beer on the wall.\n"
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} bottles of beer on the wall.\n"
    end
  end

  def container(number)
     case number
     when 1
     else
     end
  end

Time for the actual guts.

def verse(number)
    case number
    when 0
      "No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n"
    when 1
      "1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"
    when 2
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} bottle of beer on the wall.\n"
    else
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} #{container(number-1)} of beer on the wall.\n"
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} bottles of beer on the wall.\n"
    end
  end

  def container(number)
     case number
     when 1
        "bottle"
     else
       "bottles"
     end
  end

Now we remove the old else line and make sure the tests still pass.

def verse(number)
    case number
    when 0
      "No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n"
    when 1
      "1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"
    when 2
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} bottle of beer on the wall.\n"
    else
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} #{container(number-1)} of beer on the wall.\n"
    end
  end

  def container(number)
     case number
     when 1
        "bottle"
     else
       "bottles"
     end
  end

Now I'll copy our new else up to when 2 (and not return it) because I don't feel like thinking. This will let me know if it blows up without me needing to worry about its return value yet.

def verse(number)
    case number
    when 0
      "No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n"
    when 1
      "1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"
    when 2
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} #{container(number-1)} of beer on the wall.\n"
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} bottle of beer on the wall.\n"
    else
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} #{container(number-1)} of beer on the wall.\n"
    end
  end

  def container(number)
     case number
     when 1
        "bottle"
     else
       "bottles"
     end
  end

I could delete the old when 2 return value to make sure our new line is returning the right thing. Instead I'll just delete the entire when 2 statement since we're making a change to be identical to else anyway.

def verse(number)
    case number
    when 0
      "No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n"
    when 1
      "1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"
    else
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} #{container(number-1)} of beer on the wall.\n"
    end
  end

  def container(number)
     case number
     when 1
        "bottle"
     else
       "bottles"
     end
  end

Now to try and make when 1 and else identical.

@theotherzach
Copy link
Author

Side note: keeping our code branches as identifiable as possible is a good thing to assist with the pattern matching. Postfixed if mixed in with ternary statements and if else can make these steps harder.

@DarthStrom
Copy link

Setting aside for a moment the argument that the change would only require 2 additional special cases... Here's my refactor.

class Bottles {
    def verse(n) {
        "${amount(n).capitalize()} ${container(n)} of beer on the wall, " +
        "${amount(n)} ${container(n)} of beer.\n" +
        "${action(n)}, ${amount(remaining(n))} " +
        "${container(remaining(n))} of beer on the wall.\n"
    }

    def verses(first, last) {
        def song = "" 
        (first..last).each { song += verse(it) + "\n" }
        song
    }

    def sing() {
        verses(99, 0)
    }

    private def container(n) {
        if (n == 1)
            return "bottle"
        "bottles"
    }

    private def pronoun(n) {
        if (n == 1)
            return "it"
        "one"
    }

    private def amount(n) {
        if (n == 0)
            return "no more"
        n.toString()
    }

    private def action(n) {
        if (n == 0)
            return "Go to the store and buy some more"
        "Take ${pronoun(n)} down and pass it around"
    }

    private def remaining(n) {
        ((n + 99) % 100)
    }
}

@DarthStrom
Copy link

So, if I understand open/close in this context. What we really want to do to close the Bottles class (which is now poorly named...) to modification is to pass in a container object to use, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment