Rename split-loop-with-lambda to slice-with-lambda
The refactoring is now published as Slice on sammancoaching.org: https://sammancoaching.org/refactorings/slice.html - git mv ideas/split-loop-with-lambda -> ideas/slice-with-lambda - rename learning-hour file and update frontmatter name - update paths in tools/rehearse scripts and report title - update prose: 'Split Loop with Lambda' -> 'Slice with Lambda' in learning hour, exercise READMEs, storyboard, solution comments - link the published Slice refactoring from the pattern draft
This commit is contained in:
@@ -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.
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "10.0.0",
|
||||
"rollForward": "latestFeature",
|
||||
"allowPrerelease": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace IceCreamBuyers
|
||||
{
|
||||
public class Buyer
|
||||
{
|
||||
public Buyer(string name, IReadOnlyList<IceCream> favourites)
|
||||
{
|
||||
Name = name;
|
||||
Favourites = favourites;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public IReadOnlyList<IceCream> Favourites { get; }
|
||||
|
||||
public int ScoopsOf(IceCream flavour)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace IceCreamBuyers
|
||||
{
|
||||
/// <summary>
|
||||
/// Reference end state of exercise 3 (extract lambda, slide, extract method
|
||||
/// with a function parameter — the pure rep of Slice with Lambda).
|
||||
/// </summary>
|
||||
public class Buyers
|
||||
{
|
||||
private readonly PriceList priceList = new();
|
||||
private readonly List<Buyer> buyers = new()
|
||||
{
|
||||
new("Anke", new List<IceCream> { IceCream.Strawberry, IceCream.Vanilla }),
|
||||
new("Bram", new List<IceCream> { IceCream.Chocolate }),
|
||||
new("Celine", new List<IceCream> { IceCream.Vanilla, IceCream.Chocolate }),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// No-arg overload: callers unchanged, the "real" pricing function baked in.
|
||||
/// </summary>
|
||||
public int ShoppingListTotal(string buyerName)
|
||||
{
|
||||
return ShoppingListTotal(ByName(buyerName), priceList.PriceFor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The seam: the pricing source is injected as a function.
|
||||
/// Testable: pass any pure function instead of calling the pricing service.
|
||||
/// </summary>
|
||||
public int ShoppingListTotal(Buyer buyer, Func<IceCream, int> 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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace IceCreamBuyers
|
||||
{
|
||||
public enum IceCream
|
||||
{
|
||||
Strawberry,
|
||||
Chocolate,
|
||||
Vanilla
|
||||
}
|
||||
}
|
||||
@@ -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<IceCream, int> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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> { IceCream.Strawberry, IceCream.Vanilla });
|
||||
Assert.AreEqual(8, buyers.ShoppingListTotal(anke, _ => 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.6.4" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.6.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\src\Buyers.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user