I have implemented the Harry Potter Kata and I need your feedbacks.
The rules are:
A book costs 8 euros. There are 5 different volumes. To get a discount, you must buy books of different volumes:
- Buying 1 book doesn't give you a discount
- Buying 2 books applies a 5% discount
- Buying 3 books applies a 10% discount
- Buying 4 books applies a 15% discount
- Buying 5 books applies a 20% discount
Examples:
- Given a basket When I buy 2 books of volume 1 Then the total is 16 euros
- Given a basket When I buy 1 book of volume 1 And I buy 1 book of volume 2 Then the total is 15.2 euros
The implementation:
public class Book { public double Price { get; } = 8; public string Volume { get; } public Book(string volume) { Volume = volume; } public override bool Equals(object obj) { // Is null? if (obj is null) { return false; } // Is the same object? if (ReferenceEquals(this, obj)) { return true; } // Is the same type? return obj.GetType() == this.GetType() && IsEqual((Book)obj); } public override int GetHashCode() { return HashCode.Combine(Price, Volume); } private bool IsEqual(Book book) { // A pure implementation of value equality that avoids the routine checks above // We use String.Equals to really drive home our fear of an improperly overridden "==" return string.Equals(Volume, book.Volume) && Equals(Price, book.Price); } } public class Basket { private readonly IDictionary<int, IDiscountStrategy> _discountStrategies; private readonly IList<HashSet<Book>> _bookSets; public Basket(IDictionary<int, IDiscountStrategy> discountStrategies) { _discountStrategies = discountStrategies; _bookSets = new List<HashSet<Book>>(); } public void AddBook(Book book) { var setIndex = 0; var inserted = false; while (!inserted) { if (_bookSets.Count <= setIndex) _bookSets.Add(new HashSet<Book>()); if (!_bookSets[setIndex].Contains(book) && _bookSets[setIndex].Count < _discountStrategies.Count) { _bookSets[setIndex].Add(book); inserted = true; } setIndex++; } } public double Checkout() => _bookSets.Sum(set => GetTotalCostBeforeDiscount(set) * _discountStrategies[set.Count].GetDiscount()); private double GetTotalCostBeforeDiscount(IEnumerable<Book> books) => books.Sum(b => b.Price); } public class FifteenPercentDiscount : IDiscountStrategy { public double GetDiscount() => .85; } public class FivePercentDiscount : IDiscountStrategy { public double GetDiscount() => .95; } public class NoDiscount : IDiscountStrategy { public double GetDiscount() => 1; } public class TenPercentDiscount : IDiscountStrategy { public double GetDiscount() => .9; } public class TwentyPercentDiscount : IDiscountStrategy { public double GetDiscount() => 0.8; } public interface IDiscountStrategy { double GetDiscount(); } public class BookTest { private readonly Basket _basket; public BookTest() { var discountStrategies = new Dictionary<int, IDiscountStrategy> { {1, new NoDiscount() }, {2, new FivePercentDiscount() }, {3, new TenPercentDiscount() }, {4, new FifteenPercentDiscount() }, {5, new TwentyPercentDiscount() }, }; _basket = new Basket(discountStrategies); } [Fact] public void should_book_price_equals_to_8_when_created() { //arrange var book = new Book("Volume 1"); var expected = 8; //act var actual = book.Price; //assert Assert.Equal(expected, actual); } [Fact] public void should_two_books_price_equals_to_16_when_they_have_the_same_volume() { //arrange _basket.AddBook(new Book("Volume 1")); _basket.AddBook(new Book("Volume 1")); var expected = 16; //act var actual = _basket.Checkout(); //assert Assert.Equal(expected, actual); } [Fact] public void should_have_five_percent_discount_when_two_books_books_do_not_have_the_same_volume() { //arrange _basket.AddBook(new Book("Volume 1")); _basket.AddBook(new Book("Volume 2")); var expected = 16 * .95; //act var actual = _basket.Checkout(); //assert Assert.Equal(expected, actual); } [Fact] public void should_have_ten_percent_discount_when_three_books_do_not_have_the_same_volume() { //arrange _basket.AddBook(new Book("Volume 1")); _basket.AddBook(new Book("Volume 2")); _basket.AddBook(new Book("Volume 3")); var expected = 24 * .90; //act var actual = _basket.Checkout(); //assert Assert.Equal(expected, actual); } [Fact] public void should_have_fifteen_percent_discount_when_four_books_do_not_have_the_same_volume() { //arrange _basket.AddBook(new Book("Volume 1")); _basket.AddBook(new Book("Volume 2")); _basket.AddBook(new Book("Volume 3")); _basket.AddBook(new Book("Volume 4")); var expected = 32 * .85; //act var actual = _basket.Checkout(); //assert Assert.Equal(expected, actual); } [Fact] public void should_have_twenty_percent_discount_when_four_books_do_not_have_the_same_volume() { //arrange _basket.AddBook(new Book("Volume 1")); _basket.AddBook(new Book("Volume 2")); _basket.AddBook(new Book("Volume 3")); _basket.AddBook(new Book("Volume 4")); _basket.AddBook(new Book("Volume 5")); var expected = 40 * .8; //act var actual = _basket.Checkout(); //assert Assert.Equal(expected, actual); } [Fact] public void should_have_ten_percent_discount_when_three_books_do_not_have_the_same_volume_and_no_discount_for_the_last_book() { //arrange _basket.AddBook(new Book("Volume 1")); _basket.AddBook(new Book("Volume 2")); _basket.AddBook(new Book("Volume 3")); _basket.AddBook(new Book("Volume 3")); var expected = (24 * .9) + 8; //act var actual = _basket.Checkout(); //assert Assert.Equal(expected, actual); } [Fact] public void should_have_ten_percent_discount_when_three_books_do_not_have_the_same_volume_and_no_discount_for_the_last_two_books() { //arrange _basket.AddBook(new Book("Volume 1")); _basket.AddBook(new Book("Volume 2")); _basket.AddBook(new Book("Volume 3")); _basket.AddBook(new Book("Volume 3")); _basket.AddBook(new Book("Volume 3")); var expected = (24 * .9) + 16; //act var actual = _basket.Checkout(); //assert Assert.Equal(expected, actual); } [Fact] public void should_create_three_sets_and_the_max_size_of_a_set_equal_to_the_number_of_strategies_to_not_get_KeyNotFoundException() { //arrange _basket.AddBook(new Book("Volume 1")); _basket.AddBook(new Book("Volume 2")); _basket.AddBook(new Book("Volume 3")); _basket.AddBook(new Book("Volume 4")); _basket.AddBook(new Book("Volume 5")); _basket.AddBook(new Book("Volume 6")); _basket.AddBook(new Book("Volume 7")); _basket.AddBook(new Book("Volume 8")); _basket.AddBook(new Book("Volume 9")); _basket.AddBook(new Book("Volume 10")); _basket.AddBook(new Book("Volume 11")); _basket.AddBook(new Book("Volume 11")); var expected = (40 * .8) + (40 * .8) + 16; //act var actual = _basket.Checkout(); //assert Assert.Equal(expected, actual); } } Thanks in advance!
Checkout()method has two lines. I would use old style of method with curly braces instead of=>. I would renameCheckout()method to something more self-explanatory, e.g.GetTotalCostAfterDiscount()or_basket.Calculate(). In your first test method, I would check one book basket calculation with the same concept as used in all other tests. When reading this lambda.Sum(set =>I was firstly confused, that set is some keyword. I would uses =>. I like your solution! \$\endgroup\$