tsucchi’s diary(元はてなダイアリー)

はてなダイアリー(d.hatena.ne.jp/tsucchi1022)から移行したものです

「テスト駆動開発入門」でTDD


「テスト駆動開発入門」
を教材に、言語を Ruby にして TDD してみました。この本は何度か読んでいるので、ちょっと駆け足気味で。

昨日、今日で Part.1 を完遂してみました。基本的な設計は元のコードそのままでやってみたのですが、Ruby らしさ(?)なのか、ちょっと違う部分が出ました。

  1. Interface を使わずダック・タイピングになった
  2. 加算や乗算はメソッド(add(), times())ではなく、演算子オーバーロード(+, *)
  3. 通貨を文字列の変わりにシンボルを使って表現した
  4. 配列をハッシュのキーにできたので、そうした(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 に続きます。




「テスト駆動開発入門」