From 1189041d8ec759fc840affdb09c15791a7852bec Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 28 Sep 2026 21:48:30 +0000 Subject: [PATCH] Add exercise 3 (Buyers, C#): the pure extract-lambda rep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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. --- .../exercise3/README.md | 40 ++++++ .../exercise3/csharp/global.json | 7 + .../exercise3/csharp/src/Buyer.cs | 22 +++ .../exercise3/csharp/src/Buyers.cs | 33 +++++ .../exercise3/csharp/src/Buyers.csproj | 7 + .../exercise3/csharp/src/IceCream.cs | 9 ++ .../exercise3/csharp/src/PriceList.cs | 27 ++++ .../exercise3/csharp/test/BuyersTests.cs | 19 +++ .../exercise3/csharp/test/BuyersTests.csproj | 18 +++ .../exercise3/solution/README.md | 50 +++++++ .../exercise3/solution/global.json | 7 + .../exercise3/solution/src/Buyer.cs | 22 +++ .../exercise3/solution/src/Buyers.cs | 48 +++++++ .../exercise3/solution/src/Buyers.csproj | 7 + .../exercise3/solution/src/IceCream.cs | 9 ++ .../exercise3/solution/src/PriceList.cs | 27 ++++ .../exercise3/solution/test/BuyersTests.cs | 20 +++ .../solution/test/BuyersTests.csproj | 18 +++ .../storyboard/buyers-example.md | 126 ++++++++++++++++++ .../storyboard/svg/b0-start.svg | 38 ++++++ .../storyboard/svg/b1-lambda-slide.svg | 50 +++++++ .../storyboard/svg/b2-extract-test.svg | 54 ++++++++ 22 files changed, 658 insertions(+) create mode 100644 ideas/split-loop-with-lambda/exercise3/README.md create mode 100644 ideas/split-loop-with-lambda/exercise3/csharp/global.json create mode 100644 ideas/split-loop-with-lambda/exercise3/csharp/src/Buyer.cs create mode 100644 ideas/split-loop-with-lambda/exercise3/csharp/src/Buyers.cs create mode 100644 ideas/split-loop-with-lambda/exercise3/csharp/src/Buyers.csproj create mode 100644 ideas/split-loop-with-lambda/exercise3/csharp/src/IceCream.cs create mode 100644 ideas/split-loop-with-lambda/exercise3/csharp/src/PriceList.cs create mode 100644 ideas/split-loop-with-lambda/exercise3/csharp/test/BuyersTests.cs create mode 100644 ideas/split-loop-with-lambda/exercise3/csharp/test/BuyersTests.csproj create mode 100644 ideas/split-loop-with-lambda/exercise3/solution/README.md create mode 100644 ideas/split-loop-with-lambda/exercise3/solution/global.json create mode 100644 ideas/split-loop-with-lambda/exercise3/solution/src/Buyer.cs create mode 100644 ideas/split-loop-with-lambda/exercise3/solution/src/Buyers.cs create mode 100644 ideas/split-loop-with-lambda/exercise3/solution/src/Buyers.csproj create mode 100644 ideas/split-loop-with-lambda/exercise3/solution/src/IceCream.cs create mode 100644 ideas/split-loop-with-lambda/exercise3/solution/src/PriceList.cs create mode 100644 ideas/split-loop-with-lambda/exercise3/solution/test/BuyersTests.cs create mode 100644 ideas/split-loop-with-lambda/exercise3/solution/test/BuyersTests.csproj create mode 100644 ideas/split-loop-with-lambda/storyboard/buyers-example.md create mode 100644 ideas/split-loop-with-lambda/storyboard/svg/b0-start.svg create mode 100644 ideas/split-loop-with-lambda/storyboard/svg/b1-lambda-slide.svg create mode 100644 ideas/split-loop-with-lambda/storyboard/svg/b2-extract-test.svg diff --git a/ideas/split-loop-with-lambda/exercise3/README.md b/ideas/split-loop-with-lambda/exercise3/README.md new file mode 100644 index 0000000..546b5a4 --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise3/README.md @@ -0,0 +1,40 @@ +# Exercise 3 — Buyers (Split Loop with Lambda, the pure rep) + +A third, minimal example in the same kata theme: the sellers exist +(`DailyForecasts` — Steve, Julie, Francis), the buyers don't. This adds them: +`Buyers` computes a buyer's shopping-list total, and `PriceList` stands for an +external pricing service (it adds random surge pricing, so the test is flaky). + +Written in **C#** (MSTest, .NET 7 — same setup as the kata's `csharp/` folder). +Run with `dotnet test test/BuyersTests.csproj`. + +Contents: + +- `csharp/` — pristine exercise repo: one loop, **one** lambda-able expression + (`priceList.PriceFor(flavour)`), nothing else. No printing, no conditionals, + no void calls. +- `solution/` — end state: extracted static method taking + `Func priceFor`, a no-arg overload passing the method group, + and a deterministic test with a pure lambda (`_ => 2`). + +`IceCream.cs` is copied from +[IceCreamForecasting-Refactoring-Kata](https://github.com/emilybache/IceCreamForecasting-Refactoring-Kata) +(same license as the kata applies). + +## Where this sits in the sequence + +| example | lambda | teaches | +|---|---|---| +| 1. `longRangeForecast` weather loop (Java) | `LongFunction` | the pattern + the drift trap | +| 3. `Buyers` shopping list (C#, this one) | `Func` | the move in its purest form: one expression, nothing else | +| demo/next: `printSalesForecasts` → `SalesScorer`, `PriceList` → `Pricing` | role interface | when one function isn't enough (two side-effectful methods to stub) | + +Next-time arc: marketing adds surge discounts, `PriceList` grows +`DiscountFor(...)` — two methods to stub → extract a role interface +(`IPricing`) instead of two lambdas. See the demo script's closing note. + +## Not verified by a build + +This exercise was authored without a local .NET SDK (structurally mirrored from +the kata's `csharp/` project files). Run `dotnet test test/BuyersTests.csproj` +once on a machine with the SDK before facilitating. diff --git a/ideas/split-loop-with-lambda/exercise3/csharp/global.json b/ideas/split-loop-with-lambda/exercise3/csharp/global.json new file mode 100644 index 0000000..37704f0 --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise3/csharp/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "7.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} diff --git a/ideas/split-loop-with-lambda/exercise3/csharp/src/Buyer.cs b/ideas/split-loop-with-lambda/exercise3/csharp/src/Buyer.cs new file mode 100644 index 0000000..752b770 --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise3/csharp/src/Buyer.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace IceCreamBuyers +{ + public class Buyer + { + public Buyer(string name, IReadOnlyList favourites) + { + Name = name; + Favourites = favourites; + } + + public string Name { get; } + public IReadOnlyList Favourites { get; } + + public int ScoopsOf(IceCream flavour) + { + return 2; + } + } +} diff --git a/ideas/split-loop-with-lambda/exercise3/csharp/src/Buyers.cs b/ideas/split-loop-with-lambda/exercise3/csharp/src/Buyers.cs new file mode 100644 index 0000000..c8acf4e --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise3/csharp/src/Buyers.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; + +namespace IceCreamBuyers +{ + public class Buyers + { + private readonly PriceList priceList = new(); + private readonly List buyers = new() + { + new("Anke", new List { IceCream.Strawberry, IceCream.Vanilla }), + new("Bram", new List { IceCream.Chocolate }), + new("Celine", new List { IceCream.Vanilla, IceCream.Chocolate }), + }; + + 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); + } + return total; + } + + private Buyer ByName(string name) + { + return buyers.Find(b => b.Name == name) + ?? throw new InvalidOperationException($"Unknown buyer {name}."); + } + } +} diff --git a/ideas/split-loop-with-lambda/exercise3/csharp/src/Buyers.csproj b/ideas/split-loop-with-lambda/exercise3/csharp/src/Buyers.csproj new file mode 100644 index 0000000..8268829 --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise3/csharp/src/Buyers.csproj @@ -0,0 +1,7 @@ + + + + net7.0 + + + diff --git a/ideas/split-loop-with-lambda/exercise3/csharp/src/IceCream.cs b/ideas/split-loop-with-lambda/exercise3/csharp/src/IceCream.cs new file mode 100644 index 0000000..16c45ae --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise3/csharp/src/IceCream.cs @@ -0,0 +1,9 @@ +namespace IceCreamBuyers +{ + public enum IceCream + { + Strawberry, + Chocolate, + Vanilla + } +} diff --git a/ideas/split-loop-with-lambda/exercise3/csharp/src/PriceList.cs b/ideas/split-loop-with-lambda/exercise3/csharp/src/PriceList.cs new file mode 100644 index 0000000..6240fa6 --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise3/csharp/src/PriceList.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace IceCreamBuyers +{ + public class PriceList + { + // placeholder implementation - real version would call the pricing service + private static readonly Dictionary BasePrices = + new() + { + { IceCream.Strawberry, 3 }, + { IceCream.Vanilla, 2 }, + { IceCream.Chocolate, 4 }, + }; + + public int PriceFor(IceCream flavour) + { + return BasePrices[flavour] + SurgePricing(); + } + + private int SurgePricing() + { + return new Random().Next(3); + } + } +} diff --git a/ideas/split-loop-with-lambda/exercise3/csharp/test/BuyersTests.cs b/ideas/split-loop-with-lambda/exercise3/csharp/test/BuyersTests.cs new file mode 100644 index 0000000..5f1b386 --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise3/csharp/test/BuyersTests.cs @@ -0,0 +1,19 @@ +using IceCreamBuyers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace IceCreamBuyers.Tests +{ + [TestClass] + public class BuyersTests + { + [TestMethod] + public void ShoppingListTotal() + { + // Anke: 2 flavours x 2 scoops x base prices (3 + 2) = 10. + // Red or flaky — why? (PriceList adds random surge pricing; + // it stands for the external pricing service.) + var buyers = new Buyers(); + Assert.AreEqual(10, buyers.ShoppingListTotal("Anke")); + } + } +} diff --git a/ideas/split-loop-with-lambda/exercise3/csharp/test/BuyersTests.csproj b/ideas/split-loop-with-lambda/exercise3/csharp/test/BuyersTests.csproj new file mode 100644 index 0000000..09707bf --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise3/csharp/test/BuyersTests.csproj @@ -0,0 +1,18 @@ + + + + net7.0 + false + + + + + + + + + + + + + diff --git a/ideas/split-loop-with-lambda/exercise3/solution/README.md b/ideas/split-loop-with-lambda/exercise3/solution/README.md new file mode 100644 index 0000000..3e2ee61 --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise3/solution/README.md @@ -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 priceFor = flavour => priceList.PriceFor(flavour);` + — the invocation in the loop stays put (`priceFor(flavour)`). +2. The loop extracted to + `ShoppingListTotal(Buyer buyer, Func 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 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. diff --git a/ideas/split-loop-with-lambda/exercise3/solution/global.json b/ideas/split-loop-with-lambda/exercise3/solution/global.json new file mode 100644 index 0000000..37704f0 --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise3/solution/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "7.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} diff --git a/ideas/split-loop-with-lambda/exercise3/solution/src/Buyer.cs b/ideas/split-loop-with-lambda/exercise3/solution/src/Buyer.cs new file mode 100644 index 0000000..752b770 --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise3/solution/src/Buyer.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace IceCreamBuyers +{ + public class Buyer + { + public Buyer(string name, IReadOnlyList favourites) + { + Name = name; + Favourites = favourites; + } + + public string Name { get; } + public IReadOnlyList Favourites { get; } + + public int ScoopsOf(IceCream flavour) + { + return 2; + } + } +} diff --git a/ideas/split-loop-with-lambda/exercise3/solution/src/Buyers.cs b/ideas/split-loop-with-lambda/exercise3/solution/src/Buyers.cs new file mode 100644 index 0000000..f1547a0 --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise3/solution/src/Buyers.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; + +namespace IceCreamBuyers +{ + /// + /// Reference end state of exercise 3 (extract lambda, slide, extract method + /// with a function parameter — the pure rep of Split Loop with Lambda). + /// + public class Buyers + { + private readonly PriceList priceList = new(); + private readonly List buyers = new() + { + new("Anke", new List { IceCream.Strawberry, IceCream.Vanilla }), + new("Bram", new List { IceCream.Chocolate }), + new("Celine", new List { IceCream.Vanilla, IceCream.Chocolate }), + }; + + /// + /// No-arg overload: callers unchanged, the "real" pricing function baked in. + /// + public int ShoppingListTotal(string buyerName) + { + return ShoppingListTotal(ByName(buyerName), priceList.PriceFor); + } + + /// + /// The seam: the pricing source is injected as a function. + /// Testable: pass any pure function instead of calling the pricing service. + /// + public int ShoppingListTotal(Buyer buyer, Func priceFor) + { + var total = 0; + foreach (var flavour in buyer.Favourites) + { + total += buyer.ScoopsOf(flavour) * priceFor(flavour); + } + return total; + } + + private Buyer ByName(string name) + { + return buyers.Find(b => b.Name == name) + ?? throw new InvalidOperationException($"Unknown buyer {name}."); + } + } +} diff --git a/ideas/split-loop-with-lambda/exercise3/solution/src/Buyers.csproj b/ideas/split-loop-with-lambda/exercise3/solution/src/Buyers.csproj new file mode 100644 index 0000000..8268829 --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise3/solution/src/Buyers.csproj @@ -0,0 +1,7 @@ + + + + net7.0 + + + diff --git a/ideas/split-loop-with-lambda/exercise3/solution/src/IceCream.cs b/ideas/split-loop-with-lambda/exercise3/solution/src/IceCream.cs new file mode 100644 index 0000000..16c45ae --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise3/solution/src/IceCream.cs @@ -0,0 +1,9 @@ +namespace IceCreamBuyers +{ + public enum IceCream + { + Strawberry, + Chocolate, + Vanilla + } +} diff --git a/ideas/split-loop-with-lambda/exercise3/solution/src/PriceList.cs b/ideas/split-loop-with-lambda/exercise3/solution/src/PriceList.cs new file mode 100644 index 0000000..6240fa6 --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise3/solution/src/PriceList.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace IceCreamBuyers +{ + public class PriceList + { + // placeholder implementation - real version would call the pricing service + private static readonly Dictionary BasePrices = + new() + { + { IceCream.Strawberry, 3 }, + { IceCream.Vanilla, 2 }, + { IceCream.Chocolate, 4 }, + }; + + public int PriceFor(IceCream flavour) + { + return BasePrices[flavour] + SurgePricing(); + } + + private int SurgePricing() + { + return new Random().Next(3); + } + } +} diff --git a/ideas/split-loop-with-lambda/exercise3/solution/test/BuyersTests.cs b/ideas/split-loop-with-lambda/exercise3/solution/test/BuyersTests.cs new file mode 100644 index 0000000..284b366 --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise3/solution/test/BuyersTests.cs @@ -0,0 +1,20 @@ +using IceCreamBuyers; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; + +namespace IceCreamBuyers.Tests +{ + [TestClass] + public class BuyersTests + { + [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.Strawberry, IceCream.Vanilla }); + Assert.AreEqual(8, buyers.ShoppingListTotal(anke, _ => 2)); + } + } +} diff --git a/ideas/split-loop-with-lambda/exercise3/solution/test/BuyersTests.csproj b/ideas/split-loop-with-lambda/exercise3/solution/test/BuyersTests.csproj new file mode 100644 index 0000000..09707bf --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise3/solution/test/BuyersTests.csproj @@ -0,0 +1,18 @@ + + + + net7.0 + false + + + + + + + + + + + + + diff --git a/ideas/split-loop-with-lambda/storyboard/buyers-example.md b/ideas/split-loop-with-lambda/storyboard/buyers-example.md new file mode 100644 index 0000000..0ffaaaa --- /dev/null +++ b/ideas/split-loop-with-lambda/storyboard/buyers-example.md @@ -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.) + +![](svg/b0-start.svg) + +**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. + +![](svg/b1-lambda-slide.svg) + +**After:** + +```csharp +public int ShoppingListTotal(string buyerName) +{ + Func 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. + +![](svg/b2-extract-test.svg) + +**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 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.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. diff --git a/ideas/split-loop-with-lambda/storyboard/svg/b0-start.svg b/ideas/split-loop-with-lambda/storyboard/svg/b0-start.svg new file mode 100644 index 0000000..66c8393 --- /dev/null +++ b/ideas/split-loop-with-lambda/storyboard/svg/b0-start.svg @@ -0,0 +1,38 @@ + + + + + + + + Step 0 — one loop, one unwanted dependency + PriceList stands for the external pricing service (random surge pricing) — that is why the test is red & flaky. + + + + ShoppingListTotal(buyerName) + + buyer = ByName(buyerName) + + + + foreach (var flavour : buyer.Favourites) + total += buyer.ScoopsOf(flavour) * + priceList.PriceFor(flavour); + the pricing loop — one expression, nothing else + + return total; + + + + PriceList + stands for the pricing + service — Random surge + + + + + + test: Anke = 10? + red & flaky ✗ + diff --git a/ideas/split-loop-with-lambda/storyboard/svg/b1-lambda-slide.svg b/ideas/split-loop-with-lambda/storyboard/svg/b1-lambda-slide.svg new file mode 100644 index 0000000..fd609ca --- /dev/null +++ b/ideas/split-loop-with-lambda/storyboard/svg/b1-lambda-slide.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + Step 1 — extract the pricing call into a lambda, and slide it + One expression, nothing else in the way. priceList is a field, so the lambda slides all the way to the top. + + + + ShoppingListTotal(buyerName) + + + + λ priceFor + Func<IceCream,int> = + flavour => priceList.PriceFor(flavour); + + buyer = ByName(buyerName) + + + + foreach (var flavour : buyer.Favourites) + total += buyer.ScoopsOf(flavour) * + priceFor(flavour); + invocation stays exactly where the code was + + + + λ used to be defined right here + + return total; + + + + slide + + + + PriceList + still called — + but only via the λ now + + + diff --git a/ideas/split-loop-with-lambda/storyboard/svg/b2-extract-test.svg b/ideas/split-loop-with-lambda/storyboard/svg/b2-extract-test.svg new file mode 100644 index 0000000..5961691 --- /dev/null +++ b/ideas/split-loop-with-lambda/storyboard/svg/b2-extract-test.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + Step 2 — extract the loop; the lambda becomes a parameter + The test passes its own pricing (_ => 2). PriceList is never touched: deterministic, glanceable. + + + + BuyersTests + var anke = new Buyer(...) + priceFor: _ => 2 + ShoppingListTotal(anke, λ) + Assert.AreEqual(8, ...) + + + + ShoppingListTotal(buyer, λ) + no-arg overload passes + priceList.PriceFor as method group + total = 0 + foreach flavour: + total += scoops * + λ(flavour) + + + + the pricing loop + inputs: buyer, λ + sum of scoops * + λ(flavour) + return total + pure ✓ + + + + λ + + + + + PriceList + never called by the test ✗ + + ✗ + + 2 flavours x 2 scoops x 2 = 8 — every run, not just a lucky roll. +