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,38 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
<defs>
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
</marker>
</defs>
<rect width="760" height="460" fill="#ffffff"/>
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 0 — one loop, one unwanted dependency</text>
<text x="24" y="56" font-size="13" fill="#475569">PriceList stands for the external pricing service (random surge pricing) — that is why the test is red &amp; flaky.</text>
<!-- method -->
<rect x="60" y="74" width="400" height="260" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="76" y="98" font-size="14" font-weight="bold" fill="#0f172a">ShoppingListTotal(buyerName)</text>
<text x="80" y="126" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">buyer = ByName(buyerName)</text>
<!-- loop -->
<rect x="80" y="140" width="360" height="120" rx="8" fill="#eef2ff" stroke="#818cf8"/>
<text x="94" y="166" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">foreach (var flavour : buyer.Favourites)</text>
<text x="94" y="196" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">total += buyer.ScoopsOf(flavour) *</text>
<text x="94" y="216" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#dc2626"> priceList.PriceFor(flavour);</text>
<text x="94" y="244" font-size="10" fill="#64748b">the pricing loop — one expression, nothing else</text>
<text x="80" y="300" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">return total;</text>
<!-- PriceList -->
<rect x="580" y="150" width="150" height="90" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
<text x="655" y="180" font-size="14" font-weight="bold" fill="#dc2626" text-anchor="middle">PriceList</text>
<text x="655" y="200" font-size="11" fill="#7f1d1d" text-anchor="middle">stands for the pricing</text>
<text x="655" y="216" font-size="11" fill="#7f1d1d" text-anchor="middle">service — Random surge</text>
<line x1="444" y1="206" x2="572" y2="192" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
<!-- test -->
<rect x="580" y="290" width="150" height="60" rx="8" fill="#ffffff" stroke="#94a3b8" stroke-dasharray="4 3"/>
<text x="655" y="314" font-size="12" fill="#334155" text-anchor="middle">test: Anke = 10?</text>
<text x="655" y="332" font-size="12" font-weight="bold" fill="#dc2626" text-anchor="middle">red &amp; flaky ✗</text>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,50 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
<defs>
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
</marker>
<marker id="arrP" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#9333ea"/>
</marker>
</defs>
<rect width="760" height="460" fill="#ffffff"/>
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 1 — extract the pricing call into a lambda, and slide it</text>
<text x="24" y="56" font-size="13" fill="#9333ea" font-weight="bold">One expression, nothing else in the way. priceList is a field, so the lambda slides all the way to the top.</text>
<!-- method -->
<rect x="60" y="74" width="400" height="330" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="76" y="98" font-size="14" font-weight="bold" fill="#0f172a">ShoppingListTotal(buyerName)</text>
<!-- lambda at top -->
<rect x="80" y="110" width="360" height="82" rx="8" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
<text x="94" y="136" font-size="12" font-weight="bold" fill="#9333ea">λ priceFor</text>
<text x="94" y="162" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626">Func&lt;IceCream,int&gt; =</text>
<text x="94" y="180" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626"> flavour =&gt; priceList.PriceFor(flavour);</text>
<text x="80" y="200" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">buyer = ByName(buyerName)</text>
<!-- loop -->
<rect x="80" y="214" width="360" height="120" rx="8" fill="#eef2ff" stroke="#818cf8"/>
<text x="94" y="240" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">foreach (var flavour : buyer.Favourites)</text>
<text x="94" y="270" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">total += buyer.ScoopsOf(flavour) *</text>
<text x="94" y="290" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#6b21a8"> priceFor(flavour);</text>
<text x="94" y="318" font-size="10" fill="#64748b">invocation stays exactly where the code was</text>
<!-- ghost -->
<rect x="94" y="352" width="330" height="26" rx="6" fill="none" stroke="#9333ea" stroke-width="1" stroke-dasharray="4 3"/>
<text x="106" y="370" font-size="10" fill="#9333ea" font-style="italic">λ used to be defined right here</text>
<text x="80" y="398" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">return total;</text>
<!-- slide arrow -->
<line x1="260" y1="350" x2="260" y2="196" stroke="#9333ea" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrP)"/>
<text x="270" y="205" font-size="12" font-weight="bold" fill="#9333ea">slide</text>
<!-- PriceList -->
<rect x="580" y="130" width="150" height="90" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
<text x="655" y="160" font-size="14" font-weight="bold" fill="#dc2626" text-anchor="middle">PriceList</text>
<text x="655" y="180" font-size="11" fill="#7f1d1d" text-anchor="middle">still called —</text>
<text x="655" y="196" font-size="11" fill="#7f1d1d" text-anchor="middle">but only via the λ now</text>
<line x1="444" y1="142" x2="572" y2="162" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

