Friday, November 05, 2010

Software Writing Clichés

If you are writing a book, essay, or blog entry about software development, please refrain from all of the following:

  • Lame references to how everyone in offices drink coffee

  • Lame references to Mondays

Seriously, have we learned nothing from Office Space?

Saturday, September 11, 2010

Välkomna till Sverige

My son just came in from riding his "new" bike, which a slightly older neighbour boy loaned to him. Before that, he was playing in the little park, which is about 50 metres from our front door. And before that, he was helping me pick apples from the big tree in our front yard. And this is just another Saturday in Sweden. :)

We moved here almost a month ago now, to a quiet neighbourhood in the southern suburbs of Stockholm. I'm working for a small Internet company named TestFreaks, which has an office next to T-Centralen, Stockholm's central metro station. It takes me just under ten minutes to walk to the metro station near our house, then 25 minutes on the train, then five minutes from T-Centralen to my office--not a bad commute at all.

We found a townhouse to rent, one of about 40 in a little neighbourhood that was built in the 60s. The house is 100 square metres, up from 75 square metres in Dublin and 50 in Tokyo. Kai has his own bedroom, with a full-sized single bed and wallpaper with aeroplanes! The kitchen / dining room, our room, and a spare bedroom that we are using as an office round out the second floor (AKA the first floor in Sweden, the one at ground level is called the ground floor, often labelled 0 on elevators). Downstairs is a large living room and one more bedroom, which we're planning to turn into a "gym" by putting an elliptical trainer in it.

As I mentioned, our neighbourhood has its own little park, which is situated in the centre of a loop of townhouses. There are lots of kids around, and they leave toys in the park to share since it is private to our neighbourhood. There are at least four kids almost exactly Kai's age, and many more slightly older. He is very excited to play with the "big kids", and his best friend is a six year-old girl named Mina who thinks it is very funny that Kai speaks English and not Swedish. Hopefully, he'll pick up Swedish quickly from playing with the kids. We're also signing him up for kindergarten so that Lyani can concentrate on finding a job. He's very excited about going after he and Lyani went to take a tour of the kindergarten last week.

We're enjoying a nice, long summer, which seems to be drawing to a close, but still gives us tee-shirt weather like today. Later on, we're going over to a friend's house for a cookout.

So far, we're enjoying life in Sweden indeed!

Friday, August 13, 2010

Test Driven Development by Example, in Ruby

I have long been interested in the Ruby programming language. Luckily for me, I've just gotten a job which requires me to write code in Ruby, meaning that I finally have to learn Ruby properly. The hardest part of learning a new language for me is finding an interesting and somewhat challenging problem to work on with the new language. Little exercises like the Towers of Hanoi or the Fibonacci sequence are decent problems for programming interviews, but mainly test logic and basic language syntax.

I had just finished reading Programming Ruby 1.9, and was ready to sink my teeth into actually writing some Ruby. Being a long-time proponent of Test Driven Development, an interesting idea occurred to me. In Kent Beck's Test Driven Development: by Example book, he uses the example of adding different currencies together. This is a pretty easy problem to understand, and relatively easy to code up, but has just enough complexity in design to offer me some interesting practice with Ruby. In the book, Beck writes all of his unit tests and code in Java--I wrote the same tests and code in Ruby.

For readers who do not have their copy of Test Driven Development: by Example handy, the problem in a nutshell can be stated by one test:

  • $5 + 10 CHF = $10 if rate is 2:1


Or, in plain English: adding five US dollars to 10 Swiss francs should yield 10 US dollars if the exchange rate is 2 Swiss francs to 1 US dollar. Users of Test Driven Development (hereafter TDD) will not be surprised to learn that writing this test required writing many smaller tests first, such as adding two dollar amounts, converting a dollar amount into a franc amount, etc.

I was able to work through the 15 chapters of the example in two days (maybe six hours of coding time). Ruby's MiniTest unit testing framework is an xUnit variant, just like the JUnit framework that Beck uses in the book, so my Ruby tests are virtually the same as his Java tests. The class design is also the same, with the one exception that I did not need the Expression interface that his Java design requires, thanks to duck typing. It felt to me like duck typing simplified the Ruby code quite a bit, as compared to the Java code. There were certainly several times when Beck needed a page to present some refactorings to clean up the design that took me one line in Ruby. All told, my Ruby version was 62 lines of functional code and 78 lines of test code, as opposed to 91 and 89, respectively, in the Java version.

