New in-theme classes: Buyers computes a shopping-list total, PriceList stands for the pricing service (random surge, flaky test). One loop, one lambda-able expression — the third example teaches the move with nothing else in the way. Pristine repo + reference solution (extract lambda, slide, extract method with Func<IceCream,int> parameter, pure-lambda test) + storyboard in the established style. Mirrors the kata's C# setup (net7.0, MSTest); needs a build check on a machine with the SDK.
Reference solution — end state of exercise 3
Derived from IceCreamForecasting-Refactoring-Kata
(same license as the kata applies to the copied IceCream.cs).
Run with dotnet test test/BuyersTests.csproj — the test passes deterministically
without touching the pricing service.
What changed vs the exercise start
- The pricing call extracted into a lambda and slid to the top:
Func<IceCream, int> priceFor = flavour => priceList.PriceFor(flavour);— the invocation in the loop stays put (priceFor(flavour)). - The loop extracted to
ShoppingListTotal(Buyer buyer, Func<IceCream, int> priceFor)— static-free ofPriceList; all inputs are parameters. (C# note: this one is an instance method becauseByNameuses thebuyersfield; the extracted loop method itself touches no fields. If you want itstatic, extract the loop only and keep the lookup in the composed method.) - No-arg
ShoppingListTotal(string)overload keeps callers working; it passes the method grouppriceList.PriceForas the "real" pricing function. - The test passes a pure lambda (
_ => 2) through the overload — deterministic, glanceable: 2 flavours × 2 scoops × 2 = 8.
Intermediate step (between pristine and this solution)
public int ShoppingListTotal(string buyerName)
{
Func<IceCream, int> priceFor = flavour => priceList.PriceFor(flavour); // ← extracted & slid
var buyer = ByName(buyerName);
var total = 0;
foreach (var flavour in buyer.Favourites)
{
total += buyer.ScoopsOf(flavour) * priceFor(flavour); // ← invocation stays
}
return total;
}
Variations (debrief / second round)
_ => 5— everything costs 5: Anke 20, Bram 10, Celine 20.flavour => flavour == IceCream.Chocolate ? 10 : 1— chocolate is expensive.- Delete
PriceListentirely: the extracted method no longer knows it exists — a step-4 move is now unblocked (moveShoppingListTotal(Buyer, Func)to a class withoutPriceList). - Role-interface next step: add
DiscountFor(IceCream)toPriceList(a second side-effectful method) and feel why one lambda is no longer enough — extractIPricinginstead.