「テスト駆動開発入門」でTDD
「テスト駆動開発入門」を教材に、言語を Ruby にして TDD してみました。この本は何度か読んでいるので、ちょっと駆け足気味で。
昨日、今日で Part.1 を完遂してみました。基本的な設計は元のコードそのままでやってみたのですが、Ruby らしさ(?)なのか、ちょっと違う部分が出ました。
- Interface を使わずダック・タイピングになった
- 加算や乗算はメソッド(add(), times())ではなく、演算子オーバーロード(+, *)
- 通貨を文字列の変わりにシンボルを使って表現した
- 配列をハッシュのキーにできたので、そうした(Java だとできないので、from/to を束ねた Pair オブジェクトを使ってる)
テストコードはこんな感じです。
#!/usr/local/bin/ruby require 'test/unit' require 'dollar' class DollarTest < Test::Unit::TestCase def setup @fiveBucks = Money.dollar(5) @tenFrancs = Money.franc(10) @bank = Bank.new() end def test_multiplication assert_equal(Money.dollar(15), @fiveBucks * 3) end def test_equality assert(Money.dollar(5) == Money.dollar(5)) assert(Money.dollar(5) != Money.dollar(6)) assert(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 sum = @fiveBucks + @fiveBucks reduced = @bank.reduce(sum, :USD) assert_equal(Money.dollar(10), reduced) end def test_plus_returns_sum sum = @fiveBucks + @fiveBucks assert_equal(@fiveBucks, sum.augend) assert_equal(@fiveBucks, sum.addend) end def test_reduce_sum sum = Sum.new(Money.dollar(3), Money.dollar(4)) result = @bank.reduce(sum, :USD) assert_equal(Money.dollar(7), result) end def test_reduce_money result = @bank.reduce(Money.dollar(1), :USD) assert_equal(Money.dollar(1), result) end def test_reduce_money_different_currency @bank.addRate(: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.rate(:USD, :USD)) end def test_mixed_addition @bank.addRate(:CHF, :USD, 2) result = @bank.reduce(@fiveBucks + @tenFrancs, :USD) assert_equal(Money.dollar(10), result) end def test_sum_plus_money @bank.addRate(:CHF, :USD, 2) sum = Sum.new(@fiveBucks, @tenFrancs) + @fiveBucks result = @bank.reduce(sum, :USD) assert_equal(Money.dollar(15), result) end def test_sum_times @bank.addRate(:CHF, :USD, 2) sum = Sum.new(@fiveBucks, @tenFrancs) * 2 result = @bank.reduce(sum, :USD) assert_equal(Money.dollar(20), result) end end
で、コードがこんな感じ
#!/usr/local/bin/ruby class Money attr_reader :amount, :currency def initialize(amount, currency) @amount = amount @currency = currency end def ==(rhs) return @amount == rhs.amount && @currency == rhs.currency end def self.dollar(amount) return Money.new(amount, :USD) end def self.franc(amount) return Money.new(amount, :CHF) end def *(rhs) return Money.new(@amount * rhs, @currency) end def +(rhs) return Sum.new(self, rhs) end def reduce(bank, to) rate = bank.rate(@currency, to) return Money.new(@amount / rate, to) end def to_s return @amount + " " + @currency end end class Sum attr_reader :augend, :addend def initialize(augend, addend) @augend = augend @addend = addend end def +(rhs) return Sum.new(self, rhs) end def *(rhs) return Sum.new(@augend * rhs, @addend * rhs) end def reduce(bank, to) amount = augend.reduce(bank, to).amount + addend.reduce(bank, to).amount return Money.new(amount, to) end end class Bank def initialize @rates = Hash.new end def reduce(source, to) return source.reduce(self, to) end def addRate(from, to, rate) @rates[[from, to]] = rate end def rate(from, to) return 1 if ( from == to) return @rates[[from, to]] end end
モジュールを使って Interface 的なものを作っても良かったはずなのだけど、気づいたらダック・タイピングになってました。演算子については、Ruby の場合は「+」とかも単なるメソッドだから好みの問題かな。シンボルを使ったのは Ruby らしいコードじゃないかな、と思います。
ダックタイピングは楽ですが、「メソッドが定義されてねぇぞゴルァ」ってなったときにちょっと迷います。Java とか C# みたいにコンパイルエラーになってくれるほうが分かりやすいです。僕自身がコンパイラ言語上がりなせいもあるとは思うけど。
と、言うわけで多分 Part. 2 に続きます。