Compare commits
13
Commits
231f6aa72f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac45255a80 | ||
|
|
77ae05c28c | ||
|
|
125bb7ad01 | ||
|
|
d14614f78a | ||
|
|
578162851b | ||
|
|
f9c3c68e79 | ||
|
|
f9bb7b982a | ||
|
|
c153aba61e | ||
|
|
60b1a02ea4 | ||
|
|
679e95eb83 | ||
|
|
1189041d8e | ||
|
|
5bb8121592 | ||
|
|
558ad54516 |
@@ -1,2 +1,4 @@
|
|||||||
website/
|
website/
|
||||||
.yaks
|
.yaks
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
*.user
|
||||||
|
.idea/
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Slice with Lambda — C# exercises
|
||||||
|
|
||||||
|
Exercises for the **Slice with Lambda** learning hour (C# variant).
|
||||||
|
|
||||||
|
- Condensed mechanics: <https://www.sammancoaching.org/refactorings/slice.html>
|
||||||
|
(where that page says mock, we use a lambda instead).
|
||||||
|
- Full Java walkthrough: `../../ideas/slice-with-lambda/storyboard/storyboard.md`.
|
||||||
|
|
||||||
|
Open `SliceWithLambda.sln` in Rider (or run `dotnet test` — see below).
|
||||||
|
Requires the .NET 10 SDK (pinned by `global.json`).
|
||||||
|
|
||||||
|
## What's in the box
|
||||||
|
|
||||||
|
| folder | what | start state |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/SliceWithLambda/` | three demo examples (pristine) | the refactoring **not** done yet |
|
||||||
|
| `src/Buyers/` | the pair exercise (pristine) | one loop, one lambda-able expression |
|
||||||
|
| `solution/` | reference end states (facilitator + fast pairs) | mechanics done, tests stub through the seam |
|
||||||
|
| `tests/` | test projects | see the red/green table |
|
||||||
|
|
||||||
|
## The three demo examples (`src/SliceWithLambda/`)
|
||||||
|
|
||||||
|
| file | guard (client state) | unwanted dependency | lambda | tail |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `MetaphorFree.cs` | `if (a < 0) return 0;` | `_someDependency.mightThrowWhenPreconditionNotSatisfied(a)` | `Func<int, int> readB = a => ...` | `int c = a + b; return c * 2;` |
|
||||||
|
| `WeatherStation.cs` | `if (_wateredToday)` | `_lightSensor.Read()` (byte) | `Func<byte> readLight = () => ...` | sunny -> `WateringDuration.Medium`, shady -> `Short` (reading > 128 => `SunExposure.Sunny`) |
|
||||||
|
| `IceCreamShop.cs` | `if (_thawedToday)` | `_thermometer.Read()` (byte) | `Func<byte> readTemperature = () => ...` | warm -> `ThawAmount.Medium`, cold -> `Low` (temperature > 25 => `Warmth.Warm`) |
|
||||||
|
|
||||||
|
In all three, the guard reads client state (a parameter or a flag the client
|
||||||
|
owns), so it stays in the extracted method. Only the dependency call moves
|
||||||
|
into the lambda.
|
||||||
|
|
||||||
|
`MetaphorFree.cs` is the smallest possible shape: no loop, one dependency
|
||||||
|
call, a tail of one arithmetic step. It carries the capture-to-parameter
|
||||||
|
lesson: the lambda takes `a` as its parameter because a lambda moved up into
|
||||||
|
the function head cannot capture method parameters. `WeatherStation.cs` and
|
||||||
|
`IceCreamShop.cs` are same-shape domain repetitions, so participants can
|
||||||
|
practice extracting the lambda twice — the C# delegate syntax (`Func<byte>`)
|
||||||
|
is worth the repetition.
|
||||||
|
|
||||||
|
## The pair exercise (`src/Buyers/`)
|
||||||
|
|
||||||
|
`Buyers.ShoppingListTotal(buyerName)` computes a buyer's shopping-list
|
||||||
|
total: one loop, and exactly **one** lambda-able expression —
|
||||||
|
`priceList.PriceFor(flavour)`. `PriceList` adds random surge pricing, so the
|
||||||
|
test in `tests/BuyersTests/` is red or flaky by design: it stands for the
|
||||||
|
external pricing service.
|
||||||
|
|
||||||
|
Do the mechanics on the whole method:
|
||||||
|
|
||||||
|
1. Extract the dependency-using code into a lambda and slide it to the top
|
||||||
|
(`Func<IceCream, int> priceFor = flavour => priceList.PriceFor(flavour);`
|
||||||
|
— the invocation in the loop stays put).
|
||||||
|
2. Extract the loop into a method taking the lambda as a parameter.
|
||||||
|
3. Keep a no-arg overload for callers (pass the method group
|
||||||
|
`priceList.PriceFor` as the "real" pricing function) and make the test
|
||||||
|
pass with a pure lambda (`_ => 2`): 2 flavours x 2 scoops x 2 = 8.
|
||||||
|
|
||||||
|
Reference end state: `solution/Buyers/` (green test in
|
||||||
|
`tests/BuyersSolutionTests/`).
|
||||||
|
|
||||||
|
## Tests: red/green map
|
||||||
|
|
||||||
|
| test project | against | expected at start | after the refactoring |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `tests/BuyersTests/` | `src/Buyers/` | red or **flaky** (surge pricing) — that is the point | green once you stub with a pure lambda |
|
||||||
|
| `tests/BuyersSolutionTests/` | `solution/Buyers/` | green | — |
|
||||||
|
| `tests/SliceWithLambdaTests/` | `solution/SliceWithLambda/` | green | — (the demo end states, with pure-lambda stubs, incl. a throwing stub) |
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dotnet test # everything
|
||||||
|
dotnet test tests/BuyersTests # the pair exercise only
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next move (debrief)
|
||||||
|
|
||||||
|
Add `DiscountFor(IceCream)` to `PriceList` and feel why one lambda is no
|
||||||
|
longer enough — extract a role interface (`IPricing`) instead of two lambdas.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SliceWithLambda", "src\SliceWithLambda\SliceWithLambda.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Buyers", "src\Buyers\Buyers.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SliceWithLambdaSolution", "solution\SliceWithLambda\SliceWithLambda.csproj", "{C3D4E5F6-A7B8-9012-CDEF-123456789012}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BuyersSolution", "solution\Buyers\Buyers.csproj", "{D4E5F6A7-B8C9-0123-DEFA-234567890123}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SliceWithLambdaTests", "tests\SliceWithLambdaTests\SliceWithLambdaTests.csproj", "{E5F6A7B8-C9D0-1234-DEFA-234567890123}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BuyersTests", "tests\BuyersTests\BuyersTests.csproj", "{F6A7B8C9-D0E1-2345-EFAB-345678901234}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BuyersSolutionTests", "tests\BuyersSolutionTests\BuyersTests.csproj", "{A7B8C9D0-E1F2-3456-FABC-456789012345}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D4E5F6A7-B8C9-0123-DEFA-234567890123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D4E5F6A7-B8C9-0123-DEFA-234567890123}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D4E5F6A7-B8C9-0123-DEFA-234567890123}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D4E5F6A7-B8C9-0123-DEFA-234567890123}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E5F6A7B8-C9D0-1234-DEFA-234567890123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{E5F6A7B8-C9D0-1234-DEFA-234567890123}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E5F6A7B8-C9D0-1234-DEFA-234567890123}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{E5F6A7B8-C9D0-1234-DEFA-234567890123}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{F6A7B8C9-D0E1-2345-EFAB-345678901234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{F6A7B8C9-D0E1-2345-EFAB-345678901234}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F6A7B8C9-D0E1-2345-EFAB-345678901234}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{F6A7B8C9-D0E1-2345-EFAB-345678901234}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A7B8C9D0-E1F2-3456-FABC-456789012345}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A7B8C9D0-E1F2-3456-FABC-456789012345}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A7B8C9D0-E1F2-3456-FABC-456789012345}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A7B8C9D0-E1F2-3456-FABC-456789012345}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"sdk": {
|
||||||
|
"version": "10.0.400",
|
||||||
|
"rollForward": "latestFeature"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</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,72 @@
|
|||||||
|
// Example 3 solution — Ice cream shop demo of Slice with Lambda (C#), after
|
||||||
|
//
|
||||||
|
// Lambda extracted (step 1), slid to the top (step 2), method extracted
|
||||||
|
// below the guard (step 3). The constructor now takes the lambda directly,
|
||||||
|
// so tests construct with new IceCreamShop(() => 30).
|
||||||
|
//
|
||||||
|
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||||
|
// (where that page says mock, we use a lambda instead).
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
public enum Warmth
|
||||||
|
{
|
||||||
|
Cold,
|
||||||
|
Warm
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ThawAmount
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Medium,
|
||||||
|
Low
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Thermometer
|
||||||
|
{
|
||||||
|
private readonly Random _rng = new Random(); // stand-in for the hardware
|
||||||
|
|
||||||
|
public byte Read()
|
||||||
|
{
|
||||||
|
return (byte)_rng.Next(50); // raw sensor value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IceCreamShop
|
||||||
|
{
|
||||||
|
private readonly Func<byte> _readTemperature; // the thermometer, as a lambda
|
||||||
|
private bool _thawedToday; // event-style client state
|
||||||
|
|
||||||
|
public IceCreamShop(Func<byte> readTemperature)
|
||||||
|
{
|
||||||
|
_readTemperature = readTemperature;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ThawAmount ThawStock()
|
||||||
|
{
|
||||||
|
Func<byte> readTemperature = () => _readTemperature(); // lambda at the top
|
||||||
|
return ThawStockTail(readTemperature); // pass the lambda on
|
||||||
|
}
|
||||||
|
|
||||||
|
private ThawAmount ThawStockTail(Func<byte> readTemperature)
|
||||||
|
{
|
||||||
|
if (_thawedToday) // guard: client state, stays
|
||||||
|
{
|
||||||
|
return ThawAmount.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
_thawedToday = true; // set only after thawing
|
||||||
|
byte temperature = readTemperature(); // middle: the stubbed lambda
|
||||||
|
Warmth warmth = ToWarmth(temperature);
|
||||||
|
if (warmth == Warmth.Warm) // tail: complex behaviour below
|
||||||
|
{ // the extraction point
|
||||||
|
return ThawAmount.Medium;
|
||||||
|
}
|
||||||
|
return ThawAmount.Low;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Warmth ToWarmth(byte temperature)
|
||||||
|
{
|
||||||
|
return temperature > 25 ? Warmth.Warm : Warmth.Cold;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// Example 1 solution — Metaphor-free demo of Slice with Lambda (C#), after
|
||||||
|
//
|
||||||
|
// Lambda extracted (step 1), slid to the top (step 2), method extracted
|
||||||
|
// below the guard (step 3), overload seam added (step 3b), b inlined
|
||||||
|
// (final cleanup).
|
||||||
|
//
|
||||||
|
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||||
|
// (where that page says mock, we use a lambda instead).
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
public class Calculator
|
||||||
|
{
|
||||||
|
private readonly SomeDependency _someDependency;
|
||||||
|
|
||||||
|
public Calculator(SomeDependency someDependency)
|
||||||
|
{
|
||||||
|
_someDependency = someDependency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Calculate(int a) // overload: callers unchanged
|
||||||
|
{
|
||||||
|
return Calculate(a, a =>
|
||||||
|
_someDependency.mightThrowWhenPreconditionNotSatisfied(a));
|
||||||
|
} // the real λ, baked into the head
|
||||||
|
|
||||||
|
public int Calculate(int a, Func<int, int> readB) // the seam: λ is a parameter
|
||||||
|
{
|
||||||
|
if (a < 0) return 0; // guard clause: stays
|
||||||
|
return CalculateTail(a, readB);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int CalculateTail(int a, Func<int, int> readB)
|
||||||
|
{
|
||||||
|
int c = a + readB(a); // b inlined
|
||||||
|
return c * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SomeDependency
|
||||||
|
{
|
||||||
|
private readonly Random _rng = new Random(); // stand-in for unpredictable computation
|
||||||
|
|
||||||
|
public int mightThrowWhenPreconditionNotSatisfied(int a)
|
||||||
|
{
|
||||||
|
if (a > 99)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("precondition not satisfied");
|
||||||
|
}
|
||||||
|
return _rng.Next(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// Example 2 solution — Weather Station domain demo of Slice with Lambda (C#), after
|
||||||
|
//
|
||||||
|
// Lambda extracted (step 1), slid to the top (step 2), method extracted
|
||||||
|
// below the guard (step 3). The constructor now takes the lambda directly,
|
||||||
|
// so tests construct with new SprinklerController(() => 200).
|
||||||
|
//
|
||||||
|
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||||
|
// (where that page says mock, we use a lambda instead).
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
public enum SunExposure
|
||||||
|
{
|
||||||
|
Shady,
|
||||||
|
Sunny
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum WateringDuration
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Medium,
|
||||||
|
Short
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LightSensor
|
||||||
|
{
|
||||||
|
private readonly Random _rng = new Random(); // stand-in for hardware noise
|
||||||
|
|
||||||
|
public byte Read()
|
||||||
|
{
|
||||||
|
return (byte)_rng.Next(256);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SprinklerController
|
||||||
|
{
|
||||||
|
private readonly Func<byte> _readLight; // the sensor, as a lambda
|
||||||
|
private bool _wateredToday; // event-style client state
|
||||||
|
|
||||||
|
public SprinklerController(Func<byte> readLight)
|
||||||
|
{
|
||||||
|
_readLight = readLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WateringDuration ScheduleWatering()
|
||||||
|
{
|
||||||
|
Func<byte> readLight = () => _readLight(); // lambda definition at the top
|
||||||
|
return ScheduleWateringTail(readLight); // pass the lambda on
|
||||||
|
}
|
||||||
|
|
||||||
|
private WateringDuration ScheduleWateringTail(Func<byte> readLight)
|
||||||
|
{
|
||||||
|
if (_wateredToday) // guard: client state, stays
|
||||||
|
{
|
||||||
|
return WateringDuration.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
_wateredToday = true; // set only after watering
|
||||||
|
byte reading = readLight(); // middle: the stubbed lambda
|
||||||
|
SunExposure exposure = ToSunExposure(reading);
|
||||||
|
if (exposure == SunExposure.Sunny) // tail: complex behaviour below
|
||||||
|
{ // the extraction point
|
||||||
|
return WateringDuration.Medium;
|
||||||
|
}
|
||||||
|
return WateringDuration.Short;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SunExposure ToSunExposure(byte reading)
|
||||||
|
{
|
||||||
|
return reading > 128 ? SunExposure.Sunny : SunExposure.Shady;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace IceCreamBuyers
|
||||||
|
{
|
||||||
|
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 }),
|
||||||
|
};
|
||||||
|
|
||||||
|
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}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</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,79 @@
|
|||||||
|
// Example 3 — Ice cream shop demo of Slice with Lambda (C#)
|
||||||
|
//
|
||||||
|
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||||
|
// (where that page says mock, we use a lambda instead).
|
||||||
|
//
|
||||||
|
// Third repetition, same complexity as ScheduleWatering in WeatherStation.cs
|
||||||
|
// (guard -> one dependency call -> map to enum -> constant tail), so
|
||||||
|
// participants can practice extracting a lambda a second time. The C#
|
||||||
|
// delegate syntax is worth the repetition:
|
||||||
|
//
|
||||||
|
// Func<byte> readTemperature = () => _thermometer.Read();
|
||||||
|
//
|
||||||
|
// Cast:
|
||||||
|
// Thermometer — hardware-ish dependency. Read() returns a raw
|
||||||
|
// byte value (not calibrated, the client decides
|
||||||
|
// what it means).
|
||||||
|
// Warmth — enum. Reading > 25 => Warm, otherwise Cold.
|
||||||
|
// (Sunny/Shady's cousin from WeatherStation.cs.)
|
||||||
|
// ThawAmount — enum. None / Medium / Low.
|
||||||
|
// IceCreamShop — the client. Owns the _thawedToday flag, so the
|
||||||
|
// guard is about client state, not sensor state.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
public enum Warmth
|
||||||
|
{
|
||||||
|
Cold,
|
||||||
|
Warm
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ThawAmount
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Medium,
|
||||||
|
Low
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Thermometer
|
||||||
|
{
|
||||||
|
private readonly Random _rng = new Random(); // stand-in for the hardware
|
||||||
|
|
||||||
|
public byte Read()
|
||||||
|
{
|
||||||
|
return (byte)_rng.Next(50); // raw sensor value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IceCreamShop
|
||||||
|
{
|
||||||
|
private readonly Thermometer _thermometer;
|
||||||
|
private bool _thawedToday; // event-style client state
|
||||||
|
|
||||||
|
public IceCreamShop(Thermometer thermometer)
|
||||||
|
{
|
||||||
|
_thermometer = thermometer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ThawAmount ThawStock()
|
||||||
|
{
|
||||||
|
if (_thawedToday) // guard: client state, stays
|
||||||
|
{
|
||||||
|
return ThawAmount.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
_thawedToday = true; // set only after thawing
|
||||||
|
byte temperature = _thermometer.Read(); // middle: unwanted dependency
|
||||||
|
Warmth warmth = ToWarmth(temperature);
|
||||||
|
if (warmth == Warmth.Warm) // tail: complex behaviour below
|
||||||
|
{ // the extraction point
|
||||||
|
return ThawAmount.Medium;
|
||||||
|
}
|
||||||
|
return ThawAmount.Low;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Warmth ToWarmth(byte temperature)
|
||||||
|
{
|
||||||
|
return temperature > 25 ? Warmth.Warm : Warmth.Cold;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// Example 1 — Metaphor-free demo of Slice with Lambda (C#)
|
||||||
|
//
|
||||||
|
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||||
|
// (where that page says mock, we use a lambda instead).
|
||||||
|
//
|
||||||
|
// Shape of the method (per zeroth-draft-of-refactoring-pattern.md):
|
||||||
|
//
|
||||||
|
// guard clauses — must run first
|
||||||
|
// middle: dependency — one call on an object we don't want in the
|
||||||
|
// extracted method
|
||||||
|
// tail: depends on b — uses the value the dependency produced
|
||||||
|
//
|
||||||
|
// Deliberately NOT a loop: this is the smallest method that shows the
|
||||||
|
// mechanics. The dependency both validates its precondition (using the
|
||||||
|
// parameter a) and produces b — one expression, one thing to move into
|
||||||
|
// the lambda.
|
||||||
|
//
|
||||||
|
// Unwanted dependency: SomeDependency — might throw when its precondition
|
||||||
|
// on a is not satisfied, and returns an unpredictable value. An approval
|
||||||
|
// test on Calculate is flaky until the lambda is stubbed.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
public class Calculator
|
||||||
|
{
|
||||||
|
private readonly SomeDependency _someDependency;
|
||||||
|
|
||||||
|
public Calculator(SomeDependency someDependency)
|
||||||
|
{
|
||||||
|
_someDependency = someDependency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Calculate(int a)
|
||||||
|
{
|
||||||
|
if (a < 0) return 0; // guard clause: stays
|
||||||
|
int b = _someDependency.mightThrowWhenPreconditionNotSatisfied(a); // middle: unwanted dependency
|
||||||
|
int c = a + b; // tail: depends on b
|
||||||
|
return c * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SomeDependency
|
||||||
|
{
|
||||||
|
private readonly Random _rng = new Random(); // stand-in for unpredictable computation
|
||||||
|
|
||||||
|
public int mightThrowWhenPreconditionNotSatisfied(int a)
|
||||||
|
{
|
||||||
|
if (a > 99)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("precondition not satisfied");
|
||||||
|
}
|
||||||
|
return _rng.Next(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// Example 2 — Weather Station domain demo of Slice with Lambda (C#)
|
||||||
|
//
|
||||||
|
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||||
|
// (where that page says mock, we use a lambda instead).
|
||||||
|
//
|
||||||
|
// Cast (Head First Design Patterns Weather Station, reduced):
|
||||||
|
// LightSensor — hardware-ish dependency. Read() returns a byte.
|
||||||
|
// SunExposure — enum. Reading > 128 => Sunny, otherwise Shady.
|
||||||
|
// WateringDuration — enum. None / Medium / Short.
|
||||||
|
// (Long is YAGNI until the for-loop version of this
|
||||||
|
// exercise exists.)
|
||||||
|
// SprinklerController — the client. Owns the _wateredToday flag, so the
|
||||||
|
// guard is about client state, not sensor state.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
public enum SunExposure
|
||||||
|
{
|
||||||
|
Shady,
|
||||||
|
Sunny
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum WateringDuration
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Medium,
|
||||||
|
Short
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LightSensor
|
||||||
|
{
|
||||||
|
private readonly Random _rng = new Random(); // stand-in for hardware noise
|
||||||
|
|
||||||
|
public byte Read()
|
||||||
|
{
|
||||||
|
return (byte)_rng.Next(256);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SprinklerController
|
||||||
|
{
|
||||||
|
private readonly LightSensor _lightSensor;
|
||||||
|
private bool _wateredToday; // event-style client state
|
||||||
|
|
||||||
|
public SprinklerController(LightSensor lightSensor)
|
||||||
|
{
|
||||||
|
_lightSensor = lightSensor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WateringDuration ScheduleWatering()
|
||||||
|
{
|
||||||
|
if (_wateredToday) // guard: client state, stays
|
||||||
|
{
|
||||||
|
return WateringDuration.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
_wateredToday = true; // set only after watering
|
||||||
|
byte reading = _lightSensor.Read(); // middle: unwanted dependency
|
||||||
|
SunExposure exposure = ToSunExposure(reading);
|
||||||
|
if (exposure == SunExposure.Sunny) // tail: complex behaviour below
|
||||||
|
{ // the extraction point
|
||||||
|
return WateringDuration.Medium;
|
||||||
|
}
|
||||||
|
return WateringDuration.Short;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SunExposure ToSunExposure(byte reading)
|
||||||
|
{
|
||||||
|
return reading > 128 ? SunExposure.Sunny : SunExposure.Shady;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<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="..\..\solution\Buyers\Buyers.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<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\Buyers.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// Tests for the three Slice-with-Lambda solutions — step 4: the lambda as a stub.
|
||||||
|
//
|
||||||
|
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||||
|
// (where that page says mock, we use a lambda instead).
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class SliceWithLambdaTests
|
||||||
|
{
|
||||||
|
// --- Example 1: metaphor-free (Calculator) ---
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Guards_against_negative_input()
|
||||||
|
{
|
||||||
|
var calc = new Calculator(new SomeDependency()); // the real, flaky one...
|
||||||
|
Assert.Equal(0, calc.Calculate(-1, a => 42)); // ...never asked: stub not invoked
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Throws_when_precondition_not_satisfied()
|
||||||
|
{
|
||||||
|
var calc = new Calculator(new SomeDependency());
|
||||||
|
Assert.Throws<InvalidOperationException>(
|
||||||
|
() => calc.Calculate(7, a => throw new InvalidOperationException()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Combines_input_and_the_stubbed_value()
|
||||||
|
{
|
||||||
|
var calc = new Calculator(new SomeDependency());
|
||||||
|
Assert.Equal(84, calc.Calculate(0, a => 42)); // stub through the seam
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Example 2: Weather Station (SprinklerController) ---
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Skips_watering_when_already_watered()
|
||||||
|
{
|
||||||
|
var controller = new SprinklerController(() => 200); // pure lambda: no sensor
|
||||||
|
controller.ScheduleWatering(); // water once...
|
||||||
|
Assert.Equal(WateringDuration.None, controller.ScheduleWatering());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Waters_longer_in_full_sun()
|
||||||
|
{
|
||||||
|
var controller = new SprinklerController(() => 200); // sunny: > 128
|
||||||
|
Assert.Equal(WateringDuration.Medium, controller.ScheduleWatering());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Waters_less_in_the_shade()
|
||||||
|
{
|
||||||
|
var controller = new SprinklerController(() => 10); // shady: ≤ 128
|
||||||
|
Assert.Equal(WateringDuration.Short, controller.ScheduleWatering());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Example 3: Ice Cream Shop (IceCreamShop) ---
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Skips_thawing_when_already_thawed()
|
||||||
|
{
|
||||||
|
var shop = new IceCreamShop(() => 30); // pure lambda: no freezer
|
||||||
|
shop.ThawStock(); // thaw once...
|
||||||
|
Assert.Equal(ThawAmount.None, shop.ThawStock());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Thaws_more_when_warm()
|
||||||
|
{
|
||||||
|
var shop = new IceCreamShop(() => 30); // warm: > 25
|
||||||
|
Assert.Equal(ThawAmount.Medium, shop.ThawStock());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Thaws_less_when_cold()
|
||||||
|
{
|
||||||
|
var shop = new IceCreamShop(() => 5); // cold: ≤ 25
|
||||||
|
Assert.Equal(ThawAmount.Low, shop.ThawStock());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\solution\SliceWithLambda\SliceWithLambda.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Submodule
+1
Submodule export/slice-with-lambda added at 17e5d25b39
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# Exercise folder — Split Loop with Lambda (learning hour)
|
# Exercise folder — Slice with Lambda (learning hour)
|
||||||
|
|
||||||
Contents:
|
Contents:
|
||||||
|
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
// Example 3 — Ice cream shop demo of Slice with Lambda (C#)
|
||||||
|
//
|
||||||
|
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||||
|
// (where that page says mock, we use a lambda instead).
|
||||||
|
//
|
||||||
|
// Third repetition, same complexity as ScheduleWatering in WeatherStation.cs
|
||||||
|
// (guard -> one dependency call -> map to enum -> constant tail), so
|
||||||
|
// participants can practice extracting a lambda a second time. The C#
|
||||||
|
// delegate syntax is worth the repetition:
|
||||||
|
//
|
||||||
|
// Func<byte> readTemperature = () => _thermometer.Read();
|
||||||
|
//
|
||||||
|
// Cast:
|
||||||
|
// Thermometer — hardware-ish dependency. Read() returns a raw
|
||||||
|
// byte value (not calibrated, the client decides
|
||||||
|
// what it means).
|
||||||
|
// Warmth — enum. Reading > 25 => Warm, otherwise Cold.
|
||||||
|
// (Sunny/Shady's cousin from WeatherStation.cs.)
|
||||||
|
// ThawAmount — enum. None / Medium / Low.
|
||||||
|
// IceCreamShop — the client. Owns the _thawedToday flag, so the
|
||||||
|
// guard is about client state, not sensor state.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
public enum Warmth
|
||||||
|
{
|
||||||
|
Cold,
|
||||||
|
Warm
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ThawAmount
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Medium,
|
||||||
|
Low
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Thermometer
|
||||||
|
{
|
||||||
|
private readonly Random _rng = new Random(); // stand-in for the hardware
|
||||||
|
|
||||||
|
public byte Read()
|
||||||
|
{
|
||||||
|
return (byte)_rng.Next(50); // raw sensor value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IceCreamShop
|
||||||
|
{
|
||||||
|
private readonly Thermometer _thermometer;
|
||||||
|
private bool _thawedToday; // event-style client state
|
||||||
|
|
||||||
|
public IceCreamShop(Thermometer thermometer)
|
||||||
|
{
|
||||||
|
_thermometer = thermometer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ThawAmount ThawStock()
|
||||||
|
{
|
||||||
|
if (_thawedToday) // guard: client state, stays
|
||||||
|
{
|
||||||
|
return ThawAmount.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
_thawedToday = true; // set only after thawing
|
||||||
|
byte temperature = _thermometer.Read(); // middle: unwanted dependency
|
||||||
|
Warmth warmth = ToWarmth(temperature);
|
||||||
|
if (warmth == Warmth.Warm) // tail: complex behaviour below
|
||||||
|
{ // the extraction point
|
||||||
|
return ThawAmount.Medium;
|
||||||
|
}
|
||||||
|
return ThawAmount.Low;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Warmth ToWarmth(byte temperature)
|
||||||
|
{
|
||||||
|
return temperature > 25 ? Warmth.Warm : Warmth.Cold;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// Example 1 — Metaphor-free demo of Slice with Lambda (C#)
|
||||||
|
//
|
||||||
|
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||||
|
// (where that page says mock, we use a lambda instead).
|
||||||
|
//
|
||||||
|
// Shape of the method (per zeroth-draft-of-refactoring-pattern.md):
|
||||||
|
//
|
||||||
|
// guard clauses — must run first
|
||||||
|
// middle: dependency — one call on an object we don't want in the
|
||||||
|
// extracted method
|
||||||
|
// tail: depends on b — uses the value the dependency produced
|
||||||
|
//
|
||||||
|
// Deliberately NOT a loop: this is the smallest method that shows the
|
||||||
|
// mechanics. The dependency both validates its precondition (using the
|
||||||
|
// parameter a) and produces b — one expression, one thing to move into
|
||||||
|
// the lambda.
|
||||||
|
//
|
||||||
|
// Unwanted dependency: SomeDependency — might throw when its precondition
|
||||||
|
// on a is not satisfied, and returns an unpredictable value. An approval
|
||||||
|
// test on Calculate is flaky until the lambda is stubbed.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
public class Calculator
|
||||||
|
{
|
||||||
|
private readonly SomeDependency _someDependency;
|
||||||
|
|
||||||
|
public Calculator(SomeDependency someDependency)
|
||||||
|
{
|
||||||
|
_someDependency = someDependency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Calculate(int a)
|
||||||
|
{
|
||||||
|
if (a < 0) return 0; // guard clause: stays
|
||||||
|
int b = _someDependency.mightThrowWhenPreconditionNotSatisfied(a); // middle: unwanted dependency
|
||||||
|
int c = a + b; // tail: depends on b
|
||||||
|
return c * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SomeDependency
|
||||||
|
{
|
||||||
|
private readonly Random _rng = new Random(); // stand-in for unpredictable computation
|
||||||
|
|
||||||
|
public int mightThrowWhenPreconditionNotSatisfied(int a)
|
||||||
|
{
|
||||||
|
if (a > 99)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("precondition not satisfied");
|
||||||
|
}
|
||||||
|
return _rng.Next(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Slice with Lambda — C# demo examples
|
||||||
|
|
||||||
|
Three C# demo examples for the **Slice with Lambda** refactoring.
|
||||||
|
Condensed mechanics: https://www.sammancoaching.org/refactorings/slice.html
|
||||||
|
(where that page says mock, we use a lambda instead).
|
||||||
|
Full Java walkthrough: `../../storyboard/storyboard.md`.
|
||||||
|
Slides: `~/qwan/slides-rjs/decks/refactoring/slice-with-lambda/slides.md`, served at http://localhost:9001/#/refactoring/slice-with-lambda.
|
||||||
|
|
||||||
|
## The three examples
|
||||||
|
|
||||||
|
| file | guard (client state) | unwanted dependency | lambda | tail |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `MetaphorFree.cs` | `if (a < 0) return 0;` | `_someDependency.mightThrowWhenPreconditionNotSatisfied(a)` | `Func<int, int> readB = a => ...` | `int c = a + b; return c * 2;` |
|
||||||
|
| `WeatherStation.cs` | `if (_wateredToday)` | `_lightSensor.Read()` (byte) | `Func<byte> readLight = () => ...` | sunny -> `WateringDuration.Medium`, shady -> `Short` (reading > 128 => `SunExposure.Sunny`) |
|
||||||
|
| `IceCreamShop.cs` | `if (_thawedToday)` | `_thermometer.Read()` (byte) | `Func<byte> readTemperature = () => ...` | warm -> `ThawAmount.Medium`, cold -> `Low` (temperature > 25 => `Warmth.Warm`) |
|
||||||
|
|
||||||
|
In all three, the guard reads client state (a parameter or a flag the client
|
||||||
|
owns), so it stays in the extracted method. Only the dependency call moves
|
||||||
|
into the lambda.
|
||||||
|
|
||||||
|
## The ramp
|
||||||
|
|
||||||
|
`MetaphorFree.cs` is the smallest possible shape: no loop, one dependency
|
||||||
|
call, a tail of one arithmetic step. It carries the capture-to-parameter
|
||||||
|
lesson: the lambda takes `a` as its parameter (`readB(a)`) because a lambda
|
||||||
|
moved up into the function head cannot capture method parameters. Its
|
||||||
|
step-4 stub tests include a throwing stub.
|
||||||
|
|
||||||
|
`WeatherStation.cs` and `IceCreamShop.cs` are same-shape domain
|
||||||
|
repetitions — guard, one dependency call, map to enum, constant tail — so
|
||||||
|
participants can practice extracting the lambda twice. The C# delegate
|
||||||
|
syntax (`Func<byte>`) is worth the repetition.
|
||||||
|
|
||||||
|
## Same three regions every time
|
||||||
|
|
||||||
|
Each example deliberately has the same three regions:
|
||||||
|
|
||||||
|
- **before**: guard clauses
|
||||||
|
- **middle**: one dependency call
|
||||||
|
- **after**: tail that depends on the value the dependency produced
|
||||||
|
|
||||||
|
Matching the pattern: extract the dependency-using code into a lambda,
|
||||||
|
slide the definition to the top, extract the method below it, pass the
|
||||||
|
lambda as parameter, test with pure lambda stubs.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- solution/ has its own project (same class names) -->
|
||||||
|
<Compile Remove="solution/**/*.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<Solution>
|
||||||
|
<Folder Name="/solution/">
|
||||||
|
<Project Path="solution/SliceWithLambdaSolution.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Project Path="SliceWithLambda.csproj" />
|
||||||
|
</Solution>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// Example 2 — Weather Station domain demo of Slice with Lambda (C#)
|
||||||
|
//
|
||||||
|
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||||
|
// (where that page says mock, we use a lambda instead).
|
||||||
|
//
|
||||||
|
// Cast (Head First Design Patterns Weather Station, reduced):
|
||||||
|
// LightSensor — hardware-ish dependency. Read() returns a byte.
|
||||||
|
// SunExposure — enum. Reading > 128 => Sunny, otherwise Shady.
|
||||||
|
// WateringDuration — enum. None / Medium / Short.
|
||||||
|
// (Long is YAGNI until the for-loop version of this
|
||||||
|
// exercise exists.)
|
||||||
|
// SprinklerController — the client. Owns the _wateredToday flag, so the
|
||||||
|
// guard is about client state, not sensor state.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
public enum SunExposure
|
||||||
|
{
|
||||||
|
Shady,
|
||||||
|
Sunny
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum WateringDuration
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Medium,
|
||||||
|
Short
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LightSensor
|
||||||
|
{
|
||||||
|
private readonly Random _rng = new Random(); // stand-in for hardware noise
|
||||||
|
|
||||||
|
public byte Read()
|
||||||
|
{
|
||||||
|
return (byte)_rng.Next(256);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SprinklerController
|
||||||
|
{
|
||||||
|
private readonly LightSensor _lightSensor;
|
||||||
|
private bool _wateredToday; // event-style client state
|
||||||
|
|
||||||
|
public SprinklerController(LightSensor lightSensor)
|
||||||
|
{
|
||||||
|
_lightSensor = lightSensor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WateringDuration ScheduleWatering()
|
||||||
|
{
|
||||||
|
if (_wateredToday) // guard: client state, stays
|
||||||
|
{
|
||||||
|
return WateringDuration.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
_wateredToday = true; // set only after watering
|
||||||
|
byte reading = _lightSensor.Read(); // middle: unwanted dependency
|
||||||
|
SunExposure exposure = ToSunExposure(reading);
|
||||||
|
if (exposure == SunExposure.Sunny) // tail: complex behaviour below
|
||||||
|
{ // the extraction point
|
||||||
|
return WateringDuration.Medium;
|
||||||
|
}
|
||||||
|
return WateringDuration.Short;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SunExposure ToSunExposure(byte reading)
|
||||||
|
{
|
||||||
|
return reading > 128 ? SunExposure.Sunny : SunExposure.Shady;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// Example 3 solution — Ice cream shop demo of Slice with Lambda (C#), after
|
||||||
|
//
|
||||||
|
// Lambda extracted (step 1), slid to the top (step 2), method extracted
|
||||||
|
// below the guard (step 3). The constructor now takes the lambda directly,
|
||||||
|
// so tests construct with new IceCreamShop(() => 30).
|
||||||
|
//
|
||||||
|
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||||
|
// (where that page says mock, we use a lambda instead).
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
public enum Warmth
|
||||||
|
{
|
||||||
|
Cold,
|
||||||
|
Warm
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ThawAmount
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Medium,
|
||||||
|
Low
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Thermometer
|
||||||
|
{
|
||||||
|
private readonly Random _rng = new Random(); // stand-in for the hardware
|
||||||
|
|
||||||
|
public byte Read()
|
||||||
|
{
|
||||||
|
return (byte)_rng.Next(50); // raw sensor value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IceCreamShop
|
||||||
|
{
|
||||||
|
private readonly Func<byte> _readTemperature; // the thermometer, as a lambda
|
||||||
|
private bool _thawedToday; // event-style client state
|
||||||
|
|
||||||
|
public IceCreamShop(Func<byte> readTemperature)
|
||||||
|
{
|
||||||
|
_readTemperature = readTemperature;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ThawAmount ThawStock()
|
||||||
|
{
|
||||||
|
Func<byte> readTemperature = () => _readTemperature(); // lambda at the top
|
||||||
|
return ThawStockTail(readTemperature); // pass the lambda on
|
||||||
|
}
|
||||||
|
|
||||||
|
private ThawAmount ThawStockTail(Func<byte> readTemperature)
|
||||||
|
{
|
||||||
|
if (_thawedToday) // guard: client state, stays
|
||||||
|
{
|
||||||
|
return ThawAmount.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
_thawedToday = true; // set only after thawing
|
||||||
|
byte temperature = readTemperature(); // middle: the stubbed lambda
|
||||||
|
Warmth warmth = ToWarmth(temperature);
|
||||||
|
if (warmth == Warmth.Warm) // tail: complex behaviour below
|
||||||
|
{ // the extraction point
|
||||||
|
return ThawAmount.Medium;
|
||||||
|
}
|
||||||
|
return ThawAmount.Low;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Warmth ToWarmth(byte temperature)
|
||||||
|
{
|
||||||
|
return temperature > 25 ? Warmth.Warm : Warmth.Cold;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// Example 1 solution — Metaphor-free demo of Slice with Lambda (C#), after
|
||||||
|
//
|
||||||
|
// Lambda extracted (step 1), slid to the top (step 2), method extracted
|
||||||
|
// below the guard (step 3), overload seam added (step 3b), b inlined
|
||||||
|
// (final cleanup).
|
||||||
|
//
|
||||||
|
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||||
|
// (where that page says mock, we use a lambda instead).
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
public class Calculator
|
||||||
|
{
|
||||||
|
private readonly SomeDependency _someDependency;
|
||||||
|
|
||||||
|
public Calculator(SomeDependency someDependency)
|
||||||
|
{
|
||||||
|
_someDependency = someDependency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Calculate(int a) // overload: callers unchanged
|
||||||
|
{
|
||||||
|
return Calculate(a, a =>
|
||||||
|
_someDependency.mightThrowWhenPreconditionNotSatisfied(a));
|
||||||
|
} // the real λ, baked into the head
|
||||||
|
|
||||||
|
public int Calculate(int a, Func<int, int> readB) // the seam: λ is a parameter
|
||||||
|
{
|
||||||
|
if (a < 0) return 0; // guard clause: stays
|
||||||
|
return CalculateTail(a, readB);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int CalculateTail(int a, Func<int, int> readB)
|
||||||
|
{
|
||||||
|
int c = a + readB(a); // b inlined
|
||||||
|
return c * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SomeDependency
|
||||||
|
{
|
||||||
|
private readonly Random _rng = new Random(); // stand-in for unpredictable computation
|
||||||
|
|
||||||
|
public int mightThrowWhenPreconditionNotSatisfied(int a)
|
||||||
|
{
|
||||||
|
if (a > 99)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("precondition not satisfied");
|
||||||
|
}
|
||||||
|
return _rng.Next(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<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="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// Tests for the three Slice-with-Lambda solutions — step 4: the lambda as a stub.
|
||||||
|
//
|
||||||
|
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||||
|
// (where that page says mock, we use a lambda instead).
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class SliceWithLambdaTests
|
||||||
|
{
|
||||||
|
// --- Example 1: metaphor-free (Calculator) ---
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Guards_against_negative_input()
|
||||||
|
{
|
||||||
|
var calc = new Calculator(new SomeDependency()); // the real, flaky one...
|
||||||
|
Assert.Equal(0, calc.Calculate(-1, a => 42)); // ...never asked: stub not invoked
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Throws_when_precondition_not_satisfied()
|
||||||
|
{
|
||||||
|
var calc = new Calculator(new SomeDependency());
|
||||||
|
Assert.Throws<InvalidOperationException>(
|
||||||
|
() => calc.Calculate(7, a => throw new InvalidOperationException()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Combines_input_and_the_stubbed_value()
|
||||||
|
{
|
||||||
|
var calc = new Calculator(new SomeDependency());
|
||||||
|
Assert.Equal(84, calc.Calculate(0, a => 42)); // stub through the seam
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Example 2: Weather Station (SprinklerController) ---
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Skips_watering_when_already_watered()
|
||||||
|
{
|
||||||
|
var controller = new SprinklerController(() => 200); // pure lambda: no sensor
|
||||||
|
controller.ScheduleWatering(); // water once...
|
||||||
|
Assert.Equal(WateringDuration.None, controller.ScheduleWatering());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Waters_longer_in_full_sun()
|
||||||
|
{
|
||||||
|
var controller = new SprinklerController(() => 200); // sunny: > 128
|
||||||
|
Assert.Equal(WateringDuration.Medium, controller.ScheduleWatering());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Waters_less_in_the_shade()
|
||||||
|
{
|
||||||
|
var controller = new SprinklerController(() => 10); // shady: ≤ 128
|
||||||
|
Assert.Equal(WateringDuration.Short, controller.ScheduleWatering());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Example 3: Ice Cream Shop (IceCreamShop) ---
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Skips_thawing_when_already_thawed()
|
||||||
|
{
|
||||||
|
var shop = new IceCreamShop(() => 30); // pure lambda: no freezer
|
||||||
|
shop.ThawStock(); // thaw once...
|
||||||
|
Assert.Equal(ThawAmount.None, shop.ThawStock());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Thaws_more_when_warm()
|
||||||
|
{
|
||||||
|
var shop = new IceCreamShop(() => 30); // warm: > 25
|
||||||
|
Assert.Equal(ThawAmount.Medium, shop.ThawStock());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Thaws_less_when_cold()
|
||||||
|
{
|
||||||
|
var shop = new IceCreamShop(() => 5); // cold: ≤ 25
|
||||||
|
Assert.Equal(ThawAmount.Low, shop.ThawStock());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// Example 2 solution — Weather Station domain demo of Slice with Lambda (C#), after
|
||||||
|
//
|
||||||
|
// Lambda extracted (step 1), slid to the top (step 2), method extracted
|
||||||
|
// below the guard (step 3). The constructor now takes the lambda directly,
|
||||||
|
// so tests construct with new SprinklerController(() => 200).
|
||||||
|
//
|
||||||
|
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||||
|
// (where that page says mock, we use a lambda instead).
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
public enum SunExposure
|
||||||
|
{
|
||||||
|
Shady,
|
||||||
|
Sunny
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum WateringDuration
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Medium,
|
||||||
|
Short
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LightSensor
|
||||||
|
{
|
||||||
|
private readonly Random _rng = new Random(); // stand-in for hardware noise
|
||||||
|
|
||||||
|
public byte Read()
|
||||||
|
{
|
||||||
|
return (byte)_rng.Next(256);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SprinklerController
|
||||||
|
{
|
||||||
|
private readonly Func<byte> _readLight; // the sensor, as a lambda
|
||||||
|
private bool _wateredToday; // event-style client state
|
||||||
|
|
||||||
|
public SprinklerController(Func<byte> readLight)
|
||||||
|
{
|
||||||
|
_readLight = readLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WateringDuration ScheduleWatering()
|
||||||
|
{
|
||||||
|
Func<byte> readLight = () => _readLight(); // lambda definition at the top
|
||||||
|
return ScheduleWateringTail(readLight); // pass the lambda on
|
||||||
|
}
|
||||||
|
|
||||||
|
private WateringDuration ScheduleWateringTail(Func<byte> readLight)
|
||||||
|
{
|
||||||
|
if (_wateredToday) // guard: client state, stays
|
||||||
|
{
|
||||||
|
return WateringDuration.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
_wateredToday = true; // set only after watering
|
||||||
|
byte reading = readLight(); // middle: the stubbed lambda
|
||||||
|
SunExposure exposure = ToSunExposure(reading);
|
||||||
|
if (exposure == SunExposure.Sunny) // tail: complex behaviour below
|
||||||
|
{ // the extraction point
|
||||||
|
return WateringDuration.Medium;
|
||||||
|
}
|
||||||
|
return WateringDuration.Short;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SunExposure ToSunExposure(byte reading)
|
||||||
|
{
|
||||||
|
return reading > 128 ? SunExposure.Sunny : SunExposure.Shady;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# IceCreamForecasting — Split Loop with Lambda (exercise)
|
# IceCreamForecasting — Slice with Lambda (exercise)
|
||||||
|
|
||||||
Pre-factored variant of [IceCreamForecasting-Refactoring-Kata](https://github.com/emilybache/IceCreamForecasting-Refactoring-Kata):
|
Pre-factored variant of [IceCreamForecasting-Refactoring-Kata](https://github.com/emilybache/IceCreamForecasting-Refactoring-Kata):
|
||||||
the Q1/Q4 dead branch is stripped and the hardcoded dates are a constant now.
|
the Q1/Q4 dead branch is stripped and the hardcoded dates are a constant now.
|
||||||
@@ -14,7 +14,7 @@ Get it under test:
|
|||||||
|
|
||||||
1. First get any test running that calls `longRangeForecast()` —
|
1. First get any test running that calls `longRangeForecast()` —
|
||||||
it will fail or be flaky, because of the dependency on `Scorer`.
|
it will fail or be flaky, because of the dependency on `Scorer`.
|
||||||
2. Then use the **Split Loop with Lambda** mechanics (6 steps, on the handout)
|
2. Then use the **Slice with Lambda** mechanics (6 steps, on the handout)
|
||||||
so that the weather collection becomes an extracted method that takes the
|
so that the weather collection becomes an extracted method that takes the
|
||||||
weather-reading as a **lambda parameter**.
|
weather-reading as a **lambda parameter**.
|
||||||
3. Stop when you have an extracted method that takes the lambda as a parameter,
|
3. Stop when you have an extracted method that takes the lambda as a parameter,
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
# Rehearsal of the exercise with coding agents — full summary
|
# Rehearsal of the exercise with coding agents — full summary
|
||||||
|
|
||||||
**Date:** 2026-09-27 · **Harness:** `tools/rehearse/` · **Raw per-run artifacts:** [`rehearsal-runs/`](./)
|
**Date:** 2026-09-27 · **Harness:** `tools/rehearse/` · **Raw per-run artifacts:** [`rehearsal-runs/`](./)
|
||||||
· **Exercise:** Split Loop with Lambda (IceCreamForecasting kata, `LongRangeForecasts`)
|
· **Exercise:** Slice with Lambda (IceCreamForecasting kata, `LongRangeForecasts`)
|
||||||
|
|
||||||
Six scripted sub-agent runs did the refactoring under different tool rules and
|
Six scripted sub-agent runs did the refactoring under different tool rules and
|
||||||
two models. Each run got a fresh workspace, a shared AGENTS.md (identical
|
two models. Each run got a fresh workspace, a shared AGENTS.md (identical
|
||||||
+1
-1
@@ -7,7 +7,7 @@ import java.util.function.LongFunction;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference end state of the learning hour exercise (step 3 of the
|
* Reference end state of the learning hour exercise (step 3 of the
|
||||||
* Split Loop with Lambda mechanics — stop here; step 4, moving the method
|
* Slice with Lambda mechanics — stop here; step 4, moving the method
|
||||||
* to another class, is future work).
|
* to another class, is future work).
|
||||||
*
|
*
|
||||||
* Derived from https://github.com/emilybache/IceCreamForecasting-Refactoring-Kata
|
* Derived from https://github.com/emilybache/IceCreamForecasting-Refactoring-Kata
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user