Files
learning-hour-maker/ideas/split-loop-with-lambda/storyboard/buyers-example.md
T
Your Name 1189041d8e 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.
2026-09-28 21:48:30 +00:00

4.1 KiB

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.

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:

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:

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:

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:

[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.