Back to the supermarket. This week, we’ll implement the code for a checkout system that handles pricing schemes such as "apples cost 50 cents, three apples cost $1.30."
Way back in KataOne we thought about how to model the various options for supermarket pricing. We looked at things such as "three for a dollar," "$1.99 per pound," and "buy two, get one free."
This week, let’s implement the code for a supermarket checkout that calculates the total price of a number of items. In a normal supermarket, things are identified using Stock Keeping Units, or SKUs. In our store, we’ll use individual letters of the alphabet (A, B, C, and so on). Our goods are priced individually. In addition, some items are multipriced: buy n of them, and they’ll cost you y cents. For example, item ‘A’ might cost 50 cents individually, but this week we have a special offer: buy three ‘A’s and they’ll cost you $1.30. In fact this week’s prices are:
Item Unit Special
Price Price
--------------------------
A 50 3 for 130
B 30 2 for 45
C 20
D 15
Our checkout accepts items in any order, so that if we scan a B, an A, and another B, we’ll recognize the two B’s and price them at 45 (for a total price so far of 95). Because the pricing changes frequently, we need to be able to pass in a set of pricing rules each time we start handling a checkout transaction.
The interface to the checkout should look like:
co = CheckOut.new(pricing_rules)
co.scan(item)
co.scan(item)
: :
price = co.total
Here’s a set of unit tests for a Ruby implementation. The helper method price lets you specify a sequence of items using a string, calling the checkout’s scan method on each item in turn before finally returning the total price.
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
def test_incremental
co = CheckOut.new(RULES)
assert_equal( 0, co.total)
co.scan("A"); assert_equal( 50, co.total)
co.scan("B"); assert_equal( 80, co.total)
co.scan("A"); assert_equal(130, co.total)
co.scan("A"); assert_equal(160, co.total)
co.scan("B"); assert_equal(175, co.total)
end
end
There are lots of ways of implementing this kind of algorithm; if you have time, experiment with several.
Objectives of the Kata
To some extent, this is just a fun little problem. But underneath the covers, it’s a stealth exercise in decoupling. The challenge description doesn’t mention the format of the pricing rules. How can these be specified in such a way that the checkout doesn’t know about particular items and their pricing strategies? How can we make the design flexible enough so that we can add new styles of pricing rule in the future?
Finally got around to abstracted version (using Strategy Pattern), at http://www.codosaur.us/2010/11/code-kata-nine-supermarket-checkout.html . Got sidetracked by a few other things in the meantime, and posted about some of them....
Posted by: Dave Aronson | November 21, 2010 at 08:56 PM
I finally uploaded a 20" screencast for the Checkout-Kata: http://www.vimeo.com/11604377
It was the result of a Kata session we did at the Jax-conference in germany this year.
There are thousand TDD-aspects i'd still love to improve, but i like the decoupling of cart and rules in this implementation. Basically the cart allows the rules to select which items they want to bill. Nice side effect: the cart items ("A", "B",... in my case) can be replaced by any other object and the code should still work. :-)
I did this Kata about 20-25 times with 3 or 4 different approaches.
Posted by: Stdout | October 07, 2010 at 03:17 PM
Just posted a *non*-abstracted version (intending to show the difference with an abstracted one to be posted later), in Ruby, at http://www.codosaur.us/2010/09/code-kata-nine-supermarket-checkout.html .
Posted by: Dave Aronson | September 06, 2010 at 08:57 PM
I came up with about four implementations of the "buy x units, get y units free".
I'll enjoy this one.
Posted by: grow taller | August 17, 2010 at 03:39 PM
This one does offer many opportunities to refactor the code as you go.
Posted by: ClubPenguin | July 02, 2010 at 03:11 AM
I'm approaching this from a BDD angle in C#.Net When I worked through the Week 1 "Thinking / modeling" kata, I came up with about four implementations of the "buy x units, get y units free".
I'll enjoy this one.
Posted by: Rudy Lattae | October 13, 2009 at 04:09 PM
This was a nice one, i developed my soultion using TDD in C#.Net
Posted by: Luke | July 29, 2009 at 09:09 AM
I liked this one a lot. This one does offer many opportunities to refactor the code as you go. Very nice!
Posted by: Michael Finney | August 17, 2008 at 01:33 PM