Stay One Undo Away from Green
*NOTE: This code has been disabled to work with MDX. TODO: Update this to work with MDX*
Code
I recently had the good fortune of attending Practical Object-Oriented Design](http://www.sandimetz.com/courses/) with [Sandi Metz](https://twitter.com/sandimetz) and [Katrina Owen. It was fantastic. Three days with them did more to improve my object-oriented thinking than months of reading and experimentation. I can't recommended the course highly enough.
Sandi and Katrina provided an elegant refactoring guideline on the first day:
Stay one undo away from green.
It was a key take-away and it's as simple as it sounds. Make one small change at a time during refactoring. If the tests go red, hit undo and immediately get back to green. If you're like me, and easily venture a dozen or more changes away from a passing test suite, this approach is transformative.
To illustrate, they walked the class through their refined, systematic process for extracting a conditional into a method. Two examples of their approach are below. The first lays out the steps used when starting with an 'else' branch. The second starts with an explicit case. The number of steps may appear tedious but each change is tiny. The entire process goes very quickly. As we discovered in class, picking a good name for the method takes longer than typing the code.
**Extracting a conditional starting with the 'else' branch**
Code
0
1
2
3
4
5
6
7
8
9
10
11
12
Step 0 - Initial Code
class Bottles
def verse_for(number)
if number == 1
"#{number} bottle of beer on the wall..."
else
"#{number} bottles of beer on the wall..."
end
end
end
Step 1 - Identify the things that are most alike.
class Bottles
def verse_for(number)
if number == 1
"#{number} bottle of beer on the wall..." else
"#{number} bottles of beer on the wall..." end
end
end
Step 2 - Identify the smallest difference in the things that are most alike.
class Bottles
def verse_for(number)
if number == 1
"#{number}
bottle of beer on the wall..." else
"#{number}
bottles of beer on the wall..." end
end
end
Step 3 - Make a method to return the value needed by the 'else' case but don't actually call it. Just make sure it parses.
class Bottles
def verse_for(number)
if number == 1
"#{number} bottle of beer on the wall..."
else
"#{number} bottles of beer on the wall..."
end
end
def container "bottles"
end
end
Step 4 - Execute the method without actually using the value.
class Bottles
def verse_for(number)
container if number == 1
"#{number} bottle of beer on the wall..."
else
"#{number} bottles of beer on the wall..."
end
end
def container
"bottles"
end
end
Step 5 - Used the new method in the 'else' case.
class Bottles
def verse_for(number)
#
container if number == 1
"#{number} bottle of beer on the wall..."
else
"#{number}
#{container} of beer on the wall..." # Replaced 'bottles' end
end
def container
"bottles"
end
end
Step 6 - Shim on an argument and run the tests to make sure it still passes.
class Bottles
def verse_for(number)
if number == 1
"#{number} bottle of beer on the wall..."
else
"#{number} #{container} of beer on the wall..."
end
end
def container(bottle_count
=:FIXME ) "bottles"
end
end
Step 7 - Send an argument to the method even though it's not used.
class Bottles
def verse_for(number)
if number == 1
"#{number} bottle of beer on the wall..."
else
"#{number} #{container
(number) } of beer on the wall..." end
end
def container(bottle_count=:FIXME)
"bottles"
end
end
Step 8 - Remove the default shim for the argument.
class Bottles
def verse_for(number)
if number == 1
"#{number} bottle of beer on the wall..."
else
"#{number} #{container(number)} of beer on the wall..."
end
end
def container(bottle_count) # Removed
=:FIXME "bottles"
end
end
Step 9 - Update the method to have the new functionality without actually calling it in the new location.
class Bottles
def verse_for(number)
if number == 1
"#{number} bottle of beer on the wall..."
else
"#{number} #{container(number)} of beer on the wall..."
end
end
def container(bottle_count)
if bottle_count == 1 "bottle"
else
"bottles"
end end
end
Step 10 - Call the method in the new location.
class Bottles
def verse_for(number)
if number == 1
"#{number}
#{container(number)} of beer on the wall..." # Replaced 'bottle' else
"#{number} #{container(number)} of beer on the wall..."
end
end
def container(bottle_count)
if bottle_count == 1
"bottle"
else
"bottles"
end
end
end
Step 11 - Comment out unused cases.
class Bottles
def verse_for(number)
# if number == 1
# "#{number} #{container(number)} of beer on the wall..."
# else "#{number} #{container(number)} of beer on the wall..."
# end
end
def container(bottle_count)
if bottle_count == 1
"bottle"
else
"bottles"
end
end
end
Step 12 - Remove comments and you're done.
class Bottles
def verse_for(number)
"#{number} #{container(number)} of beer on the wall..."
end
def container(bottle_count)
if bottle_count == 1
"bottle"
else
"bottles"
end
end
end
**Extracting a conditional starting with a specific case**
Code
0
1
2
3
4
5
6
7
8
9
Step 0 - Initial Code
class Bottles
def verse_for(number)
if number == 1
"#{number} bottle of beer on the wall..."
else
"#{number} bottles of beer on the wall..."
end
end
end
Step 1 - Identify the things that are most alike.
class Bottles
def verse_for(number)
if number == 1
"#{number} bottle of beer on the wall..." else
"#{number} bottles of beer on the wall..." end
end
end
Step 2 - Identify the smallest difference in the things that are most alike.
class Bottles
def verse_for(number)
if number == 1
"#{number}
bottle of beer on the wall..." else
"#{number}
bottles of beer on the wall..." end
end
end
Step 3 - Make a method that takes an unused argument and returns the value needed but don't actually call it. Just make sure it parses.
class Bottles
def verse_for(number)
if number == 1
"#{number} bottle of beer on the wall..."
else
"#{number} bottles of beer on the wall..."
end
end
def container(bottle_count) "bottle"
end
end
Step 4 - Execute the method without actually using the value.
class Bottles
def verse_for(number)
container(number) if number == 1
"#{number} bottle of beer on the wall..."
else
"#{number} bottles of beer on the wall..."
end
end
def container(bottle_count)
"bottle"
end
end
Step 5 - Use the new method
class Bottles
def verse_for(number)
#
container(number) if number == 1
"#{number}
#{container(number)} of beer on the wall..." # Replaced 'bottle' else
"#{number} bottles of beer on the wall..."
end
end
def container(bottle_count)
"bottle"
end
end
Step 6 - Update the method to have the new functionality without actually calling it in the new location.
class Bottles
def verse_for(number)
if number == 1
"#{number} #{container(number)} of beer on the wall..."
else
"#{number} bottles of beer on the wall..."
end
end
def container(bottle_count)
if bottle_count == 1 "bottle"
else "bottles"
end
end
end
Step 7 - Call the method in the 'else' case.
class Bottles
def verse_for(number)
if number == 1
"#{number} #{container(number)} of beer on the wall..."
else
"#{number}
#{container(number)} of beer on the wall..." # Replaced 'bottles' end
end
def container(bottle_count)
if bottle_count == 1
"bottle"
else
"bottles"
end
end
end
Step 8 - Comment out unused case.
class Bottles
def verse_for(number)
# if number == 1 "#{number} #{container(number)} of beer on the wall..."
# else
# "#{number} #{container(number)} of beer on the wall..."
# end end
def container(bottle_count)
if bottle_count == 1
"bottle"
else
"bottles"
end
end
end
Step 9 - Remove comments and you're done.
class Bottles
def verse_for(number)
"#{number} #{container(number)} of beer on the wall..."
end
def container(bottle_count)
if bottle_count == 1
"bottle"
else
"bottles"
end
end
end
Don't let the terse nature of the examples fool you. They are simplified versions of a 99 Bottles exercise used throughout the class. The full problem has a lot more going on and the actual extraction provides benefits that aren't evident in this demonstration. If you want to learn more, definitely attend the class. Sandi and Katrina also have a '99 Bottles Of OOP' book coming out soon. Based on the class, I expect it'll be great. Sign up for info about it if you're interested.