So, with no further ado, here is the example. I'll show you the tests first, then the functional code, then finally the log of all of my commits while working on this problem (my Git repository is available for anyone who wants it: http://www.jmglov.net/opensource/src/tdd-by-example_ruby_git.tar.gz).

Test code: test_Money.rb

#!/usr/bin/env ruby1.9.1

require 'money'
require 'test/unit'

class TestMoney < MiniTest::Unit::TestCase
def test_multiplication
five = Money.dollar(5)
assert_equal(Money.dollar(10), five * 2)
assert_equal(Money.dollar(15), five * 3)
end

def test_equality
assert(Money.dollar(5) == Money.dollar(5))
refute(Money.dollar(5) == Money.dollar(6))
refute(Money.dollar(5) == Money.franc(5))
end

def test_currency
assert_equal(:USD, Money.dollar(1).currency)
assert_equal(:CHF, Money.franc(1).currency)
end

def test_simple_addition
five = Money.dollar(5)
sum = five + five
bank = Bank.new
reduced = bank.reduce(sum, :USD)
assert_equal(Money.dollar(10), reduced)
end

def test_plus_returns_sum
five = Money.dollar(5)
sum = five + five
assert_equal(five, sum.augend)
assert_equal(five, sum.addend)
end

def test_reduce_sum
sum = Sum.new(Money.dollar(3), Money.dollar(4))
bank = Bank.new
result = bank.reduce(sum, :USD)
assert_equal(Money.dollar(7), result)
end

def test_reduce_money
bank = Bank.new
result = bank.reduce(Money.dollar(1), :USD)
assert_equal(Money.dollar(1), result)
end

def test_reduce_money_different_currency
bank = Bank.new
bank.add_rate(:CHF, :USD, 2)
result = bank.reduce(Money.franc(2), :USD)
assert_equal(Money.dollar(1), result)
end

def test_identity_rate
assert_equal(1, Bank.new.rate(:USD, :USD))
end

def test_mixed_addition
five_bucks = Money.dollar(5)
ten_francs = Money.franc(10)
bank = Bank.new
bank.add_rate(:CHF, :USD, 2)
result = bank.reduce(five_bucks + ten_francs, :USD)
assert_equal(Money.dollar(10), result)
end

def test_sum_plus_money
five_bucks = Money.dollar(5)
ten_francs = Money.franc(10)
bank = Bank.new
bank.add_rate(:CHF, :USD, 2)
sum = Sum.new(five_bucks, ten_francs) + five_bucks
result = bank.reduce(sum, :USD)
assert_equal(Money.dollar(15), result)
end

def test_sum_times
five_bucks = Money.dollar(5)
ten_francs = Money.franc(10)
bank = Bank.new
bank.add_rate(:CHF, :USD, 2)
sum = Sum.new(five_bucks, ten_francs) * 2
result = bank.reduce(sum, :USD)
assert_equal(Money.dollar(20), result)
end
end


Functional code: money.rb


class Money
attr_reader :amount, :currency

def initialize(amount, currency)
@amount = amount
@currency = currency
end

def self.dollar(amount)
Money.new(amount, :USD)
end

def self.franc(amount)
Money.new(amount, :CHF)
end

def ==(other)
@amount == other.amount &&
@currency == other.currency
end

def *(multiplier)
Money.new(@amount * multiplier, @currency)
end

def +(addend)
Sum.new(self, addend)
end

def reduce(bank, to_currency)
rate = bank.rate(@currency, to_currency)
Money.new(@amount / rate, to_currency)
end
end

class Bank
def initialize
@rates = {}
end

def reduce(expression, to_currency)
expression.reduce(self, to_currency)
end

def add_rate(from, to, rate)
@rates[[from, to]] = rate
end

def rate(from, to)
return 1 if from == to
@rates[[from, to]]
end
end

class Expression
end

class Sum
attr_reader :augend, :addend

def initialize(augend, addend)
@augend = augend
@addend = addend
end

def reduce(bank, to_currency)
amount = @augend.reduce(bank, to_currency).amount +
@addend.reduce(bank, to_currency).amount
Money.new(amount, to_currency)
end

def +(addend)
Sum.new(self, addend)
end

def *(multiplier)
Sum.new(@augend * multiplier, @addend * multiplier)
end
end


