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:
@@ -0,0 +1,126 @@
|
||||
# Example 3 — Buyers: extract lambda in its purest form
|
||||
|
||||
Third rep of the Split Loop with Lambda mechanics, in C#. Example 1
|
||||
(`longRangeForecast`) had a drift trap and a dead call; the demo
|
||||
(`printSalesForecasts`) had three dependencies and grew into a role interface.
|
||||
This one has **one loop and one lambda-able expression, and nothing else** —
|
||||
the move in its purest form. Diagrams use the same
|
||||
[vocabulary as the main storyboard](storyboard.md).
|
||||
|
||||
Kata theme: the sellers exist (`DailyForecasts` — Steve, Julie, Francis), the
|
||||
buyers don't. `Buyers` computes a buyer's shopping-list total; `PriceList`
|
||||
stands for the external pricing service. Exercise repo: `../exercise3/csharp`,
|
||||
end state: `../exercise3/solution`.
|
||||
|
||||
---
|
||||
|
||||
## Step 0 — one loop, one unwanted dependency
|
||||
|
||||
**Goal:** the test is red or flaky — why? (`PriceList` adds random surge
|
||||
pricing; it stands for the external pricing service.)
|
||||
|
||||

|
||||
|
||||
**Before:**
|
||||
|
||||
```csharp
|
||||
public int ShoppingListTotal(string buyerName)
|
||||
{
|
||||
var buyer = ByName(buyerName);
|
||||
var total = 0;
|
||||
foreach (var flavour in buyer.Favourites)
|
||||
{
|
||||
total += buyer.ScoopsOf(flavour) * priceList.PriceFor(flavour); // ← unwanted dependency
|
||||
}
|
||||
return total;
|
||||
}
|
||||
```
|
||||
|
||||
**Checkpoint:** run the test. Flaky. What does the loop need from outside?
|
||||
*Given a flavour, what does it cost?* That question is the lambda.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — extract the pricing call into a lambda, and slide it
|
||||
|
||||
**Goal:** the move, with nothing else in the way. Extract, then slide — the
|
||||
invocation stays in the loop.
|
||||
|
||||

|
||||
|
||||
**After:**
|
||||
|
||||
```csharp
|
||||
public int ShoppingListTotal(string buyerName)
|
||||
{
|
||||
Func<IceCream, int> priceFor = flavour => priceList.PriceFor(flavour); // ← SLID to the top...
|
||||
var buyer = ByName(buyerName);
|
||||
var total = 0;
|
||||
foreach (var flavour in buyer.Favourites)
|
||||
{
|
||||
total += buyer.ScoopsOf(flavour) * priceFor(flavour); // ← ...invocation stays here
|
||||
}
|
||||
return total;
|
||||
}
|
||||
```
|
||||
|
||||
**Checkpoint:** compiles; test still flaky (the lambda still calls
|
||||
`PriceList` — that is fine). Unlike the scoring-loop example, `priceList` is a
|
||||
*field*, so the lambda slides all the way to the top. Slide, don't move.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — extract the loop; the lambda becomes a parameter
|
||||
|
||||
**Goal:** select the loop, Extract Method. The loop touches no fields — the
|
||||
lambda is the only thing crossing the boundary.
|
||||
|
||||

|
||||
|
||||
**After:**
|
||||
|
||||
```csharp
|
||||
public int ShoppingListTotal(string buyerName) // ← no-arg overload: callers unchanged
|
||||
{
|
||||
return ShoppingListTotal(ByName(buyerName), priceList.PriceFor); // ← the "real" pricing, as a method group
|
||||
}
|
||||
|
||||
public int ShoppingListTotal(Buyer buyer, Func<IceCream, int> priceFor) // ← the seam: pricing is a parameter
|
||||
{
|
||||
var total = 0;
|
||||
foreach (var flavour in buyer.Favourites)
|
||||
{
|
||||
total += buyer.ScoopsOf(flavour) * priceFor(flavour);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
```
|
||||
|
||||
(The extracted loop method touches no fields; it sits in the same class as
|
||||
`ByName` for convenience. Move it to a class without `PriceList` any time —
|
||||
that is step 4 of the full pattern, unblocked.)
|
||||
|
||||
**The test — the whole point:**
|
||||
|
||||
```csharp
|
||||
[TestMethod]
|
||||
public void ShoppingListTotalUsesThePricingWePassIn()
|
||||
{
|
||||
// pure pricing function: everything costs 2.
|
||||
// Anke: 2 flavours x 2 scoops x 2 = 8 — deterministic, glanceable.
|
||||
var buyers = new Buyers();
|
||||
var anke = new Buyer("Anke", new List<IceCream> { IceCream.Strawberry, IceCream.Vanilla });
|
||||
Assert.AreEqual(8, buyers.ShoppingListTotal(anke, _ => 2));
|
||||
}
|
||||
```
|
||||
|
||||
**Checkpoint:** green — deterministically. `PriceList` is never touched.
|
||||
|
||||
- `grep PriceFor src/` — the extracted method has no hits.
|
||||
- C# note: the extracted method stays an instance method here because the
|
||||
class also holds `ByName`; if you want a `static`, extract the loop only and
|
||||
keep the lookup in the composed method.
|
||||
|
||||
**Debrief in one line:** the loop now reads "for each favourite flavour, add
|
||||
scoops times price" — and the pricing is a question anyone can answer with one
|
||||
line of test code.
|
||||
Reference in New Issue
Block a user