Add exercise 3 (Buyers, C#): the pure extract-lambda rep

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.
This commit is contained in:
Your Name
2026-09-28 21:48:30 +00:00
parent 558ad54516
commit 1189041d8e
22 changed files with 658 additions and 0 deletions
@@ -0,0 +1,50 @@
# Reference solution — end state of exercise 3
Derived from [IceCreamForecasting-Refactoring-Kata](https://github.com/emilybache/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
1. 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)`).
2. The loop extracted to
`ShoppingListTotal(Buyer buyer, Func<IceCream, int> priceFor)` — static-free of
`PriceList`; all inputs are parameters. (C# note: this one is an instance method
because `ByName` uses the `buyers` field; the *extracted loop method itself*
touches no fields. If you want it `static`, extract the loop only and keep the
lookup in the composed method.)
3. No-arg `ShoppingListTotal(string)` overload keeps callers working; it passes the
method group `priceList.PriceFor` as the "real" pricing function.
4. 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)
```csharp
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 `PriceList` entirely: the extracted method no longer knows it exists —
a step-4 move is now unblocked (move `ShoppingListTotal(Buyer, Func)` to a
class without `PriceList`).
- Role-interface next step: add `DiscountFor(IceCream)` to `PriceList` (a second
side-effectful method) and feel why one lambda is no longer enough — extract
`IPricing` instead.