Commit log

  1. TestMoney#test_multiplication fails to compile (C1 : Multi-Currency Money)

  2. test_Dollar: TestDollar should subclass Test::Unit::Testcase instead of MiniTest::... (C1 - Multi-Currency Money)

  3. test_Dollar: should be Dollar.new(...) (C1 - Multi-Currency Money)

  4. test_Dollar#test_multiplication: red bar! (C1 - Multi-Currency Money)

  5. TestDollar#test_multiplication: green bar! (C1 - Multi-Currency Money)

  6. Dollar#times sets @amount (green bar) (C1 - Multi-Currency Money)

  7. Dollar#times real implementation (green bar) (C1 - Multi-Currency Money)

  8. TestDollar#test_multiplication object modified (RB) (C2 - Degenerate Objects)

  9. TestDollar expects new obj from Dollar#times (ERR) (C2 - Degenerate Objects)

  10. Dollar#times returns new obj (GB) (C2 - Degenerate Objects)

  11. TestDollar#test_equality (RB) (C3 - Equality for All)

  12. Dollar#== return true (GB) (C3 - Equality for All)

  13. TestDollar#test_equality tests inequality (RB) (C3 - Equality for All)

  14. Dollar#== actual implementation (GB) (C3 - Equality for All)

  15. test_Dollar: switched to ruby1.9.1 and MiniTest

  16. TestDollar#test_multiplication asserts equality(GB) (C4 - Privacy)

  17. Dollar#amount is protected (GB) (C4 - Privacy)

  18. TestFranc#test_multiplication (ERR) (C5 - Franc-ly Speaking)

  19. Copied Dollar to Franc (GB) (C5 - Franc-ly Speaking)

  20. Added Money class (GB) (C6 - Equality for All, Redux)

  21. Dollar subclasses Money (GB) (C6 - Equality for All, Redux)

  22. Pushed Dollar#== up to Money (GB) (C6 - Equality for All, Redux) (Ruby eliminates the need to fool around with casts and instance variables!)

  23. Added TestFranc#test_equality (GB) (C6 - Equality for All, Redux)

  24. Franc subclasses Money (GB) (C6 - Equality for All, Redux)

  25. Removed Franc#== (GB) (C6 - Equality for All, Redux)

  26. TestMoney#test_equality exposes a bug (ERR): we should have moved the protected attr_reader :amount up to Money

  27. Pushed protected @amount up to Money (GB) (C7 - Apples and Oranges)

  28. Money#== compares classes (GB) (C7 - Apples and Oranges)

  29. TestDollar#test_multiplication use Money#dollar(GB) (C8 - Makin' Objects) (Ruby's duck typing eliminates the Money#times compiler error from the book!)

  30. Using Money#dollar in all tests (GB) (C8 - Makin' Objects)

  31. Added Money#franc factory (GB) (C8 - Makin' Objects)

  32. test_Money requiring 'money' breaks tests (ERR) (C8 - Makin' Objects) (This is a Ruby-only problem, since the Java tests probably do an 'import com.wycash.money.*')

  33. All classes declared in money.rb (GB) (C8 - Makin' Objects)

  34. Deleted dollar.rb and franc.rb (GB) (C8 - Makin' Objects)

  35. Money#currency and unit tests for same (GB) (C9 - Times We're Livin' In)

  36. Currency becomes an instance variable (GB) (C9 - Times We're Livin' In)

  37. Pulled up Money#currency (GB) (C9 - Times We're Livin' In)

  38. Added currency param to Franc#new (ERR) (C9 - Times We're Livin' In)

  39. Money#franc and Franc#times pass nil currency (RB) (C9 - Times We're Livin' In)

  40. Franc#times calls Money#franc which passes :CHF(GB) (C9 - The Times We're Livin' In)

  41. Money#dollar passes :USD (GB) (C9 - The Times We're Livin' In)

  42. Pushed up Dollar#new and Franc#new to Money (GB) (C9 - The Times We're Livin' In)

  43. Inlined factory in Dollar and Franc #times (GB) (C10 - Interesting Times)

  44. Dollar and Franc#times pass @currency to ctor (GB) (C10 - Interesting times)

  45. Dollar and Franc#times return a Money obj (RB) (C10 - Interesting Times)

  46. Backed out #times returning Money (GB) (C10 - Interesting Times)

  47. TestMoney#test_different_class_equality (RB) (C10 - Interesting Times)

  48. Money#== checks currency rather than class (GB) (C10 - Interesting Times) (This change feels more natural in Ruby, where duck typing means that testing class is rarely the best way forward)

  49. Franc#times returns a Money obj (GB) (C10 - Interesting Times)

  50. Dollar#times returns a Money obj (GB) (C10 - Interesting Times)

  51. Pulled up Money#times (GB) (C10 - Interesting Times)

  52. Removed explicit Money#currency (GB) (we already have attr_reader :currency, so this is unnecessary; the fact that I stuck it in there in the beginning means I still have some way to go to think natively in Ruby)

  53. Moved attr_reader :currency to top of money.rb (GB) (just to improve readability, in my opinion)

  54. Money#dollar and #franc return Money obj (GB) (C11 - The Root of All Evil)

  55. Removed Dollar class (GB) (C11 - The Root of All Evil)

  56. Removed Franc class (RB) (C11 - The Root of All Evil)

  57. Removed TestMoney#test_dft_class_inequality (GB) (and TestFranc#test_equality, which is equivalent to removing the third and forth assertions in testEquality() from TDDbE p.52) (C11 - The Root of All Evil)

  58. Removed TestFranc#test_multiplication (GB) (and test_Franc.rb, since there are no tests left in the file) (C11 - The Root of All Evil)

  59. Moved tests in test_Dollar.rb to test_Money.rb (GB) (tests are easier to keep track of all in one file, and since we don't have a Dollar subclass any more, the test naming convention is broken anyway)

  60. Added TestMoney#test_simple_addition (ERR) (C12 - Addition, Finally)

  61. Implemented Money#+ (GB) (C12 - Addition, Finally) (obvious implementation)

  62. TestMoney#t_smpl#add uses Expressions, Banks (ERR) (C12 - Addition, Finally)

  63. Fake implementation of Bank#reduce (GB) (C12 - Addition, Finally) (there should have been a red bar step before this one, but returning nil from Bank#reduce causes a compilation error in Ruby due to the lack of a formal return specification, i.e. def [Money] reduce...)

  64. TestMoney#t_smpl_add should add five + five (GB) (oversight on my part)

  65. TestMoney#test_plus_returns_sum (ERR) (C13 - Make It)

  66. Money#+ returns a Sum obj (GB) (C13 - Make It)

  67. Added TestMoney#test_reduce_sum (RB) (C13 - Make It)

  68. Bank#reduce actually adds (ERR) (C13 - Make It) (this seems like a bug in the TDDbE book to me; as we made the amount field protected in a previous chapter)

  69. Implemented Money#reduce (ERR) (C13 - Make It) (this should be a green bar, according to TDDbE, but the protected Money#amount accessor is called again)

  70. Made Money#amount public to stop errors (GB) (we want to be able to follow TDDbE closely, despite the seeming bug--I should do this exercise in Java as well to see if there really is a bug)

  71. Bank#reduce special case for Money (GB) (C13 - Make It)

  72. Implemented Money#reduce (GB) (C13 - Make It)

  73. Removed "cast" and class check from Bank#reduce(GB) (C13 - Make It)

  74. TestMoney#test_reduce_money_different_currency (RB) (C14 - Change)

  75. Hard-coded CHF -> USD in Money#reduce (GB) (C14 - Change)

  76. Added bank parameter to all #reduce methods (GB) (C14 - Change)

  77. Moved rate calculation to Bank#rate (GB) (C14 - Change)

  78. Bank stores rates in hashtable (ERR) (C14 - Change) (because Ruby can use an array--or any other object, for that matter--as a hash key, we don't need the Pair class from TDDbEx; we can simply use a two-element array ([from_currency, to_currency]) as our rate hash key)

  79. Bank#rate returns 1 when from and to are equal (GB) (C14 - Change)

  80. Added TestMoney#test_mixed_addition (RB) (C15 - Mixed Currencies) (this would throw "a host of compile errors" in Java, according to TDDbE, but duck typing means we compile just fine and simply fail a test)

  81. Sum#reduce reduces both arguments (GB) (C15 - Mixed Currencies)

  82. Stubbed out Sum#+ (GB) (C15 - Mixed Currencies) (we just did this to stay in sync with TDDbE--Java's type system required the stub, but Ruby's duck types care not)

  83. Added TestMoney#test_sum_plus_money (ERR) (C16 - Abstration, Finally)

  84. Real implementation of Sum#+ (GB) (C16 - Abstration, Finally)

  85. Added TestMoney#test_sum_times (ERR) (C16 - Abstration, Finally)

  86. Implemented Sum#times (GB) (C16 - Abstration, Finally)

  87. Redefined Money and Sum#times as #* (GB) (this seems more Ruby-native to me)

  88. TestMoney#test_plus_same_currency_returns_money(RB) (C16 - Abstraction, Finally)

  89. Removed TestMoney#t_plus_same_curr_ret_mny (GB) (C16 - Abstraction, Finally) (we removed this test because a clean way to have Money#+ return a Money object instead of a Sum was not obvious)