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

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