Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
80 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
# TDD Kata from http://osherove.com/tdd-kata-1/ | ||
module StringCalculator | ||
extend self | ||
|
||
DEFAULT_DELIMITER = "," | ||
def add(str) | ||
res = split_str(str) | ||
res = convert_items(res) | ||
res.inject(0, &:+) | ||
end | ||
|
||
def parse_delimiter(str) | ||
delimiter = DEFAULT_DELIMITER | ||
lines = str.split("\n") | ||
|
||
flags = lines.first | ||
if flags && flags[0..1] == "//" | ||
delimiter = lines.shift[2..-1] | ||
end | ||
return delimiter, lines | ||
end | ||
|
||
def split_str(str) | ||
delimiter, lines = parse_delimiter(str) | ||
lines.map {|line| line.split(delimiter)}.flatten | ||
end | ||
|
||
def convert_items(arr) | ||
arr.map(&:to_i) | ||
end | ||
end | ||
|
||
# Notes from after kata | ||
# * Made it to step 4 | ||
# * feels ugly.... | ||
# * the overall system doesn't feel natural, it feels forced | ||
# * procedural, not object oriented | ||
# * no tests for my helper methods. I considered them as | ||
# private when I did it, but question that now | ||
# * not obvious from the final result, but lots of churn here | ||
# * inconsistent refactoring of "magic values" | ||
# * | ||
|
||
require 'rubygems' | ||
require 'bacon' | ||
|
||
describe "String calculator" do | ||
it 'has the add method' do | ||
StringCalculator.method(:add).should.not.equal nil | ||
end | ||
|
||
it 'does not have 0 arity' do | ||
StringCalculator.method(:add).arity.should.equal 1 | ||
end | ||
|
||
it 'returns 0 for empty string' do | ||
StringCalculator.add("").should.equal 0 | ||
end | ||
|
||
it 'returns 1 for "1"' do | ||
StringCalculator.add("1").should.equal 1 | ||
end | ||
|
||
it 'adds 1 and 2 for "1,2"' do | ||
StringCalculator.add("1,2").should.equal 3 | ||
end | ||
|
||
it 'adds multiple numbers' do | ||
StringCalculator.add("1,2,3").should.equal 6 | ||
end | ||
|
||
it 'can handle newline delimiters' do | ||
StringCalculator.add("1\n2").should.equal 3 | ||
end | ||
|
||
it 'can support new delimiters' do | ||
StringCalculator.add("//;\n1;2").should.equal 3 | ||
end | ||
end | ||
|
67d0010
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In response to your procedural comment, it would be easy for you to break this down even further ! After ruminating on the functions, the code is yelling at me to have a Parser! It literally yelled at me! Then your StringCalculator would just be a string calculator. The benefit here would be a couple things
Then we could make the StringCalculator's internals all kinds of beautiful:
We could clean up this jot further too. Moving the #parse method to the parser then delegating would get rid of some redundant code for instance. This would make adding additional functions to the calculator trivial.
Great job throwing this out there for people to give feedback on. What do you think about what I think!?!
67d0010
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi Jim, I'm one of Tim's coworkers.
One of the ways I like to look at OO design is by trying to identify as many nouns in the domain as possible. Verbs are very procedural ("do this", "compute that") whereas nouns are a more natural fit for a programming paradigm built around objects. (Also note that a module with only module methods is de facto procedural code). For starters, we need to create a
StringCalculator
object to hold our data and the behavior we want to attach to it.Now that we've defined our domain, what kind of nouns should it have? Well, instead of
parse_delimiter
andsplit_str
and so on, what is the most basic noun in an adding machine? It's "numbers", the collection of numbers to be added, right? So why not build anumbers
method? Then your adding is simplynumbers.reduce :+
, which is very intention revealing.So what would that
numbers
method have to do? Well, it would have to parse things and split things and so on, but again if you use a vocabulary of nouns I think the design will be easier to suss out. For instance, why not have adelimiter
noun? You have a variable named delimiter, but it's hidden away inside an opaque data structure (the[delimiter, lines]
tuple). Here's a naive implementation. Good thing you have tests!What other nouns would be helpful? What about lines, the input split into lines and with any delimiter specification removed.
And perhaps now we are ready to write
numbers
?And then of course
add
, the command that makes this a Command Object:Does this work? No idea. That's what your tests are for. But I hope the ideas are useful.
67d0010
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<3 <3 magnifique. @jredville you just invented distributed red, green, refactor, review, refactor !
67d0010
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️
67d0010
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@timlinquist: The parser idea is great. I considered it, but then ignored the idea as overkill for this problem. Looking at it now, and at your example, it is obvious that the parser is a separate responsibility, thus should be separate. I think it would cause the entire algorithm to just fall into place.
As for "distributed red, green, refactor, review, refactor" - ❤️ it!
@reinh: Hi Rein, don't know if you remember, but we met at Ruby on Ales this year and hung out a little at Deschutes. Thanks for your comments, there are some really good points in there. I'm really digging the idea of defining the domain by finding the nouns, I think it would probably help both on the design side, and the naming side. I also like the approach you took of building up the domain part by part. I often do that, but I think I neglected it due to the list that was sitting in front of me when I approached this Kata. Your point about module methods is a good one too; it is probably smart to be suspicious of modules with behavior that aren't being mixed in!
I'm really looking forward to trying this again tomorrow with these ideas to help shape it.
67d0010
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jredville I'm pretty bad at placing internet names with faces, sorry!
I did do this in a bottom up fashion but it should be pretty easy to arrive at a similar structure by writing
#numbers
and then extracting methods:The temporary explaining variables then become obvious targets for an extract method refactoring. Or, as the code is already pretty intention revealing, you may even leave as is.
67d0010
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@reinh No prob. If we were drinking beers weekly I'd be offended :)
That path makes sense as well. I ended up using the regex method you use here in my latest version (cdcaf35), and found it feels better than the method of parsing the delimiter out as I go, i.e.