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