Screaming Loud

日々是精進

CodeKata 9:会計システムの帰還

CodeKataの和訳シリーズ。
今回はCodeKata: Kata Nine: Back to the CheckOutです。
スーパーの会計システムの話ですね。


スーパーに戻ろう。今週は、「りんご1個50セント、3個だと1.3ドル」というような価格体系の会計システムを実装してみよう。

Kata1を振り返ると、スーパーの価格で色々なオプションがついたどうやってモデル化するか考えた。「3つで1ドル」「1パウンド1.99ドル」とか「2個買ったら1個タダ」とかそんなものに注目していた。

今週は、商品の合計金額を計算するスーパーの会計を実装してみよう。普通のスーパーではストックキーピングユニット(通称SKU)ってのがある。今回実装対象のスーパーの商品は、アルファベット(A,B,Cとか)の独立な文字を使う。さらに、N個買ったらy円のように複数の価格が存在するものもある。例えば、、A1個の場合は50セントだが、今週はセールをするので、Aを3個買ったら1.3ドルとする。
ということで以下のようなグラフにする。

  Item   Unit      Special
         Price     Price
  ーーーーーーーーーーーーーーー
   A     50       3 for 130
   B     30       2 for 45
   C     20
   D     15

この会計システムは、商品の順番には影響を受けないので、BとAともう一つBとスキャンしたら、ちゃんとB2個とA1個と認識して45+50=95と出力する。また価格は頻繁に変更されるので、それに合わせそれぞれの価格設定を売買会計システムで毎回使えるようにしなければいけない。


会計システムのインターフェースはこんな感じになるべき。

   co = CheckOut.new(pricing_rules)
   co.scan(item)
   co.scan(item)
       :    :
   price = co.total

ここに、Rubyで実装したユニットテストがある。"Price"メソッドはアイテム列をアイテムの集合に分け、最終的な総計を出す前にそれぞれのアイテムに対し、会計の"scan"メソッドを呼ぶ。

class TestPrice < Test::Unit::TestCase

    def price(goods)
      co = CheckOut.new(RULES)
      goods.split(//).each { |item| co.scan(item) }
      co.total
    end

    def test_totals
      assert_equal(  0, price(""))
      assert_equal( 50, price("A"))
      assert_equal( 80, price("AB"))
      assert_equal(115, price("CDBA"))

      assert_equal(100, price("AA"))
      assert_equal(130, price("AAA"))
      assert_equal(180, price("AAAA"))
      assert_equal(230, price("AAAAA"))
      assert_equal(260, price("AAAAAA"))

      assert_equal(160, price("AAAB"))
      assert_equal(175, price("AAABB"))
      assert_equal(190, price("AAABBD"))
      assert_equal(190, price("DABABA"))
    end

こういうアルゴリズムの実装は色んな方法がある。もし時間があれば、色々試してみるといい。

Kataの目的

多少なりともこれは楽しい問題だ。でも、それは上辺だけだ。これは楽しい部分を除くとストレスフルな練習だ。
問題の記述は価格設定のフォーマットに関して何も言及していない。
会計システムが知らない特定のアイテムや価格戦略に対してどうやってルールを作るのか?
どうやって追加される新たな価格ルールに柔軟に対応できるコードデザインをつくるか?