@@ -0,0 +1,54 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
<defs>
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
</marker>
<marker id="arrGr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#16a34a"/>
</marker>
</defs>
<rect width="760" height="460" fill="#ffffff"/>
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 2 — extract the loop; the lambda becomes a parameter</text>
<text x="24" y="56" font-size="13" fill="#16a34a" font-weight="bold">The test passes its own pricing (_ =&gt; 2). PriceList is never touched: deterministic, glanceable.</text>
<!-- test -->
<rect x="50" y="150" width="230" height="140" rx="10" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="66" y="176" font-size="13" font-weight="bold" fill="#14532d">BuyersTests</text>
<text x="66" y="204" font-size="11" font-family="Menlo, Consolas, monospace" fill="#334155">var anke = new Buyer(...)</text>
<text x="66" y="228" font-size="12" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#16a34a">priceFor: _ =&gt; 2</text>
<text x="66" y="252" font-size="11" font-family="Menlo, Consolas, monospace" fill="#14532d">ShoppingListTotal(anke, λ)</text>
<text x="66" y="276" font-size="11" font-family="Menlo, Consolas, monospace" fill="#14532d">Assert.AreEqual(8, ...)</text>
<!-- middle: overload -->
<rect x="330" y="150" width="200" height="180" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="344" y="176" font-size="11" font-weight="bold" fill="#0f172a">ShoppingListTotal(buyer, <tspan fill="#9333ea">λ</tspan>)</text>
<text x="344" y="196" font-size="10" fill="#475569">no-arg overload passes</text>
<text x="344" y="210" font-size="10" fill="#475569">priceList.PriceFor as method group</text>
<text x="344" y="238" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">total = 0</text>
<text x="344" y="262" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">foreach flavour:</text>
<text x="344" y="286" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">total += scoops *</text>
<text x="344" y="306" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b"><tspan fill="#9333ea" font-weight="bold">λ</tspan>(flavour)</text>
<!-- right: the extracted loop, pure -->
<rect x="570" y="150" width="170" height="180" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="584" y="176" font-size="10" font-weight="bold" fill="#0f172a">the pricing loop</text>
<text x="584" y="196" font-size="10" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">inputs: buyer, λ</text>
<text x="584" y="220" font-size="10" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">sum of scoops *</text>
<text x="584" y="238" font-size="10" font-family="Menlo, Consolas, monospace" fill="#1e1b4b"><tspan fill="#9333ea" font-weight="bold">λ</tspan>(flavour)</text>
<text x="584" y="270" font-size="10" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">return total</text>
<text x="584" y="304" font-size="10" fill="#16a34a" font-weight="bold">pure ✓</text>
<!-- green arrows -->
<line x1="284" y1="225" x2="324" y2="225" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrGr)"/>
<text x="286" y="214" font-size="10" fill="#16a34a">λ</text>
<line x1="534" y1="225" x2="564" y2="225" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrGr)"/>
<!-- PriceList, never called -->
<rect x="570" y="370" width="170" height="56" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="4 3" opacity="0.8"/>
<text x="655" y="392" font-size="12" font-weight="bold" fill="#94a3b8" text-anchor="middle">PriceList</text>
<text x="655" y="412" font-size="10" fill="#64748b" text-anchor="middle">never called by the test ✗</text>
<line x1="655" y1="368" x2="655" y2="336" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
<text x="665" y="356" font-size="11" font-weight="bold" fill="#dc2626">✗</text>
<text x="50" y="430" font-size="12" fill="#475569">2 flavours x 2 scoops x 2 = 8 — every run, not just a lucky roll.</text>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB